diff --git a/README.md b/README.md index b15406e6..7e9eeb1c 100644 --- a/README.md +++ b/README.md @@ -473,8 +473,12 @@ Keyboard controls use physical key positions (`KeyboardEvent.code`): | Slip left / slip right | `Z` / `X` | | Weave / pull | `C` / `V` | | Clinch / switch stance | `B` / `N` | -| Low blow / headbutt | `1` / `2` | +| Taunt | `M` | +| Low blow / headbutt (deliberate fouls) | `Shift+1` / `Shift+2` | | Private get-up rhythm | `Left Arrow` / `Right Arrow` | +| Corner instruction, between rounds | `1` close the cut · `2` bring down the swelling · `3` catch your breath | +| Style, in the pick before the bout | Arrow keys choose, `Enter` settles; `1`–`5` settle on that card at once | +| Camera | `K` cycles broadcast, close and over the shoulder | Standard-controller controls are: @@ -490,6 +494,8 @@ Standard-controller controls are: | Clinch / switch stance | Left-stick press / right-stick press | | Low blow / headbutt | View/Back / Menu/Start | | Private get-up rhythm | While down, press D-pad left / right; it registers immediately rather than waiting for release | +| Corner instruction, between rounds | Left (`X`/Square) close the cut · top (`Y`/Triangle) bring down the swelling · right (`B`/Circle) catch your breath | +| Style, in the pick before the bout | D-pad or left stick choose; bottom (`A`/Cross) settles | The right stick also performs one punch after a center→peak→center gesture. Left/right direction selects the hand. Using the absolute angle from horizontal: **0–22.5° hook**, **22.5–45° jab**, **45–70° straight**, and **70–90° uppercut**. The gesture fires once only after returning to center; trigger modifiers are captured at gesture start. Face/D-pad punches are the accessibility alternative when stick gestures are impractical. @@ -561,7 +567,7 @@ For a long-running deployment, run the command under your process manager of cho 2. On the application's **OAuth2** page, add `https://127.0.0.1` under **Redirects** and save it. Discord requires a Redirect URI for Activity authorization even though the Embedded App SDK handles returning to the Activity; this placeholder is the value specified by Discord's Activity guide. 3. Configure Activity OAuth for exactly `identify` and `guilds.members.read`. Install the guild app with both `bot` and `applications.commands` scopes, and keep **Server Members Intent** enabled. Message Content Intent remains needed by other IntelStream features. 4. Verify `DISCORD_GUILD_ID`, the authenticated application/client ID, and `DISCORD_CLIENT_SECRET`. Store the client secret and bot token only in the server environment. Never put either secret—or OAuth codes, access tokens, or game tickets—in a URL, Vite variable, browser/local storage, analytics, or logs. -5. Run the Python listener on `127.0.0.1:HANDS_PORT`. Put a public TLS reverse proxy on the mapped origin and proxy `/`, `/api/hands/*`, and WebSocket upgrades to that listener. Preserve HTTPS/WSS, Host, and upgrade headers; do not expose the plaintext loopback port publicly. Hands applies global and per-client request/concurrency ceilings using the direct peer address. If the proxy supplies `X-Forwarded-For`, set `HANDS_TRUSTED_PROXY_CIDRS` to only the exact proxy network(s) that connect to Hands (for example `127.0.0.1/32,::1/128` for a loopback proxy), configure the proxy to overwrite or append the real client address, and prevent clients from bypassing that proxy. Forwarded headers from any unlisted peer are ignored; never configure an all-addresses CIDR. +5. Run the Python listener on `127.0.0.1:HANDS_PORT`. Put a public TLS reverse proxy on the mapped origin and proxy `/`, `/api/hands/*`, and WebSocket upgrades to that listener. Preserve HTTPS/WSS, Host, and upgrade headers; do not expose the plaintext loopback port publicly. Hands applies global and per-client request/concurrency ceilings keyed on the peer address; because Discord's Activity proxy hides players' addresses, the sign-in ceilings are also scoped to the Activity instance, so one voice channel cannot crowd out another. If the proxy supplies `X-Forwarded-For`, set `HANDS_TRUSTED_PROXY_CIDRS` to only the exact proxy network(s) that connect to Hands (for example `127.0.0.1/32,::1/128` for a loopback proxy), configure the proxy to overwrite or append the real client address, and prevent clients from bypassing that proxy. Forwarded headers from any unlisted peer are ignored; never configure an all-addresses CIDR. 6. Start `uv run intelstream` and confirm startup configures one guild-only global `hands` Entry Point with Discord handler `DISCORD_LAUNCH_ACTIVITY`. Confirm the public `/healthz`, open Discord's App Launcher, search for BunkerIntel, and launch it. Have a second distinct guild member select **Play now** on Discord's channel message, then have a third member join and confirm they see **SPECTATING · READ ONLY** with no gameplay input. Confirm only the first two seats affect the match and ELO. A single process owns in-memory OAuth state, rooms, spectators, and reconnect tickets; do not horizontally scale Hands without shared authoritative state/routing. 7. Complete the violent-content/age-rating/disclosure and originality reviews described above before public distribution. @@ -625,7 +631,7 @@ HANDS_DEV_BACKEND=http://127.0.0.1:8091 npm --prefix web/hands run dev -- --port Open `http://localhost:5174/?e2e=1&instance_id=&player=` in two browser windows (different names) to fight. The `e2e` switch only exists in Vite development builds; the development server authenticates anyone who can reach its port, so never expose it. -`scripts/hands_e2e_scenarios.js` automates this with Playwright: `ko` (power punches until a knockdown, private get-up rhythm, stoppage), `reconnect` (one client drops offline mid-round and resumes), `rest` (two rounds with the corner rest), `spectator` (a third client joins read-only), `touch` (one player on an emulated phone), `mash` (hundreds of key presses per second) `latency` (a TCP proxy delays every frame 110 ms each way, and the clients report their measured input acknowledgement latency, which the fighter HUD also shows) `soak` (a full three-round bout sampling heap size, latency and render scale every twenty seconds) `background` (one player's page is frozen through the DevTools lifecycle state for eight seconds, as a backgrounded phone app would be, then resumed) `rematch` (both players use the Rematch button after the result and fight a second bout in the same instance) `rematchloop` (four bouts back to back while sampling heap size and GPU object counts for leaks) `clinch` (both players close in and one ties up; the harness reads the fighters' separation from the client state through the engine's draw-in, the hold and the referee break) and `response` (key press to first rendered frame for the player's own punch and step, when the opponent sees them, and whether the predicted punch ever jumps back, behind a delay proxy set with `E2E_DELAY_MS`, 60 ms each way by default). It spawns the development server itself and prints the clients' status text plus any client or server errors. The default software renderer keeps runs reproducible on machines without a GPU; set `E2E_GPU=1` to render on the machine's GPU for real frame pacing, input latency and screenshots. +`scripts/hands_e2e_scenarios.js` automates this with Playwright: `ko` (power punches until a knockdown, private get-up rhythm, stoppage), `reconnect` (one client's link goes silent mid-round through a TCP proxy, with no close in either direction as a lost network has none; it times how long the room takes to pause the other player, then restores the link and checks both resume), `rest` (two rounds with the corner rest), `spectator` (a third client joins read-only), `touch` (one player on an emulated phone), `mash` (hundreds of key presses per second) `latency` (a TCP proxy delays every frame 110 ms each way, and the clients report their measured input acknowledgement latency, which the fighter HUD also shows) `soak` (a full three-round bout sampling heap size, latency and render scale every twenty seconds) `background` (one player's page is frozen through the DevTools lifecycle state for eight seconds, as a backgrounded phone app would be, then resumed) `rematch` (both players use the Rematch button after the result and fight a second bout in the same instance) `rematchloop` (four bouts back to back while sampling heap size and GPU object counts for leaks) `clinch` (both players close in and one ties up; the harness reads the fighters' separation from the client state through the engine's draw-in, the hold and the referee break) and `response` (key press to first rendered frame for the player's own punch and step, when the opponent sees them, and whether the predicted punch ever jumps back, behind a delay proxy set with `E2E_DELAY_MS`, 60 ms each way by default). It spawns the development server itself and prints the clients' status text plus any client or server errors. The default software renderer keeps runs reproducible on machines without a GPU; set `E2E_GPU=1` to render on the machine's GPU for real frame pacing, input latency and screenshots. `HANDS_DEV_BACKEND` is a Node/Vite development-process setting, not a browser `VITE_*` value and not a production origin. Real OAuth still needs a valid Activity instance; the fixture is only recorded presentation data. Keep `HANDS_DEV_MODE=false` in production. diff --git a/scripts/check_hands_wheel.py b/scripts/check_hands_wheel.py index b3668ecc..bf0c5007 100755 --- a/scripts/check_hands_wheel.py +++ b/scripts/check_hands_wheel.py @@ -28,7 +28,8 @@ re.IGNORECASE, ) # These are the complete external URL literals carried by reviewed client code. -# They are pinned SDK metadata or model attribution, not Hands runtime targets. +# Apart from Discord's avatar host they are pinned SDK metadata or model attribution, +# not Hands runtime targets. REVIEWED_EXTERNAL_URL_LITERALS = { "https://github.com/uuidjs/uuid#getrandomvalues-not-supported", "https://discord.com", @@ -40,6 +41,8 @@ "https://staging.discord.co", "http://localhost:3333", "https://pax.discord.com", + # Discord's image host, for the players' own avatars (image requests only). + "https://cdn.discordapp.com/avatars/", # Texel Boxer source and CC BY 4.0 license attribution shown in-app. "https://sketchfab.com/3d-models/boxer-84767168720948b38728ff78ee6f6090", "https://creativecommons.org/licenses/by/4.0/", diff --git a/scripts/hands_balance.py b/scripts/hands_balance.py new file mode 100644 index 00000000..fbf71e74 --- /dev/null +++ b/scripts/hands_balance.py @@ -0,0 +1,655 @@ +#!/usr/bin/env python3 +"""Measure how Hands bouts play out, with computer boxers on both sides. + +Runs whole bouts in-process through the authoritative engine under the default rules (three +rounds of two minutes, fifteen-second rests) between computer levels, or between a scripted +human strategy and a computer level, and prints how the bouts end, how long they last, the +knockdowns, the punch output and how much of the fight is spent stunned. Every bout is +deterministic for its seed. With --style-matrix it plays every pair of fighting styles against +each other, from both corners, and prints how often each style wins with the standard error of +that share: between two computers of one level, two copies of a scripted strategy, or two +strategies ("skilled:brawler"), each style taking each side. A matrix plays held-out seeds, from +101, unless --first-seed says otherwise. + +A scripted human answers a punch 7 ticks after it starts and is heard at once, as if he sat in +the server. --reaction draws his reaction to each punch from a range, and a connection makes him +later still: he sees a punch --latency-ticks late (a client shows the opponent's punch when its +snapshot arrives) and his input reaches the server --uplink-ticks after he sends it. --rtt-ms +sets both from a round trip. + + uv run python scripts/hands_balance.py --seeds 12 + uv run python scripts/hands_balance.py --humans jabs,hooks,turtle,brawler,counter,mash --seeds 8 + uv run python scripts/hands_balance.py --humans skilled,counter --reaction 6-9 --rtt-ms 50 + uv run python scripts/hands_balance.py --style-matrix contender,champion --seeds 24 + uv run python scripts/hands_balance.py --style-matrix skilled:brawler --reaction 6-9 --rtt-ms 50 +""" + +from __future__ import annotations + +import argparse +import random +import time +from collections import Counter, deque +from dataclasses import dataclass, field +from math import hypot, sqrt +from typing import ClassVar, Protocol + +from intelstream.hands.cpu import CpuBrain, CpuLevel, cpu_style +from intelstream.hands.engine import AttackState, BoxingEngine, EngineConfig, FighterState +from intelstream.hands.rules import TICKS_PER_SECOND +from intelstream.hands.types import ( + ActionKind, + DefensivePose, + FighterStyle, + Hand, + InputCommand, + MatchPhase, + MovementAction, + Power, + PunchAction, + PunchClass, + SemanticAction, + Target, +) + +DEFAULT_MATCHUPS = ( + "champion:champion", + "contender:contender", + "rookie:rookie", + "champion:rookie", + "contender:rookie", +) +HUMAN_STRATEGIES = ("jabs", "hooks", "turtle", "brawler", "counter", "mash", "skilled") +HUMAN_REACTION_TICKS = 7 +HELD_OUT_FIRST_SEED = 101 + + +class Player(Protocol): + def decide(self, engine: BoxingEngine) -> InputCommand | None: ... + + +@dataclass(frozen=True, slots=True) +class Lag: + """How late a scripted human answers: a reaction drawn for each punch from `reaction`, a punch + seen `latency_ticks` late and input heard `uplink_ticks` after he sends it. A lagged human's + other scripted choices start from a seeded offset, so each seed plays a different bout.""" + + reaction: tuple[int, int] = (HUMAN_REACTION_TICKS, HUMAN_REACTION_TICKS) + latency_ticks: int = 0 + uplink_ticks: int = 0 + + @classmethod + def over(cls, rtt_ms: float, reaction: tuple[int, int]) -> Lag: + """A connection with this round trip: half of it each way, in whole ticks.""" + one_way = round(rtt_ms / 2 * TICKS_PER_SECOND / 1000) + return cls(reaction, one_way, one_way) + + +class _Eyes: + """Whether a scripted human has seen a punch yet: his reaction to it, drawn once per punch, + plus the ticks his connection shows it late.""" + + def __init__(self, lag: Lag | None, rng: random.Random) -> None: + self._lag = lag + self._rng = rng + self._punch = -1 + self._delay = HUMAN_REACTION_TICKS + + def see(self, attack: AttackState, tick: int) -> bool: + if self._lag is not None and attack.start_tick != self._punch: + self._punch = attack.start_tick + low, high = self._lag.reaction + self._delay = self._rng.randint(low, high) + self._lag.latency_ticks + return tick - attack.start_tick >= self._delay + + +class LaggedPlayer: + """A scripted human on a connection: he sees and answers each punch as his `Lag` says, and + every command he sends reaches the server `uplink_ticks` later.""" + + def __init__(self, human: Player, uplink_ticks: int) -> None: + self.human = human + self._in_flight: deque[InputCommand | None] = deque([None] * uplink_ticks) + + def decide(self, engine: BoxingEngine) -> InputCommand | None: + self._in_flight.append(self.human.decide(engine)) + return self._in_flight.popleft() + + +def _corner_pick(fighter: FighterState) -> tuple[SemanticAction, ...]: + if fighter.corner_choice is not None: + return () + trauma = fighter.trauma + if max(trauma.left_cut, trauma.right_cut) >= 400 or trauma.bleeding >= 200: + return (MovementAction(ActionKind.CORNER_CUT),) + if max(trauma.left_eye, trauma.right_eye) >= 500 or trauma.swelling >= 450: + return (MovementAction(ActionKind.CORNER_SWELLING),) + return (MovementAction(ActionKind.CORNER_BREATH),) + + +class SkilledHuman: + """A capable player who only sees a punch after a human's reaction time. + + Keeps his range and circles, raises the guard high or low for the punch he sees coming, + answers a punch that has missed or been blocked, jabs to keep the other man honest, saves his + stamina, drops his arms out of range to rest them, ties up when hurt, and sends his corner an + instruction between rounds. + """ + + def __init__( + self, player_id: str, opponent_id: str, lag: Lag | None = None, seed: int = 0 + ) -> None: + self.player_id = player_id + self.opponent_id = opponent_id + rng = random.Random(seed) # nosec B311 + self._eyes = _Eyes(lag, rng) + self._start = 0 if lag is None else rng.randrange(1000) + self._sequence = 0 + self._seen = -1 + self._answered = -1 + self._guard = DefensivePose.NONE + self._guard_until = -1 + self._next_jab = 0 + + def decide(self, engine: BoxingEngine) -> InputCommand | None: + self._sequence += 1 + me = engine.fighter(self.player_id) + them = engine.fighter(self.opponent_id) + tick = engine.tick + if engine.phase is MatchPhase.KNOCKDOWN: + window_middle = (me.get_up_window_start_tick + me.get_up_window_end_tick) // 2 + prompt = me.get_up_prompt + if prompt is not None and not me.get_up_prompt_resolved and tick + 1 >= window_middle: + return InputCommand(self._sequence, tick, actions=(MovementAction(prompt),)) + return InputCommand(self._sequence, tick) + if engine.phase is MatchPhase.REST: + return InputCommand(self._sequence, tick, actions=_corner_pick(me)) + if engine.phase is not MatchPhase.FIGHT: + return InputCommand(self._sequence, tick) + dx, dy = them.x - me.x, them.y - me.y + distance = max(1.0, hypot(dx, dy)) + ux, uy = dx / distance, dy / distance + hurt = me.stunned_ticks > 12 or me.poise < 200 + wanted = 210.0 if hurt else 150.0 + radial = max(-1.0, min(1.0, (distance - wanted) / 30)) + side = 1 if ((tick + self._start) // 75) % 2 == 0 else -1 + if abs(me.x) + abs(me.y) > 520: + # Off the ropes: circle the way that leads back to the middle. + side = 1 if -(me.x * -uy + me.y * ux) >= 0 else -1 + move_x = ux * radial - uy * 0.6 * side + move_y = uy * radial + ux * 0.6 * side + size = max(1.0, hypot(move_x, move_y)) + move = (round(move_x / size * 1000), round(move_y / size * 1000)) + attack = them.attack + free = me.attack is None and not me.pending_actions + if hurt and distance <= 110 and me.stamina >= 45 and free: + return InputCommand(self._sequence, tick, actions=(MovementAction(ActionKind.CLINCH),)) + if ( + attack is not None + and not attack.resolved + and attack.start_tick != self._seen + and self._eyes.see(attack, tick) + ): + self._seen = attack.start_tick + head = attack.action.target is Target.HEAD + self._guard = DefensivePose.GUARD_HIGH if head else DefensivePose.GUARD_LOW + self._guard_until = attack.start_tick + attack.rule.startup + attack.rule.active + 1 + if tick < self._guard_until: + return InputCommand(self._sequence, tick, move[0], move[1], self._guard) + if ( + attack is not None + and attack.resolved + and attack.start_tick != self._answered + and self._eyes.see(attack, tick) + and free + and distance <= 165 + and me.stamina >= 120 + ): + self._answered = attack.start_tick + punch = PunchClass.STRAIGHT if distance > 120 else PunchClass.HOOK + power = Power.POWER if me.counter_ticks > 0 and me.stamina >= 300 else Power.NORMAL + action = PunchAction(Hand.RIGHT, punch, Target.HEAD, power) + return InputCommand(self._sequence, tick, actions=(action,)) + if tick >= self._next_jab and free and distance <= 150 and me.stamina >= 300: + self._next_jab = tick + 24 + (tick * 7919 + self._start) % 18 + jab = PunchAction(Hand.LEFT, PunchClass.JAB, Target.HEAD) + return InputCommand(self._sequence, tick, move[0], move[1], actions=(jab,)) + rest_arms = distance > 175 + defense = DefensivePose.NONE if rest_arms else DefensivePose.GUARD_HIGH + return InputCommand(self._sequence, tick, move[0], move[1], defense) + + +class ScriptedHuman: + """A player who repeats one plan without reading the opponent, except the counter-puncher. + + - jabs: walks to jab range and pecks with the lead hand. + - hooks: walks inside and swings power hooks from alternate hands. + - turtle: walks in behind a high guard and jabs now and then. + - brawler: straights, hooks, uppercuts and the odd body shot from behind a high guard. + - counter: holds a guard at range, high or low for the punch he sees coming, and answers each + punch once a human could have seen it; like the skilled player he keeps stamina back and + sends his corner an instruction between rounds. + - mash: a newcomer pressing every punch button as fast as he can, never guarding. + """ + + WANTED_DISTANCE: ClassVar[dict[str, int]] = { + "jabs": 135, + "hooks": 100, + "turtle": 120, + "brawler": 112, + "counter": 150, + "mash": 110, + } + + def __init__( + self, kind: str, player_id: str, opponent_id: str, lag: Lag | None = None, seed: int = 0 + ) -> None: + if kind not in self.WANTED_DISTANCE: + raise ValueError(f"unknown strategy {kind!r}") + self.kind = kind + self.player_id = player_id + self.opponent_id = opponent_id + rng = random.Random(seed) # nosec B311 + self._eyes = _Eyes(lag, rng) + self._start = 0 if lag is None else rng.randrange(1000) + self._sequence = 0 + self._next_tick = 0 + self._flip = False + + def _command( + self, + tick: int, + move: tuple[int, int] = (0, 0), + defense: DefensivePose = DefensivePose.NONE, + actions: tuple[SemanticAction, ...] = (), + ) -> InputCommand: + return InputCommand(self._sequence, tick, move[0], move[1], defense, actions) + + def decide(self, engine: BoxingEngine) -> InputCommand | None: + self._sequence += 1 + me = engine.fighter(self.player_id) + them = engine.fighter(self.opponent_id) + tick = engine.tick + if engine.phase is MatchPhase.KNOCKDOWN: + window_middle = (me.get_up_window_start_tick + me.get_up_window_end_tick) // 2 + prompt = me.get_up_prompt + if prompt is not None and not me.get_up_prompt_resolved and tick + 1 >= window_middle: + return self._command(tick, actions=(MovementAction(prompt),)) + return self._command(tick) + if engine.phase is MatchPhase.REST and self.kind == "counter": + return self._command(tick, actions=_corner_pick(me)) + if engine.phase is not MatchPhase.FIGHT: + return self._command(tick) + dx, dy = them.x - me.x, them.y - me.y + distance = max(1.0, hypot(dx, dy)) + wanted = self.WANTED_DISTANCE[self.kind] + toward = (round(dx / distance * 1000), round(dy / distance * 1000)) + move = toward if distance > wanted else (0, 0) + guarded = self.kind in ("turtle", "brawler", "counter") + defense = DefensivePose.GUARD_HIGH if guarded else DefensivePose.NONE + roll = (tick * 2654435761 + self._sequence + self._start) % 1000 + free = me.attack is None and not me.pending_actions + if self.kind == "counter": + if distance < wanted - 10: + move = (-toward[0], -toward[1]) + attack = them.attack + seen = attack is not None and self._eyes.see(attack, tick) + answer = seen and attack is not None and attack.resolved and me.stamina >= 120 + if answer and free and distance <= 160: + punch = PunchClass.STRAIGHT if distance > 120 else PunchClass.HOOK + return self._command(tick, actions=(PunchAction(Hand.RIGHT, punch, Target.HEAD),)) + jab_ready = tick >= self._next_tick and me.stamina >= 300 + if jab_ready and free and distance <= 150 and roll < 300: + self._next_tick = tick + 20 + jab = PunchAction(Hand.LEFT, PunchClass.JAB, Target.HEAD) + return self._command(tick, move, actions=(jab,)) + if seen and attack is not None and not attack.resolved: + body = attack.action.target is Target.BODY + defense = DefensivePose.GUARD_LOW if body else DefensivePose.GUARD_HIGH + return self._command(tick, move, defense) + if self.kind == "mash": + if tick < self._next_tick or distance > 150: + return self._command(tick, move) + hand, punch = ( + (Hand.LEFT, PunchClass.JAB), + (Hand.RIGHT, PunchClass.STRAIGHT), + (Hand.LEFT, PunchClass.HOOK), + (Hand.RIGHT, PunchClass.HOOK), + (Hand.RIGHT, PunchClass.UPPERCUT), + )[roll % 5] + power = Power.POWER if roll % 3 == 0 else Power.NORMAL + self._next_tick = tick + 4 + return self._command( + tick, move, actions=(PunchAction(hand, punch, Target.HEAD, power),) + ) + if tick < self._next_tick or not free: + return self._command(tick, move, defense) + if self.kind == "brawler" and distance <= 125: + hand, punch = ( + (Hand.RIGHT, PunchClass.STRAIGHT), + (Hand.LEFT, PunchClass.HOOK), + (Hand.RIGHT, PunchClass.HOOK), + (Hand.RIGHT, PunchClass.UPPERCUT), + (Hand.LEFT, PunchClass.JAB), + )[roll % 5] + self._next_tick = tick + 12 + roll % 7 + target = Target.BODY if roll % 4 == 0 else Target.HEAD + return self._command(tick, move, actions=(PunchAction(hand, punch, target),)) + if self.kind == "jabs" and distance <= 150: + self._next_tick = tick + 9 + jab = PunchAction(Hand.LEFT, PunchClass.JAB, Target.HEAD) + return self._command(tick, move, actions=(jab,)) + if self.kind == "hooks" and distance <= 126: + self._flip = not self._flip + hand = Hand.LEFT if self._flip else Hand.RIGHT + self._next_tick = tick + 14 + hook = PunchAction(hand, PunchClass.HOOK, Target.HEAD, Power.POWER) + return self._command(tick, move, actions=(hook,)) + if self.kind == "turtle" and distance <= 150: + self._next_tick = tick + 45 + jab = PunchAction(Hand.LEFT, PunchClass.JAB, Target.HEAD) + return self._command(tick, move, defense, (jab,)) + return self._command(tick, move, defense) + + +@dataclass(slots=True) +class BoutStats: + winner_seat: int | None + method: str + rounds: int + fight_seconds: float + knockdowns: int + thrown: list[int] + landed: list[int] + stunned_ticks: int + fight_ticks: int + stuns: int + chained_stuns: int + stunned_knockdowns: int + corner_picks: Counter[str] = field(default_factory=Counter) + landed_by_round: list[list[int]] = field(default_factory=list) + """Each seat's clean punches in each round fought.""" + + +def make_player( + spec: str, + player_id: str, + opponent_id: str, + seed: int, + style: FighterStyle = FighterStyle.BALANCED, + lag: Lag | None = None, +) -> Player: + human: Player + if spec == "skilled": + human = SkilledHuman(player_id, opponent_id, lag, seed) + elif spec in HUMAN_STRATEGIES: + human = ScriptedHuman(spec, player_id, opponent_id, lag, seed) + else: + return CpuBrain(player_id, opponent_id, CpuLevel(spec), seed, style) + if lag is None or lag.uplink_ticks == 0: + return human + return LaggedPlayer(human, lag.uplink_ticks) + + +def play( + one: str, + two: str, + seed: int, + config: EngineConfig, + styles: tuple[FighterStyle, FighterStyle] = (FighterStyle.BALANCED, FighterStyle.BALANCED), + lag: Lag | None = None, +) -> BoutStats: + """One bout; `lag` puts any scripted human on a connection.""" + engine = BoxingEngine( + match_id=f"balance-{seed}", + activity_instance_id="balance", + guild_id="balance", + player_one_id="one", + player_two_id="two", + seed=seed, + config=config, + styles=styles, + ) + # Nothing here reads a checksum, and hashing each tick's state is most of what a bout costs. + engine.checksums = False + players = ( + ("one", make_player(one, "one", "two", seed * 2 + 1, styles[0], lag)), + ("two", make_player(two, "two", "one", seed * 2 + 2, styles[1], lag)), + ) + ids = ("one", "two") + thrown = [0, 0] + landed = [0, 0] + landed_by_round = [[0] * config.rounds, [0] * config.rounds] + stunned_ticks = fight_ticks = stuns = chained = stunned_knockdowns = 0 + corner_picks: Counter[str] = Counter() + while engine.result is None: + for player_id, player in players: + command = player.decide(engine) + if command is not None: + engine.submit_input(player_id, command) + before = {player_id: engine.fighter(player_id).stunned_ticks for player_id in ids} + fighting = engine.phase is MatchPhase.FIGHT + snapshot = engine.step() + if fighting: + fight_ticks += 1 + stunned_ticks += sum(1 for player_id in ids if engine.fighter(player_id).stunned_ticks) + blocked_punches = { + event.action_id + for event in snapshot.events + if event.kind in ("block", "perfect_block") and event.action_id is not None + } + for event in snapshot.events: + seat = ids.index(event.actor_id) if event.actor_id in ids else None + if event.kind == "punch_start" and seat is not None: + thrown[seat] += 1 + elif event.kind in ("hit", "counter_hit") and seat is not None: + if event.action_id not in blocked_punches: + landed[seat] += 1 + landed_by_round[seat][snapshot.round_number - 1] += 1 + elif event.kind == "stun" and event.target_id in ids: + stuns += 1 + if before[event.target_id] > 0: + chained += 1 + elif event.kind == "knockdown" and event.target_id in ids: + if before[event.target_id] > 0: + stunned_knockdowns += 1 + elif event.kind == "corner" and event.detail: + corner_picks[event.detail] += 1 + result = engine.result + winner = None if result.winner_id is None else ids.index(result.winner_id) + return BoutStats( + winner_seat=winner, + method=result.finish_method.value, + rounds=result.round_number, + fight_seconds=fight_ticks / TICKS_PER_SECOND, + knockdowns=result.player_one_knockdowns + result.player_two_knockdowns, + thrown=thrown, + landed=landed, + stunned_ticks=stunned_ticks, + fight_ticks=fight_ticks, + stuns=stuns, + chained_stuns=chained, + stunned_knockdowns=stunned_knockdowns, + corner_picks=corner_picks, + landed_by_round=[seat[: result.round_number] for seat in landed_by_round], + ) + + +def bout_styles(one: str, two: str, seed: int) -> tuple[FighterStyle, FighterStyle]: + """Against a scripted player the computer fights in the style the room would give it for + this match seed; between two computer levels both box balanced, as the style matrix varies.""" + if one in HUMAN_STRATEGIES and two not in HUMAN_STRATEGIES: + return FighterStyle.BALANCED, cpu_style(CpuLevel(two), seed) + return FighterStyle.BALANCED, FighterStyle.BALANCED + + +def standard_error(share: float, count: int) -> float: + """The standard error of a share measured over `count` bouts.""" + return sqrt(share * (1 - share) / max(1, count)) + + +def summarise(one: str, two: str, bouts: list[BoutStats], elapsed: float) -> str: + count = len(bouts) + methods = Counter(bout.method for bout in bouts) + wins = Counter(bout.winner_seat for bout in bouts) + share = (wins[0] + wins[None] / 2) / max(1, count) + rounds = sum(bout.rounds for bout in bouts) + fight_seconds = sum(bout.fight_seconds for bout in bouts) + knockdowns = sum(bout.knockdowns for bout in bouts) + stunned = sum(bout.stunned_ticks for bout in bouts) + fight_ticks = sum(bout.fight_ticks for bout in bouts) or 1 + stuns = sum(bout.stuns for bout in bouts) + chained = sum(bout.chained_stuns for bout in bouts) + stunned_knockdowns = sum(bout.stunned_knockdowns for bout in bouts) + picks: Counter[str] = Counter() + for bout in bouts: + picks.update(bout.corner_picks) + blank = [ + sum(punches == 0 for bout in bouts for punches in bout.landed_by_round[seat]) + for seat in (0, 1) + ] + per_round = [ + ( + sum(bout.thrown[seat] for bout in bouts) / max(1, rounds), + sum(bout.landed[seat] for bout in bouts) / max(1, rounds), + ) + for seat in (0, 1) + ] + finishes = ", ".join(f"{method} {methods[method]}" for method in sorted(methods)) + lines = [ + f"{one} v {two}: {count} bouts in {elapsed:.0f}s | wins {wins[0]}-{wins[1]}" + f" (draws {wins[None]}), {one} {100 * share:.0f}% +-{100 * standard_error(share, count):.0f}" + f" | {finishes}", + f" rounds {rounds / count:.2f} | fight time {fight_seconds / count:.0f}s" + f" | knockdowns {knockdowns / count:.2f}/bout ({stunned_knockdowns} while stunned)" + f" | stunned {100 * stunned / (2 * fight_ticks):.1f}% of fighter-time" + f" | stuns {stuns / count:.1f}/bout, {100 * chained / max(1, stuns):.0f}% re-stunned", + f" per round thrown/landed clean: one {per_round[0][0]:.0f}/{per_round[0][1]:.0f}" + f", two {per_round[1][0]:.0f}/{per_round[1][1]:.0f}" + f" | rounds landing nothing: one {blank[0]}/{rounds}, two {blank[1]}/{rounds}" + + (f" | corner picks {dict(picks)}" if picks else ""), + ] + return "\n".join(lines) + + +def matrix_sides(spec: str) -> tuple[str, str]: + """The players a matrix spec names: one level or strategy on both sides, or "one:two".""" + one, _, two = spec.partition(":") + return one, two or one + + +def style_matrix( + spec: str, + styles: list[FighterStyle], + seeds: range, + config: EngineConfig, + lag: Lag | None = None, +) -> str: + """Every pair of styles between the players `spec` names, each seed with each style on each + side, so neither style gains from the stronger side; a draw counts as half.""" + one, two = matrix_sides(spec) + count = 2 * len(seeds) + share: dict[tuple[FighterStyle, FighterStyle], float] = {} + lines = [] + for index, first in enumerate(styles): + for second in styles[index + 1 :]: + started = time.perf_counter() + points = 0.0 + methods: Counter[str] = Counter() + for seed in seeds: + for corners in ((first, second), (second, first)): + bout = play(one, two, seed, config, corners, lag) + methods[bout.method] += 1 + if bout.winner_seat is None: + points += 0.5 + elif corners[bout.winner_seat] is first: + points += 1 + share[first, second] = points / count + share[second, first] = 1 - share[first, second] + finishes = ", ".join(f"{method} {methods[method]}" for method in sorted(methods)) + lines.append( + f" {first.value} v {second.value}: {cell(share[first, second], count)}" + f" | {finishes} | {time.perf_counter() - started:.0f}s" + ) + width = max(len(style.value) for style in styles) + 2 + table = [ + f"{spec}: win % of the row's style against the column's, +- one standard error," + f" {count} bouts a pair, seeds {seeds.start}-{seeds.stop - 1}", + " " * width + "".join(f"{style.value[:8]:>9}" for style in styles), + ] + for row in styles: + cells = "".join( + f"{'-' if row is column else cell(share[row, column], count):>9}" for column in styles + ) + table.append(f"{row.value:<{width}}{cells}") + worst = max(share, key=lambda pair: share[pair]) + table.append(f"widest: {worst[0].value} beats {worst[1].value} {cell(share[worst], count)}") + return "\n".join(table + lines) + + +def cell(share: float, count: int) -> str: + return f"{100 * share:.0f}+-{100 * standard_error(share, count):.0f}" + + +def parse_lag(args: argparse.Namespace) -> Lag | None: + """The connection the scripted humans play on, or None for none at all.""" + if not (args.reaction or args.latency_ticks or args.uplink_ticks or args.rtt_ms is not None): + return None + low, _, high = (args.reaction or str(HUMAN_REACTION_TICKS)).partition("-") + reaction = (int(low), int(high or low)) + if args.rtt_ms is not None: + return Lag.over(args.rtt_ms, reaction) + return Lag(reaction, args.latency_ticks, args.uplink_ticks) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--matchups", default=",".join(DEFAULT_MATCHUPS)) + parser.add_argument("--humans", default="", help="scripted strategies to play every level") + parser.add_argument("--levels", default=",".join(level.value for level in CpuLevel)) + parser.add_argument("--seeds", type=int, default=12) + parser.add_argument("--first-seed", type=int, default=None, help="1, or 101 for a matrix") + parser.add_argument( + "--style-matrix", + default="", + help="levels, strategies or strategy pairs (skilled:brawler) to play every style pair at", + ) + parser.add_argument("--styles", default=",".join(style.value for style in FighterStyle)) + parser.add_argument("--reaction", default="", help="a human's reaction to a punch, e.g. 6-9") + parser.add_argument("--latency-ticks", type=int, default=0, help="ticks late he sees a punch") + parser.add_argument("--uplink-ticks", type=int, default=0, help="ticks before he is heard") + parser.add_argument("--rtt-ms", type=float, default=None, help="a round trip, half each way") + args = parser.parse_args() + config = EngineConfig() + lag = parse_lag(args) + if lag is not None: + print( + f"scripted humans react in {lag.reaction[0]}-{lag.reaction[1]} ticks, see a punch" + f" {lag.latency_ticks} late and are heard {lag.uplink_ticks} late", + flush=True, + ) + if args.style_matrix: + styles = [FighterStyle(style) for style in args.styles.split(",")] + first = HELD_OUT_FIRST_SEED if args.first_seed is None else args.first_seed + seeds = range(first, first + args.seeds) + for spec in args.style_matrix.split(","): + print(style_matrix(spec, styles, seeds, config, lag), flush=True) + return + if args.humans: + pairs = [ + (human, level) for human in args.humans.split(",") for level in args.levels.split(",") + ] + else: + pairs = [tuple(matchup.split(":", 1)) for matchup in args.matchups.split(",")] # type: ignore[misc] + for one, two in pairs: + started = time.perf_counter() + first = 1 if args.first_seed is None else args.first_seed + bouts = [ + play(one, two, seed, config, bout_styles(one, two, seed), lag) + for seed in range(first, first + args.seeds) + ] + print(summarise(one, two, bouts, time.perf_counter() - started), flush=True) + + +if __name__ == "__main__": + main() diff --git a/scripts/hands_e2e_scenarios.js b/scripts/hands_e2e_scenarios.js index 5118421f..da5b90ff 100644 --- a/scripts/hands_e2e_scenarios.js +++ b/scripts/hands_e2e_scenarios.js @@ -7,12 +7,21 @@ * Each scenario spawns scripts/hands_e2e_server.py on port 8091, drives two headless players with * keyboard or touch input, reads the client's screen-reader status text, and writes screenshots to * $TMPDIR/hands-e2e. Set E2E_GPU=1 to render on the machine's GPU instead of the software renderer - * (real frame pacing and input latency). + * (real frame pacing and input latency). The response scenario takes E2E_DELAY_MS and E2E_JITTER_MS. * - * node scripts/hands_e2e_scenarios.js ko|reconnect|rest|spectator|touch|mash|latency|soak|background|rematch|rematchloop|clinch|response + * The reconnect scenario drops one player's link the way a lost network does, with no close in either + * direction, through a TCP proxy, times how long the room takes to pause the other player, and checks + * both resume once the link is back. + * The cpu scenario is one player against the computer (E2E_CPU_LEVEL, default contender) through to the + * result card. The tko scenario gets the floored player up twice, so the bout ends on the punch of the + * third knockdown, and fails unless the knockout replay still plays. The styles scenario has one fighter pick a style with the keyboard and the other by + * touch while a spectator watches; every other scenario settles on the offered style at once. E2E_PORT + * moves the server off 8091. + * + * node scripts/hands_e2e_scenarios.js ko|tko|reconnect|rest|spectator|touch|mash|latency|soak|background|rematch|rematchloop|clinch|response|cpu|styles */ const { chromium, devices } = require('playwright'); -const { spawn } = require('node:child_process'); +const { spawn, spawnSync } = require('node:child_process'); const http = require('node:http'); const net = require('node:net'); @@ -26,6 +35,11 @@ fs.mkdirSync(out, { recursive: true }); const scenario = process.argv[2] || 'ko'; const PORT = Number(process.env.E2E_PORT ?? 8091); const wait = (ms) => new Promise((r) => setTimeout(r, ms)); +/** + * A link lost without a goodbye is noticed only by the room's websocket heartbeat: a ping 15 s after + * the last frame, half that to answer, then the room's own close (23.7 s measured on Windows). + */ +const SILENT_DROP_NOTICE_MS = 30_000; function healthz() { return new Promise((resolve) => { @@ -35,30 +49,88 @@ function healthz() { }); } -/** TCP proxy that delays every chunk in both directions, so websocket frames see real latency. */ -function delayProxy(listenPort, targetPort, delayMs) { +/** + * TCP proxy that delays every chunk in both directions, so websocket frames see real latency. + * `jitterMs` adds a random extra wait to each chunk while keeping them in order. `silence` and + * `restore` drop a player's link the way a lost connection does (see `silence`). + */ +function delayProxy(listenPort, targetPort, delayMs, jitterMs = 0) { + const links = new Set(); const server = net.createServer((client) => { const upstream = net.connect(targetPort, '127.0.0.1'); - const pipe = (from, to) => { - from.on('data', (chunk) => setTimeout(() => { if (!to.destroyed) to.write(chunk); }, delayMs)); - from.on('end', () => setTimeout(() => to.end(), delayMs)); - from.on('error', () => to.destroy()); + // `head` keeps the start of what the page sent (its websocket handshake); `dark` swallows the link. + const link = { client, upstream, head: '', dark: false, upstreamGone: false }; + links.add(link); + client.on('close', () => { if (!link.dark) links.delete(link); }); + const pipe = (from, to, fromPage) => { + const queue = []; + let timer = null; + const drain = () => { + timer = null; + while (queue.length > 0 && queue[0].due <= Date.now()) { + const { chunk } = queue.shift(); + if (chunk === null) to.end(); + else if (!to.destroyed) to.write(chunk); + } + if (queue.length > 0) timer = setTimeout(drain, Math.max(1, queue[0].due - Date.now())); + }; + const hold = (chunk) => { + if (fromPage && chunk !== null && link.head.length < 8192) link.head += chunk.toString('latin1'); + // A silenced link passes nothing, not even the room hanging up on it. + if (link.dark) { + if (chunk === null && !fromPage) link.upstreamGone = true; + return; + } + const due = Math.max(queue.at(-1)?.due ?? 0, Date.now() + delayMs + Math.random() * jitterMs); + queue.push({ chunk, due }); + if (timer === null) timer = setTimeout(drain, Math.max(1, queue[0].due - Date.now())); + }; + from.on('data', hold); + from.on('end', () => hold(null)); + from.on('error', () => { + if (!link.dark) to.destroy(); + else if (!fromPage) link.upstreamGone = true; + }); }; - pipe(client, upstream); - pipe(upstream, client); + pipe(client, upstream, true); + pipe(upstream, client, false); }); server.listen(listenPort, '127.0.0.1'); + /** + * Every open websocket whose handshake carries `marker` goes silent in both directions, as when a + * phone loses its network: no close frame and no FIN either way, so the room learns of it only from + * its websocket heartbeat and the page goes on thinking it is connected. Returns how many it silenced. + */ + server.silence = (marker) => { + let silenced = 0; + for (const link of links) { + if (!/upgrade:\s*websocket/i.test(link.head) || !link.head.includes(marker)) continue; + link.dark = true; + silenced += 1; + } + return silenced; + }; + /** The network is back: a link the room gave up on meanwhile now fails on the page too, so it reconnects. */ + server.restore = () => { + for (const link of links) { + if (!link.dark) continue; + link.dark = false; + links.delete(link); + if (link.upstreamGone || link.upstream.destroyed) link.client.destroy(); + } + }; return server; } -async function startServer(args, { oneWayDelayMs = 0 } = {}) { - const backendPort = oneWayDelayMs > 0 ? PORT + 1 : PORT; +async function startServer(args, { oneWayDelayMs = 0, jitterMs = 0, proxy = false } = {}) { + const proxied = proxy || oneWayDelayMs > 0; + const backendPort = proxied ? PORT + 1 : PORT; const child = spawn('uv', ['run', 'python', 'scripts/hands_e2e_server.py', '--port', String(backendPort), ...args], { cwd: repo, stdio: ['ignore', 'pipe', 'pipe'] }); const log = []; child.stdout.on('data', (d) => log.push(String(d))); child.stderr.on('data', (d) => log.push(String(d))); - const proxy = oneWayDelayMs > 0 ? delayProxy(PORT, backendPort, oneWayDelayMs) : null; - for (let i = 0; i < 60; i += 1) { if (await healthz()) return { child, log, proxy }; await wait(500); } + const tcpProxy = proxied ? delayProxy(PORT, backendPort, oneWayDelayMs, jitterMs) : null; + for (let i = 0; i < 60; i += 1) { if (await healthz()) return { child, log, proxy: tcpProxy }; await wait(500); } throw new Error('server did not start: ' + log.join('')); } @@ -72,6 +144,26 @@ async function status(page) { })); } +/** Settles on the style the picker offers, as soon as the pick before the bout appears. */ +async function settleStyle(page) { + await page.waitForSelector('[data-style-picker]:not([hidden])', { timeout: 60000 }); + await page.keyboard.press('Enter'); +} + +async function pickerState(page) { + return page.evaluate(() => { + const element = document.querySelector('[data-style-picker]'); + if (element === null) return null; + return { + hidden: element.hidden, + chosen: element.querySelector('[data-chosen]')?.dataset.style ?? null, + disabled: [...element.querySelectorAll('[data-style]')].filter((button) => button.disabled).length, + clock: element.querySelector('.style-clock')?.textContent ?? null, + status: element.querySelector('.style-status')?.textContent ?? null, + }; + }); +} + async function waitFor(page, predicate, timeoutMs, label) { const started = Date.now(); while (Date.now() - started < timeoutMs) { @@ -133,6 +225,8 @@ async function main() { const instance = `e2e-${scenario}-${Date.now()}`; const open = async (name, options = {}) => { const context = await browser.newContext(options.mobile ? { ...devices['Pixel 7'], viewport: { width: 844, height: 390 } } : { viewport: { width: 1280, height: 720 } }); + // Rides along on every request to the room, websocket handshake included, so the TCP proxy can tell the players apart. + await context.addCookies([{ name: 'hands_e2e_player', value: name, url: base }]); const page = await context.newPage(); if (scenario === 'rematchloop') await page.addInitScript(countLiveGlObjects); if (scenario === 'response') { @@ -157,6 +251,7 @@ async function main() { const serverArgs = { ko: ['--rounds', '3', '--round-seconds', '90', '--rest-seconds', '5'], + tko: ['--rounds', '3', '--round-seconds', '90', '--rest-seconds', '5'], reconnect: ['--rounds', '1', '--round-seconds', '70', '--rest-seconds', '5'], rest: ['--rounds', '2', '--round-seconds', '14', '--rest-seconds', '9'], spectator: ['--rounds', '1', '--round-seconds', '30', '--rest-seconds', '5'], @@ -169,12 +264,22 @@ async function main() { rematchloop: ['--rounds', '1', '--round-seconds', '20', '--rest-seconds', '5'], clinch: ['--rounds', '1', '--round-seconds', '30', '--rest-seconds', '5'], response: ['--rounds', '1', '--round-seconds', '40', '--rest-seconds', '5'], + cpu: ['--rounds', '2', '--round-seconds', '40', '--rest-seconds', '6'], + styles: ['--rounds', '1', '--round-seconds', '30', '--rest-seconds', '5', '--style-select-seconds', '30'], }[scenario]; const responseDelayMs = Number(process.env.E2E_DELAY_MS ?? 60); - const server = await startServer(serverArgs, { oneWayDelayMs: scenario === 'latency' ? 110 : scenario === 'response' ? responseDelayMs : 0 }); + const responseJitterMs = Number(process.env.E2E_JITTER_MS ?? 0); + const server = await startServer(serverArgs, { oneWayDelayMs: scenario === 'latency' ? 110 : scenario === 'response' ? responseDelayMs : 0, jitterMs: scenario === 'response' ? responseJitterMs : 0, proxy: scenario === 'reconnect' }); try { const A = await open('Alpha'); - const B = await open('Bravo', { mobile: scenario === 'touch' }); + if (scenario === 'cpu') { + await runCpu(A, note); + report.errors.push(...A.errors); + return; + } + const B = await open('Bravo', { mobile: scenario === 'touch' || scenario === 'rest' || scenario === 'styles' }); + if (scenario === 'styles') await pickStyles(A, B, open, note, report); + else await Promise.all([settleStyle(A.page), settleStyle(B.page)]); if (scenario === 'latency') note('both clients behind a TCP proxy adding 110 ms each way (220 ms round trip) to every frame'); const startedState = await waitFor(A.page, (s) => /countdown|fight/.test(s.summary ?? ''), 60000, 'bout start'); note('bout started:', startedState !== null); @@ -182,7 +287,27 @@ async function main() { // Approach: A right, B left. await A.page.keyboard.down('d'); await B.page.keyboard.down('a'); await wait(1600); await A.page.keyboard.up('d'); await B.page.keyboard.up('a'); - if (scenario === 'ko') { + if (scenario === 'tko') { + // Presses each get-up prompt once, in the middle of its window, from inside the page. + await B.page.evaluate(() => { + let answered = -1; + const press = () => { + const app = window.__handsApp; + const snapshot = app?.state?.snapshot; + const me = snapshot?.fighters.find((fighter) => fighter.player_id === app.state.playerId); + if (me?.is_downed && me.get_up_prompt !== null && answered !== me.get_up_window_start_tick && snapshot.tick >= me.get_up_window_start_tick + 2) { + answered = me.get_up_window_start_tick; + const code = me.get_up_prompt === 'get_up_left' ? 'ArrowLeft' : 'ArrowRight'; + window.dispatchEvent(new KeyboardEvent('keydown', { code, key: code })); + window.dispatchEvent(new KeyboardEvent('keyup', { code, key: code })); + } + requestAnimationFrame(press); + }; + requestAnimationFrame(press); + }); + } + + if (scenario === 'ko' || scenario === 'tko') { // Records every fighter's punch phase while the knockout replay plays, to catch a stuttering replay. await A.page.evaluate(() => { const state = { frames: 0, jumps: 0, worst: 0 }; @@ -213,13 +338,27 @@ async function main() { if (/^Knockdown/.test(live) || /\. knockdown\./.test(sB.summary ?? '')) { if (!downSeen) { downSeen = true; note('KNOCKDOWN seen at', ((Date.now() - started) / 1000).toFixed(1), 's:', live); await A.page.screenshot({ path: `${out}/e2e-ko-A-down.png` }); await B.page.screenshot({ path: `${out}/e2e-ko-B-down.png` }); } const count = /Count (\d+)/.exec(live); if (count && Number(count[1]) !== lastCount) { lastCount = Number(count[1]); note('count', lastCount, '|', live); } - if (/Press left now/.test(live)) { await B.page.keyboard.press('ArrowLeft'); promptsPressed += 1; } + if (scenario === 'tko') { /* the page answers its own prompts */ } + else if (/Press left now/.test(live)) { await B.page.keyboard.press('ArrowLeft'); promptsPressed += 1; } else if (/Press right now/.test(live)) { await B.page.keyboard.press('ArrowRight'); promptsPressed += 1; } await wait(60); continue; } - // A keeps pressure: step in, throw power hooks and uppercuts, occasionally straights. - await A.page.keyboard.down('d'); await wait(120); await A.page.keyboard.up('d'); + // A keeps pressure: walk at B (after a count A comes back from a neutral corner), then throw + // power hooks and uppercuts, occasionally straights. + const gap = await A.page.evaluate(() => { + const app = window.__handsApp; + const fighters = app?.state?.snapshot?.fighters ?? []; + const me = fighters.find((fighter) => fighter.player_id === app.state.playerId); + const other = fighters.find((fighter) => fighter.player_id !== app.state.playerId); + return me && other ? { dx: other.x - me.x, dy: other.y - me.y, stamina: me.stamina } : null; + }); + const steps = gap === null ? ['d'] : [gap.dx > 60 ? 'd' : gap.dx < -60 ? 'a' : null, gap.dy > 60 ? 'w' : gap.dy < -60 ? 's' : null].filter(Boolean); + for (const step of steps) await A.page.keyboard.down(step); + await wait(gap !== null && Math.hypot(gap.dx, gap.dy) > 180 ? 320 : 120); + for (const step of steps) await A.page.keyboard.up(step); + // A tired fighter's punches are weak arm punches, so A gets the breath back before the next one. + if (gap !== null && gap.stamina < 350) { await wait(500); continue; } const key = ['g', 't', 'h', 'y', 'u'][Math.floor(Math.random() * 5)]; await A.page.keyboard.press(key); await wait(380); @@ -232,20 +371,43 @@ async function main() { await A.page.screenshot({ path: `${out}/e2e-ko-A-final.png` }); await B.page.screenshot({ path: `${out}/e2e-ko-B-final.png` }); await wait(8000); await A.page.screenshot({ path: `${out}/e2e-ko-A-result.png` }); - note('knockout replay:', JSON.stringify(await A.page.evaluate(() => window.__replayProbe))); + const replay = await A.page.evaluate(() => window.__replayProbe); + note('knockout replay:', JSON.stringify(replay)); + if (scenario === 'tko') { + const ending = await A.page.evaluate(() => { + const renderer = window.__handsApp?.renderer; + return { lastRecordedTick: renderer?.history?.at(-1)?.tick ?? null, knockdownTick: renderer?.lastKnockdown?.knockdown?.tick ?? null }; + }); + note('recording ends at tick', ending.lastRecordedTick, '| last knockdown at tick', ending.knockdownTick); + if (!/^tko\b/i.test(final?.final ?? '')) report.errors.push(`expected a TKO on the third knockdown, got: ${final?.final ?? 'no result'}`); + else if (!(replay?.frames > 0)) report.errors.push('no knockout replay after a TKO that ended on its punch'); + } + } + + if (scenario === 'styles') { + await wait(800); + await A.page.screenshot({ path: `${out}/e2e-styles-A-plates.png` }); + await B.page.screenshot({ path: `${out}/e2e-styles-B-plates.png` }); } if (scenario === 'reconnect') { for (let i = 0; i < 6; i += 1) { await A.page.keyboard.press('f'); await B.page.keyboard.press('j'); await wait(400); } note('before drop A:', (await status(A.page)).status, '| B:', (await status(B.page)).status); - await B.context.setOffline(true); - note('B offline'); - const paused = await waitFor(A.page, (s) => /paused|reconnect/i.test((s.status ?? '') + (s.live ?? '')), 30000, 'opponent pause on A'); - note('A during drop:', paused?.status, '|', paused?.live); - await B.page.screenshot({ path: `${out}/e2e-reconnect-B-offline.png` }); + // Bravo's network goes: nothing more passes either way and nobody hangs up, as when Wi-Fi drops + // or the phone suspends Discord. Only the room's websocket heartbeat can notice; a close() from + // the page would tell the room at once, which a lost connection never does. + const droppedAt = Date.now(); + const silenced = server.proxy.silence('hands_e2e_player=Bravo'); + note(`B's link goes silent (${silenced} websocket)`); + if (silenced !== 1) report.errors.push(`expected to silence Bravo's one websocket, silenced ${silenced}`); + const paused = await waitFor(A.page, (s) => /paused|reconnect/i.test((s.status ?? '') + (s.live ?? '')), SILENT_DROP_NOTICE_MS + 5000, 'opponent pause on A'); + const noticedMs = Date.now() - droppedAt; + note(`A sees the pause ${(noticedMs / 1000).toFixed(1)} s after the drop:`, paused?.status, '|', paused?.live); + if (paused === null || noticedMs > SILENT_DROP_NOTICE_MS) report.errors.push(`the room took ${noticedMs} ms to pause for a silent drop (limit ${SILENT_DROP_NOTICE_MS} ms)`); + await B.page.screenshot({ path: `${out}/e2e-reconnect-B-dark.png` }); await wait(3000); - await B.context.setOffline(false); - note('B online'); + server.proxy.restore(); + note('B network back'); const resumed = await waitFor(A.page, (s) => /in progress|fight/i.test((s.status ?? '') + (s.summary ?? '')) && !/paused|reconnect/i.test(s.status ?? ''), 40000, 'resume on A'); note('A after resume:', resumed?.status, '|', resumed?.live); const bBack = await waitFor(B.page, (s) => /in progress|fight/i.test((s.status ?? '') + (s.summary ?? '')) && !/Unable|paused|reconnect/i.test(s.status ?? ''), 40000, 'B resumed'); @@ -283,10 +445,40 @@ async function main() { for (let i = 0; i < 8; i += 1) { await A.page.keyboard.press('f'); await B.page.keyboard.press('j'); await wait(400); } const rest = await waitFor(A.page, (s) => /\. rest\./.test(s.summary ?? ''), 40000, 'rest phase'); note('rest reached:', rest !== null, '|', rest?.status, '|', rest?.live); - await wait(4500); + // Each corner gets an instruction: Alpha with the 3 key, Bravo by tapping the panel on a phone. + const own = (page) => page.evaluate(() => { + const app = window.__handsApp; + return app?.state?.snapshot?.fighters.find((fighter) => fighter.player_id === app.state.playerId) ?? null; + }); + const panelShown = (page) => page.evaluate(() => document.querySelector('[data-corner]')?.hidden === false); + await wait(800); + note('corner panels shown:', await panelShown(A.page), await panelShown(B.page)); + await A.page.screenshot({ path: `${out}/e2e-rest-A-panel.png` }); await B.page.screenshot({ path: `${out}/e2e-rest-B-panel.png` }); + const beforeA = await own(A.page); + const beforeB = await own(B.page); + await A.page.keyboard.press('3'); + await B.page.tap('[data-corner-pick="corner_cut"]'); + await wait(1200); + const afterA = await own(A.page); + const afterB = await own(B.page); + const corner = await Promise.all([A.page, B.page].map((page) => page.evaluate(() => document.querySelector('.corner-status')?.textContent ?? null))); + note('corner choices:', afterA?.corner_choice, afterB?.corner_choice, '|', corner.join(' | ')); + note('Alpha health', beforeA?.conditioning, '->', afterA?.conditioning, '| Bravo cuts', beforeB?.trauma.left_cut, beforeB?.trauma.right_cut, '->', afterB?.trauma.left_cut, afterB?.trauma.right_cut); + const cutBefore = Math.max(beforeB?.trauma.left_cut ?? 0, beforeB?.trauma.right_cut ?? 0); + const cutAfter = Math.max(afterB?.trauma.left_cut ?? 0, afterB?.trauma.right_cut ?? 0); + const cornerWorked = afterA?.corner_choice === 'breath' && afterB?.corner_choice === 'cut' + && afterA.conditioning === Math.min(1000, beforeA.conditioning + 180) + && cutAfter === Math.max(0, cutBefore - 250); + note('CORNER CHECK:', cornerWorked ? 'PASS' : 'FAIL'); + if (!cornerWorked) report.errors.push('corner instructions did not take effect'); + await wait(2500); await A.page.screenshot({ path: `${out}/e2e-rest-A.png` }); await B.page.screenshot({ path: `${out}/e2e-rest-B.png` }); + await B.page.setViewportSize({ width: 390, height: 844 }); + await wait(700); + await B.page.screenshot({ path: `${out}/e2e-rest-B-portrait.png` }); + await B.page.setViewportSize({ width: 844, height: 390 }); const round2 = await waitFor(A.page, (s) => /Round 2\. fight/.test(s.summary ?? ''), 40000, 'round 2'); - note('round 2 reached:', round2 !== null, '|', round2?.summary?.slice(0, 60)); + note('round 2 reached:', round2 !== null, '|', round2?.summary?.slice(0, 60), '| panels hidden:', !(await panelShown(A.page)) && !(await panelShown(B.page))); await A.page.keyboard.down('d'); await B.page.keyboard.down('a'); await wait(2500); await A.page.keyboard.up('d'); await B.page.keyboard.up('a'); await A.page.screenshot({ path: `${out}/e2e-rest-A-round2.png` }); const final = await waitFor(A.page, (s) => Boolean(s.final), 90000, 'final'); @@ -406,6 +598,7 @@ async function main() { note('rematch enabled after', ((Date.now() - enabledAt) / 1000).toFixed(1), 's:', JSON.stringify(await rematchState(A.page)), JSON.stringify(await rematchState(B.page))); await A.page.click('[data-rematch]'); await B.page.click('[data-rematch]'); + await Promise.all([settleStyle(A.page), settleStyle(B.page)]); const second = await waitFor(A.page, (s) => /countdown|\. fight\./.test(s.summary ?? '') && !s.final, 60000, 'second bout start'); note('second bout started:', second !== null, '|', second?.status, '|', second?.summary?.slice(0, 60)); await waitFor(A.page, (s) => /\. fight\./.test(s.summary ?? ''), 20000, 'second fight phase'); @@ -432,6 +625,7 @@ async function main() { for (let i = 0; i < 60; i += 1) { if ((await rematchReady(A.page)) && (await rematchReady(B.page))) break; await wait(500); } await A.page.click('[data-rematch]'); await B.page.click('[data-rematch]'); + await Promise.all([settleStyle(A.page), settleStyle(B.page)]); const next = await waitFor(A.page, (s) => /countdown|\. fight\./.test(s.summary ?? '') && !s.final, 60000, `bout ${bout + 1} start`); note(`bout ${bout + 1} started: ${next !== null}`); } @@ -448,7 +642,7 @@ async function main() { } if (scenario === 'response') { - note(`network delay ${responseDelayMs} ms each way (${responseDelayMs * 2} ms round trip); times are key press to the first rendered frame, within one frame`); + note(`network delay ${responseDelayMs} ms each way (${responseDelayMs * 2} ms round trip), up to ${responseJitterMs} ms of jitter; times are key press to the first rendered frame, within one frame`); const probe = () => { const state = { frames: [], keys: [] }; window.__probe = state; @@ -460,7 +654,7 @@ async function main() { const latest = renderer?.buffer?.latest?.(); if (graphs && latest) { const Vector3 = graphs[0].boxer.root.position.constructor; - const frame = { at: epoch(), viewer: latest.fighters.findIndex((fighter) => fighter.player_id === renderer.viewerId), fighters: [] }; + const frame = { at: epoch(), viewer: latest.fighters.findIndex((fighter) => fighter.player_id === renderer.viewerId), news: latest.fighters.map((fighter) => fighter.action_id), delay: renderer.buffer.interpolationDelayTicks, fighters: [] }; for (const graph of graphs) { const left = graph.boxer.rig.bones.gloveL.getWorldPosition(new Vector3()); const right = graph.boxer.rig.bones.gloveR.getWorldPosition(new Vector3()); @@ -490,7 +684,7 @@ async function main() { const median = (values) => { const sorted = values.filter((v) => v !== null).sort((x, y) => x - y); return sorted.length === 0 ? null : Math.round(sorted[Math.floor(sorted.length / 2)]); }; const viewerA = a.frames.at(-1).viewer; const punchKeys = a.keys.filter((key) => ['KeyF', 'KeyU', 'KeyG'].includes(key.code)); - const local = []; const moved = []; const remote = []; + const local = []; const moved = []; const remote = []; const shown = []; for (const key of punchKeys) { const hand = key.code === 'KeyU' ? 'right' : 'left'; const before = frameAt(a.frames, key.at).fighters[viewerA]; @@ -498,6 +692,9 @@ async function main() { const glove = a.frames.find((frame) => frame.at > key.at && distance(frame.fighters[viewerA][hand], before[hand]) > 0.03); const seen = b.frames.find((frame) => frame.at > key.at && frame.fighters[viewerA].active && frame.fighters[viewerA].age < 8); local.push(started ? started.at - key.at : null); moved.push(glove ? glove.at - key.at : null); remote.push(seen ? seen.at - key.at : null); + const known = frameAt(b.frames, key.at).news[viewerA]; + const news = b.frames.find((frame) => frame.at > key.at && frame.news[viewerA] !== null && frame.news[viewerA] !== known); + shown.push(news && seen ? seen.at - news.at : null); } // Progress through the punch (0 at the press, 1 at contact, 2 at the end of the active phase, 3 when // recovered), so a change in the server's timing is not mistaken for the glove going back. @@ -532,6 +729,8 @@ async function main() { note('own punch starts on screen, ms:', JSON.stringify(local.map((v) => (v === null ? null : Math.round(v)))), '| median', median(local)); note('own glove has moved 3 cm, ms:', JSON.stringify(moved.map((v) => (v === null ? null : Math.round(v)))), '| median', median(moved)); note('opponent sees the punch start, ms:', JSON.stringify(remote.map((v) => (v === null ? null : Math.round(v)))), '| median', median(remote)); + note('of which waiting on the opponent\'s screen after the news arrived, ms:', JSON.stringify(shown.map((v) => (v === null ? null : Math.round(v)))), '| median', median(shown)); + note('opponent\'s playback delay, ticks:', JSON.stringify([...new Set(b.frames.map((frame) => frame.delay))])); const step = a.keys.find((key) => key.code === 'KeyA'); if (step) { const before = frameAt(a.frames, step.at).fighters[viewerA]; @@ -613,13 +812,129 @@ async function main() { report.errors.push(...A.errors, ...B.errors); } finally { - server.child.kill('SIGTERM'); + // On Windows `uv run` starts the server two processes down; killing uv alone leaves it running + // with this script's pipes open, and the script never exits. + if (process.platform === 'win32') spawnSync('taskkill', ['/pid', String(server.child.pid), '/T', '/F'], { stdio: 'ignore' }); + else server.child.kill('SIGTERM'); server.proxy?.close(); await browser.close(); + const serverErrors = server.log.join('').split('\n').filter((l) => /error|warning|Traceback/i.test(l) && !/healthz/.test(l)); + if (scenario === 'cpu') { + console.log('client errors:', JSON.stringify(report.errors.filter((e) => !/GL Driver/.test(e)).slice(0, 20), null, 1)); + console.log('server log lines of interest:', JSON.stringify(serverErrors.slice(0, 20), null, 1)); + } } + if (scenario === 'cpu') return; const serverErrors = server.log.join('').split('\n').filter((l) => /error|warning|Traceback/i.test(l) && !/healthz/.test(l)); console.log('client errors:', JSON.stringify(report.errors.filter((e) => !/GL Driver/.test(e)).slice(0, 20), null, 1)); console.log('server log lines of interest:', JSON.stringify(serverErrors.slice(0, 20), null, 1)); } +/** One player calls in the computer from the waiting screen and boxes it to the result card. */ +async function runCpu(A, note) { + const level = process.env.E2E_CPU_LEVEL || 'contender'; + const picker = await A.page.waitForSelector('[data-cpu]:not([hidden])', { timeout: 60000 }); + note('computer offered while waiting:', picker !== null, '|', (await status(A.page)).status); + await A.page.screenshot({ path: `${out}/e2e-cpu-waiting.png` }); + await A.page.click(`[data-cpu-level="${level}"]`); + note('asked for:', level, '|', (await status(A.page)).status); + await A.page.waitForSelector('[data-style-picker]:not([hidden])', { timeout: 30000 }); + note('the computer has picked:', (await pickerState(A.page))?.status); + await A.page.keyboard.press('Enter'); + const started = await waitFor(A.page, (s) => /\. fight\./.test(s.summary ?? ''), 30000, 'fight phase'); + note('bout started against:', /computer opponent/.test(started?.summary ?? '') ? 'the computer' : 'someone else', '|', started?.summary?.slice(0, 160)); + const begun = Date.now(); + let final = null; let downs = 0; let shots = 0; let rested = false; let cornered = null; + while (Date.now() - begun < 240000) { + const s = await status(A.page); + if (s.final) { final = s; break; } + if (/\. rest\./.test(s.summary ?? '')) { + // Between rounds the computer gives its corner an instruction like a player does. + rested = true; + const choice = await A.page.evaluate(() => window.__handsApp?.state?.snapshot?.fighters.find((fighter) => fighter.player_id.startsWith('cpu:'))?.corner_choice ?? null); + if (choice !== null && choice !== 'balanced') cornered = choice; + await wait(250); + continue; + } + if (/You are down/.test(s.summary ?? '')) { + const press = /Press left/.test(s.summary) ? 'ArrowLeft' : /Press right/.test(s.summary) ? 'ArrowRight' : null; + if (press) { await A.page.keyboard.press(press); downs += 1; } + await wait(120); + continue; + } + // A plain plan: step in, jab and follow with a right, keep the guard up between. + await A.page.keyboard.down('d'); await wait(150); await A.page.keyboard.up('d'); + await A.page.keyboard.press('f'); await wait(120); await A.page.keyboard.press(['u', 'h', 'y'][shots % 3]); + await A.page.keyboard.down('q'); await wait(450); await A.page.keyboard.up('q'); + if (shots % 10 === 4) await A.page.screenshot({ path: `${out}/e2e-cpu-bout-${shots}.png` }); + shots += 1; + } + note('get-up presses:', downs); + note('computer corner instruction in the rest:', rested ? (cornered ?? 'none (a forgetful corner, or the rest was missed)') : 'no rest reached'); + if (rested && cornered === null && level === 'champion') A.errors.push('the champion gave its corner no instruction'); + if (final === null) final = await waitFor(A.page, (s) => Boolean(s.final), 120000, 'final'); + note('FINAL:', final?.final); + for (let i = 0; i < 80; i += 1) { if (await A.page.evaluate(() => window.__handsApp?.renderer?.resultVisible ?? false)) break; await wait(250); } + await wait(800); + await A.page.screenshot({ path: `${out}/e2e-cpu-result.png` }); + note('result card shown:', await A.page.evaluate(() => window.__handsApp?.renderer?.resultVisible ?? false), '| status:', (await status(A.page)).status); + const rematch = await A.page.waitForSelector('[data-rematch]:not([disabled]):not([hidden])', { timeout: 30000 }).catch(() => null); + note('rematch offered:', rematch !== null); + if (rematch !== null) { + await A.page.click('[data-rematch]'); + await settleStyle(A.page); + const again = await waitFor(A.page, (s) => /countdown|\. fight\./.test(s.summary ?? '') && !s.final, 60000, 'rematch start'); + note('rematch against the computer started:', /computer opponent/.test(again?.summary ?? '')); + } +} + +/** Alpha picks with the keyboard and Bravo by touch while Charlie watches; the bout starts once both settle. */ +async function pickStyles(A, B, open, note, report) { + await Promise.all([A.page.waitForSelector('[data-style-picker]:not([hidden])', { timeout: 60000 }), B.page.waitForSelector('[data-style-picker]:not([hidden])', { timeout: 60000 })]); + note('pick shown to Alpha:', JSON.stringify(await pickerState(A.page))); + const C = await open('Charlie'); + await C.page.waitForSelector('[data-style-picker]:not([hidden])', { timeout: 30000 }); + await C.page.keyboard.press('Digit3'); + const watching = await pickerState(C.page); + note('pick shown to the spectator:', JSON.stringify(watching)); + if (watching?.disabled !== 5 || watching.chosen !== null) report.errors.push('the spectator could pick a style'); + await A.page.keyboard.press('ArrowRight'); + await A.page.keyboard.press('ArrowRight'); + note('Alpha moves to:', (await pickerState(A.page))?.chosen); + await A.page.keyboard.press('Enter'); + // Bravo learns that Alpha has settled, never on what: both styles are revealed at the bell. + let heard = null; + for (let i = 0; i < 40 && heard === null; i += 1) { const state = await pickerState(B.page); if (/Alpha is ready/.test(state?.status ?? '')) heard = state; else await wait(250); } + note('Bravo hears:', heard?.status ?? 'nothing'); + if (heard === null) report.errors.push('Bravo never heard that Alpha had settled'); + else if (/slugger/i.test(heard.status ?? '')) report.errors.push("Bravo saw Alpha's style before the bell"); + const watched = (await pickerState(C.page))?.status ?? ''; + note('the spectator hears:', watched); + if (/slugger/i.test(watched)) report.errors.push("the spectator saw Alpha's style before the bell"); + await A.page.screenshot({ path: `${out}/e2e-styles-A-pick.png` }); + await B.page.screenshot({ path: `${out}/e2e-styles-B-pick.png` }); + // A phone on its side, narrower than 700 px: the five cards in one row, each with its strengths. + for (const [width, height] of [[568, 320], [667, 375]]) { + await B.page.setViewportSize({ width, height }); + const layout = await B.page.evaluate(() => [...document.querySelectorAll('.style-cards button')].map((button) => ({ top: Math.round(button.getBoundingClientRect().top), stats: getComputedStyle(button.querySelector('.style-stats')).display }))); + note(`pick on a ${width}x${height} phone:`, JSON.stringify(layout)); + if (new Set(layout.map((card) => card.top)).size !== 1 || layout.some((card) => card.stats === 'none')) report.errors.push(`the pick at ${width}x${height} does not show the five cards in a row with their strengths`); + } + await B.page.setViewportSize({ width: 844, height: 390 }); + await B.page.tap('[data-style="swarmer"]'); + const started = await waitFor(A.page, (s) => /countdown|\. fight\./.test(s.summary ?? ''), 20000, 'bout start after the pick'); + note('bout started once both settled:', started !== null); + await wait(600); + await A.page.screenshot({ path: `${out}/e2e-styles-A-intro.png` }); + const styles = await A.page.evaluate(() => { const state = window.__handsApp?.state; return state?.snapshot?.fighters.map((fighter) => `${state.players[fighter.player_id]?.name}:${fighter.style}`) ?? null; }); + note('styles in the bout:', JSON.stringify(styles)); + if (JSON.stringify(styles) !== JSON.stringify(['Alpha:slugger', 'Bravo:swarmer'])) report.errors.push(`unexpected styles in the bout: ${JSON.stringify(styles)}`); + const remembered = [await A.page.evaluate(() => localStorage.getItem('hands.style.v1')), await B.page.evaluate(() => localStorage.getItem('hands.style.v1'))]; + note('remembered for next time:', JSON.stringify(remembered)); + if (remembered[0] !== 'slugger' || remembered[1] !== 'swarmer') report.errors.push(`styles not remembered: ${JSON.stringify(remembered)}`); + note('pick hidden once the bout began:', (await pickerState(A.page))?.hidden, (await pickerState(C.page))?.hidden); + report.errors.push(...C.errors); + await C.context.close(); +} + main().catch((e) => { console.error('ERR', e.stack || e.message); process.exit(1); }); diff --git a/scripts/hands_e2e_server.py b/scripts/hands_e2e_server.py index f052933f..21358f31 100644 --- a/scripts/hands_e2e_server.py +++ b/scripts/hands_e2e_server.py @@ -81,7 +81,12 @@ async def run(args: argparse.Namespace) -> None: port=args.port, dev_mode=True, auth=DevAuth(), - rooms=HandsRoomManager(repository, config=RoomConfig(engine_config=engine_config)), + rooms=HandsRoomManager( + repository, + config=RoomConfig( + engine_config=engine_config, style_select_seconds=args.style_select_seconds + ), + ), ) await server.start() try: @@ -105,6 +110,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser.add_argument("--rounds", type=int, default=3) parser.add_argument("--round-seconds", type=int, default=120) parser.add_argument("--rest-seconds", type=int, default=15) + parser.add_argument("--style-select-seconds", type=float, default=10.0) return parser.parse_args(argv) diff --git a/scripts/hands_parity_fixtures.py b/scripts/hands_parity_fixtures.py new file mode 100644 index 00000000..756785c7 --- /dev/null +++ b/scripts/hands_parity_fixtures.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +"""Write the tables the client's prediction tests check against, taken from the real engine. + +The client predicts a punch's timing and its fighter's footwork the way the engine will work them +out, so a press shows at once. Its tests check that against these files, and +tests/test_hands/test_parity_fixtures.py checks that the files still match the engine, so a rule +change that the client does not follow fails on one side or the other: + +- punch-timing-table.json: startup and recovery of every punch for a balanced fighter, across + conditioning and body trauma near the fatigue steps; +- style-timing-table.json: the same, with the stamina price, for every other style; +- movement-traces.json: what the engine sent, tick by tick, while a fighter holding a direction + was parried, flinched mid-punch or finished a punch. + + uv run python scripts/hands_parity_fixtures.py # rewrite the files + uv run python scripts/hands_parity_fixtures.py --check # fail if they are out of date +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from intelstream.hands.engine import BoxingEngine, EngineConfig +from intelstream.hands.protocol import encode_snapshot +from intelstream.hands.rules import PUNCH_RULES, STYLE_RULES, style_punch_rule +from intelstream.hands.types import ( + DefensivePose, + FighterStyle, + Hand, + InputCommand, + Power, + PunchAction, + PunchClass, + Stance, + Target, +) + +if TYPE_CHECKING: + from collections.abc import Callable, Iterator + +ROOT = Path(__file__).resolve().parent.parent +FIXTURES = ROOT / "web" / "hands" / "src" / "test" +# Conditioning either side of the fatigue steps a punch's own cost can cross, and the extremes. +PUNCH_CONDITIONING = (0, 101, 260, 445, 553, 640, 695, 697, 699, 733, 820) +PUNCH_CONDITIONING += (839, 841, 843, 901, 947, 949, 951, 955, 983, 1000) +STYLE_CONDITIONING = (0, 260, 553, 820, 1000) +BODY_TRAUMA = (0, 900) + + +def _engine(styles: tuple[FighterStyle, FighterStyle], gap: int) -> BoxingEngine: + engine = BoxingEngine( + match_id="parity", + activity_instance_id="parity", + guild_id="parity", + player_one_id="one", + player_two_id="two", + seed=1, + config=EngineConfig(countdown_ticks=0, flash_ko_enabled=False), + styles=styles, + ) + engine.fighter("one").x = -(gap // 2) + engine.fighter("two").x = gap - gap // 2 + return engine + + +def _punches() -> Iterator[tuple[str, PunchAction]]: + for punch_class in PunchClass: + for target in Target: + for power in Power: + for hand in Hand: + key = punch_class.value[0] + target.value[0] + power.value[0] + hand.value[0] + yield key, PunchAction(hand, punch_class, target, power) + + +def _timing(style: FighterStyle, action: PunchAction, conditioning: int, body: int) -> str: + engine = _engine((style, FighterStyle.BALANCED), 360) + fighter = engine.fighter("one") + fighter.stance = Stance.ORTHODOX + fighter.conditioning = conditioning + fighter.trauma.body = body + engine.step({"one": InputCommand(1, 1, actions=(action,))}) + attack = fighter.attack + assert attack is not None + return f"{attack.rule.startup},{attack.rule.recovery}" + + +def punch_timing_rows() -> list[str]: + return [ + f"{key},{conditioning},{body},{_timing(FighterStyle.BALANCED, action, conditioning, body)}" + for key, action in _punches() + for conditioning in PUNCH_CONDITIONING + for body in BODY_TRAUMA + ] + + +def style_timing_rows() -> list[str]: + rows = [] + for style in FighterStyle: + if style is FighterStyle.BALANCED: + continue + for key, action in _punches(): + rule = PUNCH_RULES[(action.punch_class, action.target, action.power)] + cost = style_punch_rule(rule, STYLE_RULES[style]).stamina_cost + for conditioning in STYLE_CONDITIONING: + for body in BODY_TRAUMA: + timing = _timing(style, action, conditioning, body) + rows.append(f"{style.value},{key},{conditioning},{body},{timing},{cost}") + return rows + + +def _trace( + engine: BoxingEngine, held: InputCommand, script: Callable[[int], dict[str, InputCommand]] +) -> list[dict[str, Any]]: + """Steps `engine` for 45 ticks, fighter one holding `held` and `script(tick)` adding inputs, + and records what the engine sends fighter one every tick, with the input he holds.""" + records = [] + for sequence in range(1, 46): + inputs = script(engine.tick + 1) + inputs.setdefault("one", InputCommand(sequence, sequence, held.move_x, held.move_y)) + snapshot = engine.step(inputs) + payload = json.loads(encode_snapshot(snapshot, viewer_id="one"))["payload"] + records.append( + { + "tick": snapshot.tick, + "fighter": payload["fighters"][0], + "events": payload["events"], + "held": {"moveX": held.move_x, "moveY": held.move_y, "defense": "none"}, + } + ) + return records + + +def movement_traces() -> dict[str, list[dict[str, Any]]]: + back = InputCommand(0, 0, -1000, 0) + straight = PunchAction(Hand.RIGHT, PunchClass.STRAIGHT, Target.HEAD, Power.POWER) + parried = _engine((FighterStyle.BALANCED, FighterStyle.BALANCED), 130) + contact = 1 + PUNCH_RULES[(PunchClass.STRAIGHT, Target.HEAD, Power.POWER)].startup + + def parry(tick: int) -> dict[str, InputCommand]: + inputs = {} + if tick == 1: + inputs["one"] = InputCommand(tick, tick, back.move_x, back.move_y, actions=(straight,)) + if tick == contact - 2: + inputs["two"] = InputCommand(tick, tick, defense=DefensivePose.GUARD_HIGH) + return inputs + + hook = PunchAction(Hand.LEFT, PunchClass.HOOK, Target.HEAD, Power.NORMAL) + jab = PunchAction(Hand.LEFT, PunchClass.JAB, Target.HEAD, Power.NORMAL) + flinched = _engine((FighterStyle.BALANCED, FighterStyle.BALANCED), 100) + + def counter(tick: int) -> dict[str, InputCommand]: + # His hook is still winding up when the other man's quicker jab lands on it. + if tick != 1: + return {} + return { + "one": InputCommand(tick, tick, back.move_x, back.move_y, actions=(hook,)), + "two": InputCommand(tick, tick, actions=(jab,)), + } + + finished = _engine((FighterStyle.BALANCED, FighterStyle.BALANCED), 300) + + def alone(tick: int) -> dict[str, InputCommand]: + if tick != 1: + return {} + return {"one": InputCommand(tick, tick, back.move_x, back.move_y, actions=(jab,))} + + return { + "parried while holding back": _trace(parried, back, parry), + "flinched mid-hook while holding back": _trace(flinched, back, counter), + "a jab thrown holding back": _trace(finished, back, alone), + } + + +def documents() -> dict[Path, str]: + return { + FIXTURES / "punch-timing-table.json": json.dumps(punch_timing_rows(), separators=(",", ":")) + + "\n", + FIXTURES / "style-timing-table.json": json.dumps(style_timing_rows()), + FIXTURES / "movement-traces.json": json.dumps( + movement_traces(), separators=(",", ":"), sort_keys=True + ) + + "\n", + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--check", action="store_true", help="fail if a file is out of date") + args = parser.parse_args() + stale = [] + for path, text in documents().items(): + current = path.read_text(encoding="utf-8") if path.exists() else None + if current == text: + continue + stale.append(path.name) + if not args.check: + path.write_text(text, encoding="utf-8", newline="\n") + print(f"wrote {path.relative_to(ROOT)}") + if args.check and stale: + raise SystemExit(f"out of date, run scripts/hands_parity_fixtures.py: {', '.join(stale)}") + if not stale: + print("client parity fixtures match the engine") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/hands_replay.py b/scripts/hands_replay.py index 63841935..2f39a671 100644 --- a/scripts/hands_replay.py +++ b/scripts/hands_replay.py @@ -5,6 +5,17 @@ schedule, recording every tick's protocol-encoded snapshot plus attack internals. The output feeds the browser lab (pause/step/slow-mo/overlay) and the latency/timing report. Re-running must produce byte-identical JSON. + +A boxer fights a slugger through the rules the snapshot checksum is there to +lock in: two jabs into the guard and a straight that drops the man behind it, +who beats the count and takes the standing eight; off the x axis, a circle that +takes an angle on the man who stands, so his straight goes past, and a jab whose +recovery the straight behind it cuts short; a power straight that rocks a hurt +man and a punch landing inside the stun it started; a clinch the referee breaks; +a low blow, the warning and the referee sending them apart; a clinch broken by +the bell; the rest, the walk to the corners and the corners' work; and in round +two a body shot that drops a man with his body broken down a moment later. The +script stops with an error if any of them stops happening. """ from __future__ import annotations @@ -12,15 +23,21 @@ import argparse import hashlib import json +from math import isqrt from pathlib import Path -from intelstream.hands.engine import BoxingEngine, EngineConfig +from intelstream.hands.engine import BoxingEngine, EngineConfig, FighterState from intelstream.hands.protocol import encode_snapshot from intelstream.hands.types import ( + ActionKind, DefensivePose, + FighterStyle, + Foul, + FoulAction, Hand, InputCommand, MatchPhase, + MovementAction, Power, PunchAction, PunchClass, @@ -29,6 +46,51 @@ ROOT = Path(__file__).resolve().parent.parent OUTPUT = ROOT / "web" / "hands" / "replays" / "golden.json" +SEED = 20260805 +STYLES = (FighterStyle.BOXER, FighterStyle.SLUGGER) +# Long enough for both to walk to their corners and turn round to sit facing the ring. +REST_TICKS = 240 +MAX_TICKS = 4000 +JAB = PunchAction(Hand.LEFT, PunchClass.JAB, Target.HEAD, Power.NORMAL) +STRAIGHT = PunchAction(Hand.RIGHT, PunchClass.STRAIGHT, Target.HEAD, Power.NORMAL) +# Events marked on the timeline, by the stage of the schedule they come in. +MARKED_EVENTS = { + ("get_up", "get_up"): "get_up", + ("get_up", "box"): "box", + ("outflanked", "whiff"): "outflanked_whiff", + ("rocked", "stun"): "rocked", + ("rocked", "hit"): "hit_inside_the_stun", + ("clinch", "clinch"): "clinch", + ("clinch", "referee_break"): "referee_break", + ("foul", "foul"): "foul", + ("foul", "resume"): "resume", + ("bell_clinch", "clinch"): "clinch_before_the_bell", + ("bell_clinch", "bell"): "round_end", + ("rest", "corner"): "corner", + ("rest", "bell"): "round_start", + ("round_two", "body_collapse"): "body_collapse", + ("round_two", "knockdown"): "body_knockdown", +} +REQUIRED_MARKERS = ( + "knockdown", + "get_up", + "box", + "outflanked_whiff", + "recovery_cancel", + "rocked", + "hit_inside_the_stun", + "clinch", + "referee_break", + "foul", + "resume", + "clinch_before_the_bell", + "round_end", + "bell_breaks_the_clinch", + "corner", + "round_start", + "body_collapse", + "body_knockdown", +) def lab_engine(seed: int) -> BoxingEngine: @@ -42,12 +104,13 @@ def lab_engine(seed: int) -> BoxingEngine: config=EngineConfig( rounds=3, round_ticks=1_000_000, - rest_ticks=10, + rest_ticks=REST_TICKS, countdown_ticks=0, flash_ko_enabled=False, doctor_cut_threshold=700, doctor_swelling_threshold=820, ), + styles=STYLES, ) engine.fighter("one").x = -300 engine.fighter("two").x = 300 @@ -72,18 +135,32 @@ def command( ) -def jab(sequence: int, defense: DefensivePose = DefensivePose.NONE) -> InputCommand: - return command( - sequence, - defense=defense, - actions=(PunchAction(Hand.LEFT, PunchClass.JAB, Target.HEAD, Power.NORMAL),), +def _part(value: int, scale: int, distance: int) -> int: + magnitude = abs(value) * scale // distance + return magnitude if value >= 0 else -magnitude + + +def gap(mover: FighterState, other: FighterState) -> int: + return isqrt((other.x - mover.x) ** 2 + (other.y - mover.y) ** 2) + + +def heading(mover: FighterState, other: FighterState) -> tuple[int, int]: + """A full-speed walk straight at the other man.""" + distance = max(1, gap(mover, other)) + return ( + _part(other.x - mover.x, 1000, distance), + _part(other.y - mover.y, 1000, distance), ) -def straight(sequence: int) -> InputCommand: - return command( - sequence, - actions=(PunchAction(Hand.RIGHT, PunchClass.STRAIGHT, Target.HEAD, Power.NORMAL),), +def circling(mover: FighterState, other: FighterState, *, keep: int) -> tuple[int, int]: + """A full-speed walk anticlockwise round the other man, closing or opening to `keep`.""" + dx, dy = other.x - mover.x, other.y - mover.y + distance = max(1, gap(mover, other)) + radial = max(-1000, min(1000, (distance - keep) * 50)) + return ( + _part(-dy, 1000, distance) + _part(dx * radial, 1, distance), + _part(dx, 1000, distance) + _part(dy * radial, 1, distance), ) @@ -112,98 +189,213 @@ def main() -> int: ) args = parser.parse_args() - engine = lab_engine(20260805) + engine = lab_engine(SEED) + one = engine.fighter("one") two = engine.fighter("two") + sequences = {"one": 0, "two": 0} + + def send( + player_id: str, + *, + move: tuple[int, int] = (0, 0), + defense: DefensivePose = DefensivePose.NONE, + actions: tuple = (), + ) -> InputCommand: + sequences[player_id] += 1 + return command( + sequences[player_id], move_x=move[0], move_y=move[1], defense=defense, actions=actions + ) - schedule: dict[int, dict[str, InputCommand]] = {} - one_sequence = 0 - two_sequence = 0 - - def schedule_at( - tick: int, *, one: InputCommand | None = None, two_cmd: InputCommand | None = None - ) -> None: - frame: dict[str, InputCommand] = {} - if one is not None: - frame["one"] = one - if two_cmd is not None: - frame["two"] = two_cmd - schedule.setdefault(tick, {}).update(frame) - - # Fight-phase command schedule (planned against absolute ticks after the - # countdown is observed; the schedule is built dynamically below). ticks: list[dict] = [] timeline: list[dict] = [] + setup: dict[str, object] = {"two_poise_set_before_straight": 1} + + def mark(name: str, tick: int) -> None: + timeline.append({"marker": name, "tick": tick}) + stage = "countdown" - jab_count = 0 + stage_tick = 0 + seen: set[str] = set() + submitted = 0 guard_engaged = False straight_thrown = False knockdown_tick: int | None = None - max_ticks = 4000 + sent: dict[str, int] = {} + end_tick: int | None = None - for _ in range(max_ticks): + for _ in range(MAX_TICKS): tick = engine.tick - inputs = schedule.get(tick, {}) + inputs: dict[str, InputCommand] = {} + next_stage = stage + # The opening exchange on the x axis: a jab into the guard twice, then a straight. if stage == "countdown": if engine.phase is MatchPhase.FIGHT: - stage = "approach" + next_stage = "approach" elif stage == "approach": - one = engine.fighter("one") - distance = abs(two.x - one.x) - if distance > 140: - one_sequence += 1 - inputs["one"] = command(one_sequence, move_x=1000) + if abs(two.x - one.x) > 140: + inputs["one"] = send("one", move=(1000, 0)) else: - stage = "guard" + next_stage = "guard" elif stage == "guard": if not guard_engaged: - two_sequence += 1 - inputs["two"] = command(two_sequence, defense=DefensivePose.GUARD_HIGH) + inputs["two"] = send("two", defense=DefensivePose.GUARD_HIGH) guard_engaged = True else: - stage = "jab_one" + next_stage = "jab_one" elif stage == "jab_one": - if jab_count == 0: - one_sequence += 1 - two_sequence += 1 - inputs["one"] = jab(one_sequence) - inputs["two"] = command(two_sequence, defense=DefensivePose.GUARD_HIGH) - jab_count = 1 - timeline.append({"marker": "jab_1_submit", "tick": tick}) - elif engine.fighter("one").attack is None and tick > timeline[-1]["tick"] + 12: - stage = "reset" + if "jab_one" not in sent: + inputs["one"] = send("one", actions=(JAB,)) + inputs["two"] = send("two", defense=DefensivePose.GUARD_HIGH) + sent["jab_one"] = submitted = tick + mark("jab_1_submit", tick) + elif one.attack is None and tick > submitted + 12: + next_stage = "reset" elif stage == "reset": - one_sequence += 1 - two_sequence += 1 - inputs["one"] = command(one_sequence) - inputs["two"] = command(two_sequence, defense=DefensivePose.GUARD_HIGH) - if tick > timeline[-1]["tick"] + 26: - stage = "jab_two" + inputs["one"] = send("one") + inputs["two"] = send("two", defense=DefensivePose.GUARD_HIGH) + if tick > submitted + 26: + next_stage = "jab_two" elif stage == "jab_two": - one_sequence += 1 - two_sequence += 1 - inputs["one"] = jab(one_sequence) - inputs["two"] = command(two_sequence, defense=DefensivePose.GUARD_HIGH) - jab_count = 2 - timeline.append({"marker": "jab_2_submit", "tick": tick}) - stage = "straight_wait" + inputs["one"] = send("one", actions=(JAB,)) + inputs["two"] = send("two", defense=DefensivePose.GUARD_HIGH) + submitted = tick + mark("jab_2_submit", tick) + next_stage = "straight_wait" elif stage == "straight_wait": - if engine.fighter("one").attack is None and tick > timeline[-1]["tick"] + 12: - stage = "straight" + if one.attack is None and tick > submitted + 12: + next_stage = "straight" elif stage == "straight" and not straight_thrown: two.poise = 1 - one_sequence += 1 - two_sequence += 1 - inputs["one"] = straight(one_sequence) - inputs["two"] = command(two_sequence) + inputs["one"] = send("one", actions=(STRAIGHT,)) + inputs["two"] = send("two") straight_thrown = True - timeline.append({"marker": "straight_submit", "tick": tick}) + mark("straight_submit", tick) + + # Two beats the count and takes the standing eight. + elif stage == "get_up": + prompt = two.get_up_prompt + middle = (two.get_up_window_start_tick + two.get_up_window_end_tick) // 2 + if prompt is not None and not two.get_up_prompt_resolved and tick + 1 >= middle: + inputs["two"] = send("two", actions=(MovementAction(prompt),)) + if engine.phase is MatchPhase.FIGHT: + next_stage = "close_in" + + # Off the x axis: one circles the man who stands, takes an angle and lets his hands go. + elif stage == "close_in": + if gap(one, two) > 150: + inputs["one"] = send("one", move=heading(one, two)) + inputs["two"] = send("two", move=heading(two, one)) + else: + inputs["two"] = send("two") + next_stage = "circle" + elif stage == "circle": + inputs["one"] = send("one", move=circling(one, two, keep=120)) + if tick - stage_tick >= 30: + inputs["two"] = send("two", actions=(STRAIGHT,)) + mark("outflanked_straight_submit", tick) + next_stage = "outflanked" + elif stage == "outflanked": + inputs["one"] = send("one", move=circling(one, two, keep=120)) + if two.attack is None and "whiff" in seen: + next_stage = "combination" + elif stage == "combination": + # A jab, and the straight that cuts its recovery short. + if "jab" not in sent: + inputs["one"] = send("one", actions=(JAB,)) + sent["jab"] = tick + elif "straight" not in sent: + inputs["one"] = send("one", actions=(STRAIGHT,)) + sent["straight"] = tick + elif one.attack is None: + next_stage = "rocked" + + # A hurt man rocked by a power straight, and hit again inside the stun it started. + elif stage == "rocked": + if "power" not in sent: + if two.stunned_ticks == 0 and tick >= two.rocked_immune_until_tick: + two.poise = 170 + setup["two_poise_set_before_power_straight"] = 170 + power = PunchAction(Hand.RIGHT, PunchClass.STRAIGHT, Target.HEAD, Power.POWER) + inputs["one"] = send("one", actions=(power,)) + sent["power"] = tick + elif "inside" not in sent: + if one.attack is None and two.stunned_ticks > 0: + inputs["one"] = send("one", actions=(STRAIGHT,)) + sent["inside"] = tick + elif one.attack is None and two.stunned_ticks == 0: + next_stage = "clinch" + + # Two ties him up until the referee breaks them, then fouls him and is warned. + elif stage == "clinch": + if "clinch" not in sent: + if gap(one, two) > 95: + inputs["two"] = send("two", move=heading(two, one)) + else: + inputs["two"] = send("two", actions=(MovementAction(ActionKind.CLINCH),)) + sent["clinch"] = tick + elif "referee_break" in seen: + next_stage = "foul" + elif stage == "foul": + if "foul" not in sent: + if gap(one, two) > 95: + inputs["two"] = send("two", move=heading(two, one)) + else: + inputs["two"] = send("two", actions=(FoulAction(Foul.LOW_BLOW),)) + sent["foul"] = tick + elif "resume" in seen: + next_stage = "bell_clinch" + + # One ties him up in turn as the round ends: the bell breaks them, and they rest. + elif stage == "bell_clinch": + if "bell_clinch" not in sent: + if gap(one, two) > 95: + inputs["one"] = send("one", move=heading(one, two)) + else: + inputs["one"] = send("one", actions=(MovementAction(ActionKind.CLINCH),)) + sent["bell_clinch"] = tick + elif "clinch" in seen and "bell" not in sent: + engine.phase_ticks_remaining = 10 + setup["round_one_ends_ten_ticks_into_the_second_clinch"] = True + sent["bell"] = tick + elif engine.phase is MatchPhase.REST: + next_stage = "rest" + elif stage == "rest": + if "corners" not in sent: + inputs["one"] = send("one", actions=(MovementAction(ActionKind.CORNER_CUT),)) + inputs["two"] = send("two", actions=(MovementAction(ActionKind.CORNER_SWELLING),)) + sent["corners"] = tick + elif engine.phase is MatchPhase.FIGHT: + next_stage = "round_two" + + # Round two: a body shot on a man with his body broken down drops him a moment later. + elif stage == "round_two": + if "body" not in sent: + if gap(one, two) > 105: + inputs["one"] = send("one", move=heading(one, two)) + inputs["two"] = send("two", move=heading(two, one)) + else: + two.trauma.body = 700 + two.stamina = 300 + setup["two_body_and_stamina_set_before_the_body_hook"] = [700, 300] + body_hook = PunchAction(Hand.LEFT, PunchClass.HOOK, Target.BODY, Power.POWER) + inputs["one"] = send("one", actions=(body_hook,)) + inputs["two"] = send("two") + sent["body"] = tick + elif engine.phase is MatchPhase.KNOCKDOWN and end_tick is None: + end_tick = tick + 40 + if knockdown_tick is None and engine.phase is MatchPhase.KNOCKDOWN: knockdown_tick = tick - timeline.append({"marker": "knockdown", "tick": tick}) - if knockdown_tick is not None and tick > knockdown_tick + 40: + mark("knockdown", tick) + next_stage = "get_up" + if end_tick is not None and tick > end_tick: break + if next_stage != stage: + stage, stage_tick, seen = next_stage, tick, set() + attack_before = one.attack + stunned_before = two.stunned_ticks snapshot = engine.step(inputs if inputs else None) ticks.append( { @@ -213,17 +405,33 @@ def schedule_at( "attack_two": attack_internals(engine, "two"), } ) + kinds = {event.kind for event in snapshot.events} + seen |= kinds + if attack_before is not None and one.attack not in (None, attack_before): + # The follow-up started while the punch before it was still on: a recovery cancel. + mark("recovery_cancel", snapshot.tick) + for event in snapshot.events: + name = MARKED_EVENTS.get((stage, event.kind)) + if name == "hit_inside_the_stun" and not stunned_before: + continue + if name is not None: + mark(name, snapshot.tick) + if stage == "bell_clinch" and {"bell", "referee_break"} <= kinds: + mark("bell_breaks_the_clinch", snapshot.tick) else: - raise SystemExit("golden replay did not reach knockdown within the tick budget") + raise SystemExit("golden replay did not finish its schedule within the tick budget") - if knockdown_tick is None: - raise SystemExit("golden replay ended without a knockdown") + reached = {marker["marker"] for marker in timeline} + missing = [name for name in REQUIRED_MARKERS if name not in reached] + if missing: + raise SystemExit(f"golden replay never reached: {', '.join(missing)}") document = { "format": 1, - "seed": 20260805, + "seed": SEED, "tick_rate": 30, - "setup": {"two_poise_set_before_straight": 1}, + "styles": {"one": STYLES[0].value, "two": STYLES[1].value}, + "setup": setup, "markers": timeline, "ticks": ticks, } @@ -238,7 +446,7 @@ def schedule_at( return 0 OUTPUT.parent.mkdir(parents=True, exist_ok=True) - OUTPUT.write_text(encoded) + OUTPUT.write_text(encoded, newline="\n") print(f"wrote {OUTPUT} ({len(ticks)} ticks, sha256 {digest})") for marker in timeline: print(f" {marker['marker']}: tick {marker['tick']}") diff --git a/src/intelstream/database/repository.py b/src/intelstream/database/repository.py index df896b56..2a545741 100644 --- a/src/intelstream/database/repository.py +++ b/src/intelstream/database/repository.py @@ -43,7 +43,7 @@ calculate_elo, repeat_pairing_k_factor, ) -from intelstream.hands.types import FinishMethod, MatchResult +from intelstream.hands.types import FighterStyle, FinishMethod, MatchResult logger = structlog.get_logger() @@ -68,6 +68,11 @@ "WHERE posted_to_discord = 0 AND summary IS NOT NULL", ) +# A boxing record counts a doctor's stoppage as a technical knockout. +HANDS_KNOCKOUT_METHODS: frozenset[FinishMethod] = frozenset( + {FinishMethod.KO, FinishMethod.FLASH_KO, FinishMethod.TKO, FinishMethod.DOCTOR_STOPPAGE} +) + MIN_POLL_INTERVAL_MINUTES = 1 MAX_POLL_INTERVAL_MINUTES = 1440 SQLITE_IN_BATCH_SIZE = 900 @@ -99,6 +104,7 @@ async def initialize(self) -> None: await conn.run_sync(Base.metadata.create_all) await self._migrate_sources_table(conn) await self._migrate_content_item_indexes(conn) + await self._recount_hands_knockouts(conn) logger.info("Database initialization complete") async def _migrate_sources_table(self, conn: AsyncConnection) -> None: @@ -116,6 +122,26 @@ async def _migrate_content_item_indexes(self, conn: AsyncConnection) -> None: for statement in CONTENT_ITEM_INDEX_MIGRATIONS: await conn.execute(text(statement)) + async def _recount_hands_knockouts(self, conn: AsyncConnection) -> None: + """Counts each fighter's knockouts again from the stored bouts. + + Wins by doctor stoppage were once left out. The count is exact and idempotent, so it runs at + every start and only writes a row whose count was wrong. + """ + counted = ( + select(func.count()) + .select_from(HandsMatch) + .where( + HandsMatch.guild_id == HandsRating.guild_id, + HandsMatch.winner_id == HandsRating.user_id, + HandsMatch.finish_method.in_(sorted(HANDS_KNOCKOUT_METHODS)), + ) + .scalar_subquery() + ) + await conn.execute( + update(HandsRating).where(HandsRating.knockouts != counted).values(knockouts=counted) + ) + async def migrate_sources_to_channel(self, guild_id: str, channel_id: str) -> int: """Assign existing sources without a channel to the specified guild and channel.""" async with self.session() as session: @@ -938,12 +964,22 @@ async def get_hands_match(self, match_id: str) -> HandsMatch | None: ) return result.scalar_one_or_none() - async def record_hands_match(self, match_result: MatchResult) -> HandsMatch: + async def record_hands_match( + self, + match_result: MatchResult, + *, + styles: tuple[FighterStyle, FighterStyle] | None = None, + ) -> HandsMatch: + """Records a rated bout; `styles` are the two fighters' styles, in player order.""" self._validate_hands_result(match_result) async with self._hands_write_lock: - return await self._record_hands_match_locked(match_result) + return await self._record_hands_match_locked(match_result, styles) - async def _record_hands_match_locked(self, match_result: MatchResult) -> HandsMatch: + async def _record_hands_match_locked( + self, + match_result: MatchResult, + styles: tuple[FighterStyle, FighterStyle] | None, + ) -> HandsMatch: try: async with self.session() as session, session.begin(): existing_result = await session.execute( @@ -1017,16 +1053,17 @@ async def _record_hands_match_locked(self, match_result: MatchResult) -> HandsMa separators=(",", ":"), sort_keys=True, ) - result_json = json.dumps( - { - "player_one_damage": match_result.player_one_damage, - "player_one_knockdowns": match_result.player_one_knockdowns, - "player_two_damage": match_result.player_two_damage, - "player_two_knockdowns": match_result.player_two_knockdowns, - }, - separators=(",", ":"), - sort_keys=True, - ) + result_details: dict[str, object] = { + "player_one_damage": match_result.player_one_damage, + "player_one_knockdowns": match_result.player_one_knockdowns, + "player_two_damage": match_result.player_two_damage, + "player_two_knockdowns": match_result.player_two_knockdowns, + } + if styles is not None: + # Win rates by style in rated play are what a balance change is checked against. + result_details["player_one_style"] = styles[0].value + result_details["player_two_style"] = styles[1].value + result_json = json.dumps(result_details, separators=(",", ":"), sort_keys=True) match = HandsMatch( match_id=match_result.match_id, activity_instance_id=match_result.activity_instance_id, @@ -1113,11 +1150,7 @@ def _apply_hands_outcome(one: HandsRating, two: HandsRating, match_result: Match winner.current_streak += 1 loser.losses += 1 loser.current_streak = 0 - if match_result.finish_method in ( - FinishMethod.KO, - FinishMethod.FLASH_KO, - FinishMethod.TKO, - ): + if match_result.finish_method in HANDS_KNOCKOUT_METHODS: winner.knockouts += 1 async def record_suck_boobs_usage( diff --git a/src/intelstream/hands/auth.py b/src/intelstream/hands/auth.py index bb4bc2c6..7b3ad24c 100644 --- a/src/intelstream/hands/auth.py +++ b/src/intelstream/hands/auth.py @@ -225,6 +225,15 @@ async def begin(self, instance_id: object) -> tuple[str, ActivityInstance]: raise HandsAuthError("service_unavailable") return state, validated + def instance_for_state(self, state: object) -> str | None: + """The Activity instance a live OAuth state was issued for, without spending the state.""" + if not isinstance(state, str): + return None + record = self._states.get(state) + if record is None or record.expires_at <= self._monotonic(): + return None + return record.instance_id + async def exchange(self, *, code: object, state: object) -> AuthExchange: self._ensure_open() if not isinstance(code, str) or not 1 <= len(code) <= MAX_CODE_LENGTH: diff --git a/src/intelstream/hands/combat-manifest.json b/src/intelstream/hands/combat-manifest.json index 68610775..e44c0f0d 100644 --- a/src/intelstream/hands/combat-manifest.json +++ b/src/intelstream/hands/combat-manifest.json @@ -15,7 +15,7 @@ "action_buffer_ticks": 6, "facing": { "scale": 1000, - "turn_percent_per_update": 35 + "turn_degrees_per_second": 75 }, "combos": { "recovery_cancel_percent": 50, @@ -43,8 +43,25 @@ ] }, "knockdown": { - "neutral_separation": 230, - "referee_walk_speed": 4 + "referee_walk_speed": 5, + "mandatory_count": 8, + "box_pause_ticks": 60, + "get_up_stun_ticks": 20, + "get_up_stamina": 500, + "get_up_base": 34, + "get_up_per_knockdown": 20, + "get_up_trauma_divisor": 18 + }, + "countdown": { + "opening_ticks": 240 + }, + "tired": { + "startup_ticks": 3, + "recovery_ticks": 5, + "impact_percent": 40 + }, + "foul": { + "separation": 45 }, "hitstop_ms": { "hit": 78, @@ -175,6 +192,127 @@ "hold_distance": 60, "draw_speed": 6 }, + "corner": { + "cut": { + "worse_cut": 250, + "other_cut": 100, + "bleeding_kept_percent": 30 + }, + "swelling": { + "swelling": 250, + "eyes": 200 + }, + "breath": { + "conditioning": 180, + "body": 150 + }, + "balanced": { + "worse_cut": 80, + "bleeding_kept_percent": 75, + "swelling": 80, + "eyes": 60 + } + }, + "blind_side": { + "eye_threshold": 700, + "impact_percent": 120 + }, + "doctor": { + "cut": 800, + "swelling": 900 + }, + "parry": { + "stagger_ticks": 20 + }, + "body": { + "wind_percent": 90, + "collapse_body_trauma": 650, + "collapse_stamina": 350, + "collapse_minimum_damage": 40, + "collapse_delay_ticks": 10, + "collapse_cooldown_ticks": 300 + }, + "styles": { + "balanced": {}, + "boxer": { + "recovery_ticks": { + "jab": -1 + }, + "reach_percent": 102, + "impact_percent": 98, + "poise_damage_percent": 98 + }, + "slugger": { + "impact_percent": 108, + "poise_damage_percent": 108, + "poise_taken_percent": 95, + "stamina_cost_percent": 115, + "recovery_ticks": { + "hook": 1, + "uppercut": 1 + }, + "move_speed_percent": 96 + }, + "swarmer": { + "move_speed_percent": 103, + "startup_ticks": { + "hook": -1, + "uppercut": -1 + }, + "body_damage_percent": 110, + "conditioning_loss_percent": 85, + "impact_percent": 96, + "poise_damage_percent": 96, + "evasion_ticks": 1 + }, + "counter_puncher": { + "counter_bonus_percent": 38, + "counter_window_ticks": 3, + "evasion_ticks": 1, + "perfect_block_ticks": 1, + "impact_percent": 96, + "poise_damage_percent": 96 + } + }, + "stun": { + "flinch_minimum_damage": 36, + "flinch_base_ticks": 5, + "flinch_damage_divisor": 16, + "rocked_counter_damage": 50, + "rocked_power_damage": 70, + "rocked_hurt_poise": 180, + "rocked_base_ticks": 16, + "rocked_damage_divisor": 6, + "rocked_max_ticks": 36, + "chain_max_ticks": 45, + "immunity_ticks": 24, + "rocked_immunity_ticks": 8, + "moving_speed_percent": 75 + }, + "guard": { + "block_poise_percent": 25, + "perfect_block_poise_percent": 0, + "leak_base_percent": 70, + "leak_minimum_percent": 10, + "block_minimum": 60, + "perfect_rearm_ticks": 6, + "held_regen_every_ticks": 4, + "damage_percent": 60, + "stamina_regen_percent": 50 + }, + "trauma": { + "head_per_damage_percent": 100, + "eye_per_damage_percent": 70, + "cut_per_damage_percent": 50, + "jab_cut_percent": 25, + "jab_swelling_percent": 50, + "swelling_per_damage_percent": 33, + "poise_ceiling_per_head_percent": 30, + "poise_ceiling_floor": 220, + "body_per_damage_percent": 100, + "poise_regen_every_ticks": 2, + "poise_damage_percent": 100 + }, "hitbox": { "glove_radius": 0.11 } diff --git a/src/intelstream/hands/cpu.py b/src/intelstream/hands/cpu.py new file mode 100644 index 00000000..28b68ff4 --- /dev/null +++ b/src/intelstream/hands/cpu.py @@ -0,0 +1,1061 @@ +"""Computer opponent for Hands: a boxer that plays through the same inputs a person sends. + +The brain reads both fighters from the authoritative engine once per tick and answers with one +`InputCommand`. It only notices the opponent's punch after a reaction drawn afresh for each punch, +so the fast lead jab can only be met with a guard that is already up, while a champion slips, +weaves or times a perfect block against anything slower. It reads the server's state as it is, +where a player sees each punch a connection's trip late, so even a champion's reactions are kept +within a few ticks of a sharp player's. Everything it does goes through `BoxingEngine.submit_input` +under the same rules as a player's input. +""" + +from __future__ import annotations + +import random +from dataclasses import dataclass, replace +from enum import StrEnum +from math import atan2, cos, hypot, radians, sin + +from intelstream.hands.engine import ( + EVASION_TICKS, + PERFECT_BLOCK_TICKS, + BoxingEngine, + FighterState, +) +from intelstream.hands.rules import ( + BLIND_SIDE_EYE_THRESHOLD, + BODY_COLLAPSE_STAMINA, + BODY_COLLAPSE_TRAUMA, + COMPATIBLE_COMBO_CHAINS, + FACING_SCALE, + FACING_TURN_DEGREES_PER_SECOND, + FIGHTER_RADIUS, + GUARD_BLOCK_MINIMUM, + PERFECT_BLOCK_REARM_TICKS, + PUNCH_RULES, + RING_CORNER_REACH, + RING_HALF_HEIGHT, + RING_HALF_WIDTH, + STYLE_RULES, + TICKS_PER_SECOND, + PunchRule, + poise_ceiling, + style_punch_rule, +) +from intelstream.hands.types import ( + ActionKind, + DefensivePose, + FighterStyle, + Hand, + InputCommand, + MatchPhase, + MovementAction, + Power, + PunchAction, + PunchClass, + SemanticAction, + Stance, + Target, +) + +CPU_ID_PREFIX = "cpu:" +MOVE_SCALE = 1000 +EVASION_STAMINA = 25 +CLINCH_RANGE = 115 +CLINCH_STAMINA = 45 +THREAT_RANGE = 190 +TAUNT_RANGE = 230 +ROCKED_TICKS = 12 +HURT_POISE = 160 +# A battered head lowers the poise a fighter can get back, so "nearly out of it" is a share of that. +HURT_CEILING_PERCENT = 70 +OPPONENT_HURT_POISE = 200 +CORNER_CUT_AT = 400 +CORNER_BLEEDING_AT = 200 +CORNER_EYE_AT = 500 +CORNER_SWELLING_AT = 450 +GET_UP_ACCURACY_TRAUMA_DIVISOR = 45 +GET_UP_KNOCKDOWN_PENALTY = 14 +# The most a fighter turns toward the other man in a tick. +_TURN_PER_TICK = radians(FACING_TURN_DEGREES_PER_SECOND / TICKS_PER_SECOND) +# The quickest punch the computer loads up: it never throws a power jab. +QUICKEST_POWER_STARTUP = min( + rule.startup + for (punch_class, _target, power), rule in PUNCH_RULES.items() + if power is Power.POWER and punch_class is not PunchClass.JAB +) +_LIMIT_X = RING_HALF_WIDTH - FIGHTER_RADIUS +_LIMIT_Y = RING_HALF_HEIGHT - FIGHTER_RADIUS + + +class CpuLevel(StrEnum): + ROOKIE = "rookie" + CONTENDER = "contender" + CHAMPION = "champion" + + +@dataclass(frozen=True, slots=True) +class CpuProfile: + name: str + rating: int + reaction_ticks: int + """Ticks before the opponent's punch can be noticed.""" + reaction_spread: int + """Up to this many more ticks, drawn for each punch: nobody reacts to every punch alike.""" + read_percent: int + """Chance to answer a noticed punch with a defence that works against it.""" + perfect_percent: int + """Chance a guard answer is held back and raised late enough to be a perfect block.""" + guard_percent: int + """Chance to keep a high guard while in range and not punching.""" + aggression_percent: int + attack_interval: int + combo_length: int + combo_percent: int + reach_margin: int + """Units inside full reach before it throws; below zero it throws from too far.""" + leads_target: bool + """Aims where a moving opponent will be when the punch arrives.""" + stamina_reserve: int + footwork_percent: int + outside_distance: int + jab_bias: int + """Extra weight on the jab, which seldom stuns: a weak boxer pecks rather than hurts.""" + counter_percent: int + power_percent: int + body_percent: int + finish_percent: int + """Extra aggression against a hurt opponent.""" + admire_ticks: int + """Pause after landing before the next attack, so a weaker boxer lets a hurt man off.""" + head_movement_percent: int + """Chance per look to slip or duck ahead of a busy opponent's punches.""" + get_up_percent: int + get_up_jitter: int + clinch_percent: int + taunt_percent: int + exploit_percent: int + """Chance to aim a hook or uppercut at the side of the opponent's shut eye.""" + corner_percent: int + """Chance to give the corner the instruction the fighter needs rather than none.""" + busy_lead_percent: int + """Chance per look to lead at a man throwing punches with his hands free; a beginner mostly + waits him out, and leads at one who covers up or stands off.""" + + +PROFILES: dict[CpuLevel, CpuProfile] = { + CpuLevel.ROOKIE: CpuProfile( + name="Kid Cole", + rating=850, + reaction_ticks=10, + reaction_spread=4, + read_percent=15, + perfect_percent=0, + guard_percent=15, + aggression_percent=14, + attack_interval=34, + combo_length=2, + combo_percent=0, + reach_margin=-8, + leads_target=False, + stamina_reserve=60, + footwork_percent=25, + outside_distance=130, + jab_bias=60, + counter_percent=0, + power_percent=0, + body_percent=25, + finish_percent=0, + admire_ticks=60, + head_movement_percent=4, + get_up_percent=70, + get_up_jitter=6, + clinch_percent=15, + taunt_percent=3, + exploit_percent=0, + corner_percent=60, + busy_lead_percent=17, + ), + CpuLevel.CONTENDER: CpuProfile( + name="Marcus 'Hammer' Reed", + rating=1100, + reaction_ticks=7, + reaction_spread=3, + read_percent=64, + perfect_percent=35, + guard_percent=70, + aggression_percent=55, + attack_interval=7, + combo_length=3, + combo_percent=70, + reach_margin=4, + leads_target=True, + stamina_reserve=180, + footwork_percent=70, + outside_distance=160, + jab_bias=10, + counter_percent=65, + power_percent=55, + body_percent=45, + finish_percent=20, + admire_ticks=6, + head_movement_percent=12, + get_up_percent=85, + get_up_jitter=3, + clinch_percent=40, + taunt_percent=2, + exploit_percent=50, + corner_percent=90, + busy_lead_percent=100, + ), + CpuLevel.CHAMPION: CpuProfile( + name="Viktor 'Iron' Volkov", + rating=1400, + reaction_ticks=5, + reaction_spread=3, + read_percent=80, + perfect_percent=80, + guard_percent=80, + aggression_percent=60, + attack_interval=7, + combo_length=4, + combo_percent=80, + reach_margin=10, + leads_target=True, + stamina_reserve=260, + footwork_percent=85, + outside_distance=156, + jab_bias=0, + counter_percent=80, + power_percent=35, + body_percent=43, + finish_percent=30, + admire_ticks=0, + head_movement_percent=14, + get_up_percent=95, + get_up_jitter=1, + clinch_percent=65, + taunt_percent=1, + exploit_percent=80, + corner_percent=100, + busy_lead_percent=100, + ), +} + +# The styles each of the computer's boxers fights in, one picked per bout from the match seed. +CPU_STYLES: dict[CpuLevel, tuple[FighterStyle, ...]] = { + CpuLevel.ROOKIE: (FighterStyle.BALANCED, FighterStyle.SLUGGER, FighterStyle.SWARMER), + CpuLevel.CONTENDER: ( + FighterStyle.BOXER, + FighterStyle.SLUGGER, + FighterStyle.SWARMER, + FighterStyle.COUNTER_PUNCHER, + ), + CpuLevel.CHAMPION: ( + FighterStyle.BOXER, + FighterStyle.SLUGGER, + FighterStyle.SWARMER, + FighterStyle.COUNTER_PUNCHER, + ), +} +CPU_STYLE_SALT = 0x2545F491 + + +def cpu_style(level: CpuLevel, seed: int) -> FighterStyle: + """The style the computer's boxer fights in for the bout with this match seed.""" + pool = CPU_STYLES[level] + return pool[random.Random(seed ^ CPU_STYLE_SALT).randrange(len(pool))] # nosec B311 + + +def _percent(value: int) -> int: + return max(0, min(100, value)) + + +def styled_profile(profile: CpuProfile, style: FighterStyle) -> CpuProfile: + """How the computer boxes in a style: a boxer keeps range and jabs, a slugger loads up, a + swarmer presses and goes to the body, a counter-puncher waits and makes him pay.""" + if style is FighterStyle.BOXER: + return replace( + profile, + outside_distance=profile.outside_distance + 10, + footwork_percent=_percent(profile.footwork_percent + 10), + jab_bias=profile.jab_bias + 5, + aggression_percent=_percent(profile.aggression_percent + 4), + ) + if style is FighterStyle.SLUGGER: + return replace( + profile, + outside_distance=profile.outside_distance - 12, + jab_bias=max(0, profile.jab_bias - 10), + power_percent=_percent(profile.power_percent + 20), + combo_percent=_percent(profile.combo_percent - 10), + aggression_percent=_percent(profile.aggression_percent + 5), + footwork_percent=_percent(profile.footwork_percent - 15), + ) + if style is FighterStyle.SWARMER: + return replace( + profile, + outside_distance=profile.outside_distance - 8, + head_movement_percent=_percent(profile.head_movement_percent + 5), + clinch_percent=_percent(profile.clinch_percent - 10), + ) + if style is FighterStyle.COUNTER_PUNCHER: + return replace( + profile, + outside_distance=profile.outside_distance + 6, + aggression_percent=_percent(profile.aggression_percent - 10), + counter_percent=_percent(profile.counter_percent + 6), + read_percent=_percent(profile.read_percent + 3), + perfect_percent=_percent(profile.perfect_percent + 4), + ) + return profile + + +_FOLLOW_UPS: dict[PunchClass, tuple[PunchClass, ...]] = { + first: tuple(second for start, second in sorted(COMPATIBLE_COMBO_CHAINS) if start is first) + for first in PunchClass +} + + +def cpu_player_id(level: CpuLevel) -> str: + return f"{CPU_ID_PREFIX}{level.value}" + + +def _lead_hand(fighter: FighterState) -> Hand: + return Hand.LEFT if fighter.stance is Stance.ORTHODOX else Hand.RIGHT + + +def _other_hand(hand: Hand) -> Hand: + return Hand.RIGHT if hand is Hand.LEFT else Hand.LEFT + + +def _vision_penalty(fighter: FighterState) -> int: + trauma = fighter.trauma + return min(30, (trauma.left_eye + trauma.right_eye + trauma.swelling) // 70) + + +def _blind_to(fighter: FighterState, hand: Hand) -> bool: + """A punch from `hand` arrives on the side of this fighter's eye that is swollen shut.""" + eye = fighter.trauma.right_eye if hand is Hand.LEFT else fighter.trauma.left_eye + return eye >= BLIND_SIDE_EYE_THRESHOLD + + +def _rope_room(x: int, y: int) -> float: + """Distance from a fighter's centre to the nearest rope or corner pad.""" + corner = (RING_CORNER_REACH - abs(x) - abs(y)) / 1.4142 + return min(_LIMIT_X - abs(x), _LIMIT_Y - abs(y), corner) + + +def _evades( + action: PunchAction, rule: PunchRule, distance: float, lateral: float +) -> DefensivePose | None: + """The evasion that `BoxingEngine._evades` accepts against this punch, if there is one.""" + slip = DefensivePose.SLIP_LEFT if action.hand is Hand.RIGHT else DefensivePose.SLIP_RIGHT + if action.punch_class is PunchClass.HOOK: + if action.target is Target.HEAD or lateral >= 14: + return DefensivePose.WEAVE + return None + if action.punch_class is PunchClass.UPPERCUT: + if action.target is Target.HEAD or lateral >= 18: + return slip + return None + if action.target is Target.HEAD and distance > rule.reach * 65 // 100: + return slip if distance < rule.reach * 80 // 100 else DefensivePose.PULL + return slip + + +_EVASION_ACTIONS: dict[DefensivePose, ActionKind] = { + DefensivePose.SLIP_LEFT: ActionKind.SLIP_LEFT, + DefensivePose.SLIP_RIGHT: ActionKind.SLIP_RIGHT, + DefensivePose.WEAVE: ActionKind.WEAVE, + DefensivePose.PULL: ActionKind.PULL, +} + + +class CpuBrain: + def __init__( + self, + player_id: str, + opponent_id: str, + level: CpuLevel, + seed: int, + style: FighterStyle = FighterStyle.BALANCED, + ) -> None: + self.player_id = player_id + self.opponent_id = opponent_id + self.level = level + self.style = style + self.style_rule = STYLE_RULES[style] + self.profile = styled_profile(PROFILES[level], style) + self._rng = random.Random(seed) # nosec B311 + self._sequence = 0 + self._read_attack: tuple[int, str] | None = None + self._seen_attack: tuple[int, str] | None = None + self._reaction = 0 + self._guard_pose = DefensivePose.NONE + self._guard_from = 0 + self._guard_until = -1 + self._hold_guard = False + self._guard_window_until = 0 + self._next_attack_tick = 0 + self._combo_left = 0 + self._followed_start = -1 + self._step_in: PunchAction | None = None + self._step_in_until = -1 + self._strafe = 1 + self._strafe_until = 0 + self._punish_start = -1 + self._next_clinch_tick = 0 + self._next_taunt_tick = 0 + self._get_up_window = -1 + self._get_up_press_tick = -1 + self._get_up_key: ActionKind | None = None + self._opponent_punches: list[int] = [] + self._last_opponent_start = -1 + self._admire_until = 0 + self._grab_until = -1 + self._next_look_tick = 0 + self._landed_start = -1 + self._corner_round = -1 + self._tick = 0 + + def decide(self, engine: BoxingEngine) -> InputCommand | None: + if engine.result is not None or engine.phase is MatchPhase.COMPLETE: + return None + me = engine.fighter(self.player_id) + them = engine.fighter(self.opponent_id) + tick = engine.tick + self._tick = tick + self._sequence += 1 + if engine.phase is MatchPhase.KNOCKDOWN: + self._reset_exchange() + return self._command(tick, actions=self._get_up(tick, me)) + if engine.phase is MatchPhase.REST: + self._reset_exchange() + return self._command(tick, actions=self._corner(me, engine.round_number)) + if engine.phase is not MatchPhase.FIGHT or me.clinch_ticks or them.clinch_ticks: + self._reset_exchange() + return self._command(tick) + return self._fight(tick, me, them) + + def _corner(self, me: FighterState, round_number: int) -> tuple[SemanticAction, ...]: + """One instruction to the corner each rest, for the worst of what is wrong.""" + if me.corner_choice is not None or self._corner_round == round_number: + return () + self._corner_round = round_number + if not self._roll(self.profile.corner_percent): + return () + trauma = me.trauma + if ( + max(trauma.left_cut, trauma.right_cut) >= CORNER_CUT_AT + or trauma.bleeding >= CORNER_BLEEDING_AT + ): + kind = ActionKind.CORNER_CUT + elif ( + max(trauma.left_eye, trauma.right_eye) >= CORNER_EYE_AT + or trauma.swelling >= CORNER_SWELLING_AT + ): + kind = ActionKind.CORNER_SWELLING + else: + kind = ActionKind.CORNER_BREATH + return (MovementAction(kind),) + + def _command( + self, + tick: int, + move: tuple[int, int] = (0, 0), + defense: DefensivePose = DefensivePose.NONE, + actions: tuple[SemanticAction, ...] = (), + ) -> InputCommand: + return InputCommand( + sequence=self._sequence, + client_tick=tick, + move_x=move[0], + move_y=move[1], + defense=defense, + actions=actions, + ) + + def _roll(self, percent: int) -> bool: + return self._rng.randrange(100) < percent + + def _reset_exchange(self) -> None: + self._guard_until = -1 + self._combo_left = 0 + self._step_in = None + self._punish_start = -1 + + def _fight(self, tick: int, me: FighterState, them: FighterState) -> InputCommand: + profile = self.profile + distance = max(1.0, hypot(them.x - me.x, them.y - me.y)) + self._note_opponent(tick, them) + # A flinch is over in a few ticks; only a real stun, or a man nearly out of poise, is hurt. + hurt = ( + me.stunned_ticks > ROCKED_TICKS + or me.poise < HURT_POISE + or ( + me.trauma.head >= 900 + and me.poise < poise_ceiling(me.trauma.head) * HURT_CEILING_PERCENT // 100 + ) + ) + tired = me.stamina < self._reserve(me) + opponent_hurt = ( + them.stunned_ticks > ROCKED_TICKS + or them.poise < OPPONENT_HURT_POISE + or them.guard < GUARD_BLOCK_MINIMUM + ) + + evasion = self._read(tick, me, them) + held = me.held_input.defense + if evasion is not None: + return self._command(tick, defense=held, actions=(MovementAction(evasion),)) + + reacting = self._guard_from <= tick < self._guard_until + if me.attack is not None: + if me.attack.landed and me.attack.start_tick != self._landed_start: + self._landed_start = me.attack.start_tick + if profile.admire_ticks: + pause = profile.admire_ticks + self._rng.randrange(profile.admire_ticks + 1) + self._admire_until = tick + pause + follow_up = self._follow_up(me, them, opponent_hurt) + move = self._movement(tick, me, them, distance, hurt, tired, opponent_hurt) + if follow_up is not None: + return self._command(tick, move, held, (follow_up,)) + # Hands are busy with the punch: the guard only comes up for a punch it has read. + if reacting: + defense = self._guard_pose + elif me.pending_actions: + defense = held + else: + defense = DefensivePose.NONE + return self._command(tick, move, defense, ()) + + if me.pending_actions: + # A queued punch is cleared if a guard goes up, so the guard stays as it is. + move = self._movement(tick, me, them, distance, hurt, tired, opponent_hurt) + return self._command(tick, move, held, ()) + + if hurt and distance <= 170 and tick >= self._next_clinch_tick: + # Hurt: tie him up rather than run, some of the time. + self._next_clinch_tick = tick + 20 + if me.stamina >= CLINCH_STAMINA and self._roll(profile.clinch_percent): + self._grab_until = tick + 25 + if tick < self._grab_until and distance <= CLINCH_RANGE and me.stamina >= CLINCH_STAMINA: + self._grab_until = -1 + return self._command(tick, actions=(MovementAction(ActionKind.CLINCH),)) + + look = self._head_movement(tick, me, distance) + if look is not None: + return self._command(tick, defense=held, actions=(MovementAction(look),)) + + if me.stunned_ticks == 0 and tick >= self._guard_until and tick >= self._admire_until: + punch = self._attack(tick, me, them, distance, hurt, tired, opponent_hurt) + if punch is not None: + move = self._movement(tick, me, them, distance, hurt, tired, opponent_hurt) + return self._command(tick, move, DefensivePose.NONE, (punch,)) + + if self._taunts(tick, me, them, distance): + return self._command(tick, actions=(MovementAction(ActionKind.TAUNT),)) + + move = self._movement(tick, me, them, distance, hurt, tired, opponent_hurt) + return self._command(tick, move, self._defense(tick, me, distance, hurt), ()) + + def _reserve(self, me: FighterState) -> int: + """Stamina kept back: more once the body is broken down, where an empty tank means a knee. + + Never more than a battered body can still hold, or the fighter would rest for good. + """ + reserve = self.profile.stamina_reserve + if me.trauma.body >= BODY_COLLAPSE_TRAUMA: + reserve = max(reserve, BODY_COLLAPSE_STAMINA + 50) + return min(reserve, me.maximum_stamina * 2 // 3) + + def _note_opponent(self, tick: int, them: FighterState) -> None: + attack = them.attack + if attack is not None and attack.start_tick != self._last_opponent_start: + self._last_opponent_start = attack.start_tick + self._opponent_punches.append(attack.start_tick) + while self._opponent_punches and self._opponent_punches[0] < tick - 60: + self._opponent_punches.pop(0) + + def _read(self, tick: int, me: FighterState, them: FighterState) -> ActionKind | None: + """Notices the opponent's punch after the reaction delay and picks an answer. + + Returns an evasion to send now; a guard answer is remembered and held by `_defense`. + """ + attack = them.attack + if attack is None or attack.resolved: + return None + key = (attack.start_tick, attack.action.punch_class.value) + if key == self._read_attack: + return None + if key != self._seen_attack: + self._seen_attack = key + spread = self.profile.reaction_spread + self._reaction = self.profile.reaction_ticks + self._rng.randrange(spread + 1) + if tick - attack.start_tick < self._reaction: + return None + self._read_attack = key + if _blind_to(me, attack.action.hand): + # Thrown on the side of a shut eye: there is nothing to see it with. + return None + contact = attack.start_tick + attack.rule.startup + lead = contact - tick + if lead < 2: + return None + forward, lateral = self._incoming(me, them, lead) + reach = attack.rule.reach * (100 - _vision_penalty(them)) // 100 + arc = attack.rule.lateral_arc * (100 - _vision_penalty(them)) // 100 + threat = ( + FIGHTER_RADIUS // 3 < forward <= reach + and hypot(forward, lateral) <= reach + and lateral <= arc + ) + if not threat: + if self._roll(self.profile.counter_percent): + self._punish_start = attack.start_tick + return None + if not self._roll(self.profile.read_percent): + return None + guard = ( + DefensivePose.GUARD_HIGH + if attack.action.target is Target.HEAD + else DefensivePose.GUARD_LOW + ) + evasion = _evades(attack.action, attack.rule, hypot(forward, lateral), lateral) + can_evade = ( + evasion is not None + and me.attack is None + and me.stunned_ticks == 0 + and me.stamina >= EVASION_STAMINA + and lead - 1 < EVASION_TICKS + self.style_rule.evasion_ticks + ) + if can_evade and (me.defense is guard or self._roll(55)): + assert evasion is not None + self._punish_start = attack.start_tick + return _EVASION_ACTIONS[evasion] + held = me.defense is guard + window = PERFECT_BLOCK_TICKS + self.style_rule.perfect_block_ticks + # Only a guard let down long enough parries, so a parry is tried only when the late raise + # will be one; otherwise the guard simply goes up and blocks. + perfect = ( + not held + and self._rearmed(me, contact - window) + and self._roll(self.profile.perfect_percent) + ) + late = contact - (tick + 1) <= window + if not held and not perfect and late and self._rearmed(me, tick + 1): + # Raised now, the guard would be a fresh one when the punch lands: a parry, which the + # roll did not give. There is no time left for an ordinary block. + return None + self._guard_pose = guard + self._guard_until = contact + attack.rule.active + self._guard_from = contact - window - 1 if perfect else tick + if self._roll(self.profile.counter_percent): + # Blocked, he is still in his recovery when the guard comes down. + self._punish_start = attack.start_tick + return None + + @staticmethod + def _rearmed(me: FighterState, raise_tick: int) -> bool: + """Whether a guard raised on `raise_tick` would come up after being let down long enough + to parry, if it is not held again before then.""" + return raise_tick - me.guard_held_tick > PERFECT_BLOCK_REARM_TICKS + + @staticmethod + def _incoming(me: FighterState, them: FighterState, lead: int) -> tuple[float, float]: + """Where this fighter will stand, along and across the punch, when it arrives.""" + x = me.x + me.velocity_x * lead + y = me.y + me.velocity_y * lead + dx, dy = x - them.x, y - them.y + fx, fy = them.facing_x / 1000, them.facing_y / 1000 + return dx * fx + dy * fy, abs(dx * fy - dy * fx) + + def _head_movement(self, tick: int, me: FighterState, distance: float) -> ActionKind | None: + """Slips and ducks ahead of a busy opponent's punches, which a read cannot always catch.""" + if distance > 160 or tick < self._next_look_tick or len(self._opponent_punches) < 2: + return None + self._next_look_tick = tick + 18 + self._rng.randrange(12) + if me.stamina < self.profile.stamina_reserve + EVASION_STAMINA or me.stunned_ticks: + return None + if not self._roll(self.profile.head_movement_percent): + return None + return (ActionKind.SLIP_LEFT, ActionKind.SLIP_RIGHT, ActionKind.WEAVE)[ + self._rng.randrange(3) + ] + + def _defense(self, tick: int, me: FighterState, distance: float, hurt: bool) -> DefensivePose: + if self._guard_from <= tick < self._guard_until: + return self._guard_pose + if tick < self._guard_until: + return DefensivePose.NONE + if distance > THREAT_RANGE: + self._hold_guard = False + return DefensivePose.NONE + if tick >= self._guard_window_until: + self._guard_window_until = tick + 15 + self._rng.randrange(16) + busy = len(self._opponent_punches) >= 3 + blind = _blind_to(me, Hand.LEFT) or _blind_to(me, Hand.RIGHT) + chance = ( + self.profile.guard_percent + + (30 if hurt else 0) + + (15 if busy else 0) + + (25 if blind else 0) + ) + self._hold_guard = self._roll(chance) + if hurt and me.stunned_ticks == 0: + return DefensivePose.GUARD_HIGH + return DefensivePose.GUARD_HIGH if self._hold_guard else DefensivePose.NONE + + def _rule(self, action: PunchAction) -> PunchRule: + """The punch as this boxer's style throws it: its reach and its cost.""" + return style_punch_rule( + PUNCH_RULES[(action.punch_class, action.target, action.power)], self.style_rule + ) + + def _timing(self, me: FighterState, action: PunchAction) -> tuple[int, PunchRule]: + rule = self._rule(action) + lead_jab = action.punch_class is PunchClass.JAB and action.hand is _lead_hand(me) + startup = max( + 2, + rule.startup * 100 // me.fatigue + - (1 if lead_jab else 0) + + self.style_rule.startup_ticks.get(action.punch_class, 0), + ) + return startup, rule + + def _reach(self, me: FighterState, rule: PunchRule) -> int: + """How far this punch carries for this boxer: swollen eyes shorten it, as in the engine.""" + return rule.reach * (100 - _vision_penalty(me)) // 100 - self.profile.reach_margin + + def _reaches( + self, + me: FighterState, + them: FighterState, + action: PunchAction, + delay: int = 0, + turning: int | None = None, + ) -> bool: + """Whether the punch, started `delay` ticks from now, lands: judged along the way the boxer + will face after `turning` ticks of turning (every tick until it starts, by default), as + the engine judges it, where the other man will be by then.""" + startup, rule = self._timing(me, action) + vision = _vision_penalty(me) + reach = self._reach(me, rule) + arc = rule.lateral_arc * (100 - vision) // 100 - max(0, self.profile.reach_margin) + tx, ty = float(them.x), float(them.y) + if self.profile.leads_target: + tx += them.velocity_x * (delay + startup + 1) + ty += them.velocity_y * (delay + startup + 1) + fx, fy = self._facing_at_start(me, them, delay + 1 if turning is None else turning) + dx, dy = tx - me.x, ty - me.y + forward = dx * fx + dy * fy + lateral = abs(dx * fy - dy * fx) + return FIGHTER_RADIUS // 3 < forward <= reach and hypot(dx, dy) <= reach and lateral <= arc + + @staticmethod + def _facing_at_start(me: FighterState, them: FighterState, ticks: int) -> tuple[float, float]: + """The way a punch is thrown along that starts after `ticks` of turning: the facing turned + toward the other man by at most that many ticks' turn, which the engine then holds through + the punch and judges it along.""" + fx, fy = me.facing_x / FACING_SCALE, me.facing_y / FACING_SCALE + off = atan2( + fx * (them.y - me.y) - fy * (them.x - me.x), fx * (them.x - me.x) + fy * (them.y - me.y) + ) + most = _TURN_PER_TICK * ticks + turn = max(-most, min(most, off)) + return fx * cos(turn) - fy * sin(turn), fx * sin(turn) + fy * cos(turn) + + def _affordable(self, me: FighterState, action: PunchAction, reserve: int) -> bool: + return me.stamina >= self._rule(action).stamina_cost + reserve + + def _choose( + self, + me: FighterState, + them: FighterState, + distance: float, + power_percent: int, + ) -> PunchAction: + lead = _lead_hand(me) + rear = _other_hand(lead) + weights: tuple[tuple[PunchClass, int], ...] + if distance >= 130: + weights = ((PunchClass.JAB, 60), (PunchClass.STRAIGHT, 35), (PunchClass.HOOK, 5)) + elif distance >= 105: + weights = ( + (PunchClass.JAB, 25), + (PunchClass.STRAIGHT, 30), + (PunchClass.HOOK, 35), + (PunchClass.UPPERCUT, 10), + ) + else: + weights = ( + (PunchClass.JAB, 10), + (PunchClass.STRAIGHT, 15), + (PunchClass.HOOK, 45), + (PunchClass.UPPERCUT, 30), + ) + weights = ((PunchClass.JAB, self.profile.jab_bias), *weights) + pick = self._rng.randrange(sum(weight for _, weight in weights)) + punch_class = weights[-1][0] + for candidate, weight in weights: + if pick < weight: + punch_class = candidate + break + pick -= weight + if punch_class is PunchClass.JAB: + hand = lead if self._roll(85) else rear + elif punch_class is PunchClass.STRAIGHT: + hand = rear if self._roll(90) else lead + elif punch_class is PunchClass.HOOK: + hand = lead if self._roll(60) else rear + else: + hand = rear if self._roll(60) else lead + if punch_class in (PunchClass.HOOK, PunchClass.UPPERCUT): + for side in (Hand.LEFT, Hand.RIGHT): + shut = _blind_to(them, side) and not _blind_to(them, _other_hand(side)) + if shut and self._roll(self.profile.exploit_percent): + # Work the side he cannot see: a left hand lands on his right eye. + hand = side + return self._shaped(PunchAction(hand, punch_class, Target.HEAD), them, power_percent) + + def _shaped(self, action: PunchAction, them: FighterState, power_percent: int) -> PunchAction: + body_percent = self.profile.body_percent + if them.defense is DefensivePose.GUARD_HIGH: + body = self._roll(body_percent) + elif them.defense is DefensivePose.GUARD_LOW: + body = False + else: + body = self._roll(body_percent // 3) + power = action.punch_class is not PunchClass.JAB and self._roll(power_percent) + if power and self._parry_risk(them) and self._roll(self.profile.exploit_percent): + power = False + return PunchAction( + action.hand, + action.punch_class, + Target.BODY if body else Target.HEAD, + Power.POWER if power else Power.NORMAL, + ) + + def _parry_risk(self, them: FighterState) -> bool: + """A power punch at a guard that is down can be parried by a late raise, once the guard + has been down long enough to come up fresh before the punch lands. + + A guard that is up only blocks it, since the punch lands too long after the raise, and a + man punching or stunned cannot raise one in time. + """ + if them.stunned_ticks > 0 or them.attack is not None: + return False + if them.defense in (DefensivePose.GUARD_HIGH, DefensivePose.GUARD_LOW): + return False + latest_raise = self._tick + QUICKEST_POWER_STARTUP - 1 + return latest_raise - them.guard_held_tick > PERFECT_BLOCK_REARM_TICKS + + def _attack( + self, + tick: int, + me: FighterState, + them: FighterState, + distance: float, + hurt: bool, + tired: bool, + opponent_hurt: bool, + ) -> PunchAction | None: + profile = self.profile + countering = me.counter_ticks > 0 + punishing = ( + self._punish_start >= 0 + and them.attack is not None + and them.attack.start_tick == self._punish_start + and them.attack.resolved + ) + reserve = self._reserve(me) // 2 if countering or opponent_hurt else self._reserve(me) + if self._step_in is not None: + if tick > self._step_in_until or hurt: + self._step_in = None + elif self._reaches(me, them, self._step_in): + action = self._step_in + self._step_in = None + if self._affordable(me, action, 0): + return self._launch(tick, action) + return None + if countering or punishing: + power = profile.power_percent if countering else profile.power_percent // 2 + for _ in range(3): + action = self._choose(me, them, distance, power) + if self._reaches(me, them, action) and self._affordable(me, action, 0): + self._punish_start = -1 + return self._launch(tick, action) + if tick < self._next_attack_tick or (tired and not opponent_hurt): + return None + if hurt and not countering: + return None + self._next_attack_tick = tick + 2 + self._rng.randrange(2) + guarding = them.defense in (DefensivePose.GUARD_HIGH, DefensivePose.GUARD_LOW) + busy = not guarding and bool(self._opponent_punches) + if busy and profile.busy_lead_percent < 100 and not self._roll(profile.busy_lead_percent): + # A beginner pecks at a man who covers up or stands off, and mostly waits out one who + # keeps throwing with his hands free. + return None + aggression = profile.aggression_percent + (profile.finish_percent if opponent_hurt else 0) + if not self._roll(aggression): + return None + power = profile.power_percent if opponent_hurt else profile.power_percent // 4 + action = self._choose(me, them, distance, power) + if not self._affordable(me, action, reserve): + return None + if self._reaches(me, them, action): + return self._launch(tick, action) + startup, rule = self._timing(me, action) + # Whether one step brings the punch into range is judged on its full reach; the step + # itself goes as far as swollen eyes need, with the time for the extra ground. + if distance - (rule.reach - profile.reach_margin) <= 45: + self._step_in = action + self._step_in_until = ( + tick + + 10 + + startup + + (rule.reach - profile.reach_margin - self._reach(me, rule)) // 5 + ) + return None + + def _launch(self, tick: int, action: PunchAction) -> PunchAction: + profile = self.profile + self._combo_left = 0 + while self._combo_left < profile.combo_length - 1 and self._roll(profile.combo_percent): + self._combo_left += 1 + self._next_attack_tick = ( + tick + profile.attack_interval + self._rng.randrange(profile.attack_interval + 1) + ) + return action + + def _follow_up( + self, me: FighterState, them: FighterState, opponent_hurt: bool + ) -> PunchAction | None: + attack = me.attack + assert attack is not None + if not attack.resolved or attack.start_tick == self._followed_start: + return None + self._followed_start = attack.start_tick + if self._combo_left <= 0 or not attack.landed: + self._combo_left = 0 + return None + options = _FOLLOW_UPS[attack.action.punch_class] + if not options: + self._combo_left = 0 + return None + self._combo_left -= 1 + punch_class = options[self._rng.randrange(len(options))] + power = self.profile.power_percent if opponent_hurt else self.profile.power_percent // 3 + action = self._shaped( + PunchAction(_other_hand(attack.action.hand), punch_class, Target.HEAD), them, power + ) + rule = self._rule(action) + # It goes at the cancel point, along the facing the recovery has turned by then. The + # engine charges the full price before a combination's discount: short of it, the punch + # would wait out the recovery or come as a slow arm punch. + # The facing holds through the punch's startup and active ticks and turns after them. + held = attack.rule.startup + attack.rule.active + delay = max(1, attack.cancel_age - attack.age) + turning = max(1, attack.cancel_age - max(attack.age, held)) + if not self._reaches(me, them, action, delay, turning) or me.stamina < rule.stamina_cost: + self._combo_left = 0 + return None + return action + + def _taunts(self, tick: int, me: FighterState, them: FighterState, distance: float) -> bool: + if distance < TAUNT_RANGE or tick < self._next_taunt_tick: + return False + self._next_taunt_tick = tick + 30 + ahead = ( + me.knockdowns < them.knockdowns or me.conditioning > them.conditioning + 250 + ) and me.stamina > me.maximum_stamina * 3 // 4 + return ahead and self._roll(self.profile.taunt_percent) + + def _movement( + self, + tick: int, + me: FighterState, + them: FighterState, + distance: float, + hurt: bool, + tired: bool, + opponent_hurt: bool, + ) -> tuple[int, int]: + profile = self.profile + if tick < self._grab_until: + wanted = 90.0 + elif hurt or (tired and not opponent_hurt): + wanted = 240.0 + elif self._step_in is not None: + _startup, rule = self._timing(me, self._step_in) + wanted = self._reach(me, rule) - 10 + elif opponent_hurt and profile.finish_percent: + wanted = 105.0 + else: + wanted = float(profile.outside_distance) + target_x, target_y = float(them.x), float(them.y) + if profile.leads_target and wanted < profile.outside_distance: + # Cutting off the ring: head for where the opponent is going, not where he is. + target_x += them.velocity_x * 8 + target_y += them.velocity_y * 8 + dx, dy = target_x - me.x, target_y - me.y + span = max(1.0, hypot(dx, dy)) + ux, uy = dx / span, dy / span + error = distance - wanted + radial = 0.0 if abs(error) < 6 else max(-1.0, min(1.0, error / 30)) + # A flat-footed boxer is slow to take or give ground. + radial *= 0.5 + 0.5 * profile.footwork_percent / 100 + + if tick >= self._strafe_until: + self._strafe = 1 if self._roll(50) else -1 + self._strafe_until = tick + 30 + self._rng.randrange(60) + px, py = -uy, ux + footwork = profile.footwork_percent / 100 + lateral = footwork * (0.9 if hurt or tired else 0.55) + room = _rope_room(me.x, me.y) + if room < 90 and self._roll(profile.footwork_percent): + # Off the ropes: circle toward the open ring rather than along the ropes. + toward_centre = -(me.x * px + me.y * py) + self._strafe = 1 if toward_centre >= 0 else -1 + lateral = max(lateral, footwork) + move_x = ux * radial + px * lateral * self._strafe + move_y = uy * radial + py * lateral * self._strafe + if room < 40 and radial < 0: + # Backing straight into the ropes gains nothing; slide along them instead. + outward = (me.x * move_x + me.y * move_y) / max(1.0, hypot(me.x, me.y)) + if outward > 0: + norm = max(1.0, hypot(me.x, me.y)) + move_x -= me.x / norm * outward + move_y -= me.y / norm * outward + size = hypot(move_x, move_y) + if size < 0.05: + return 0, 0 + scale = MOVE_SCALE / max(1.0, size) if size > 1 else MOVE_SCALE + return round(move_x * scale), round(move_y * scale) + + def _get_up(self, tick: int, me: FighterState) -> tuple[SemanticAction, ...]: + prompt = me.get_up_prompt + if prompt is None or me.get_up_prompt_resolved: + return () + if me.get_up_window_start_tick != self._get_up_window: + self._get_up_window = me.get_up_window_start_tick + centre = (me.get_up_window_start_tick + me.get_up_window_end_tick) // 2 + jitter = self.profile.get_up_jitter + self._get_up_press_tick = centre + self._rng.randint(-jitter, jitter) + accuracy = ( + self.profile.get_up_percent + - me.trauma.head // GET_UP_ACCURACY_TRAUMA_DIVISOR + - GET_UP_KNOCKDOWN_PENALTY * max(0, me.knockdowns - 1) + ) + wrong = ( + ActionKind.GET_UP_RIGHT + if prompt is ActionKind.GET_UP_LEFT + else ActionKind.GET_UP_LEFT + ) + self._get_up_key = prompt if self._roll(max(10, accuracy)) else wrong + if tick + 1 < self._get_up_press_tick or self._get_up_key is None: + return () + key = self._get_up_key + self._get_up_key = None + return (MovementAction(key),) diff --git a/src/intelstream/hands/engine.py b/src/intelstream/hands/engine.py index 778379f1..71f805f0 100644 --- a/src/intelstream/hands/engine.py +++ b/src/intelstream/hands/engine.py @@ -9,21 +9,61 @@ from typing import Final from intelstream.hands.rules import ( + BLIND_SIDE_EYE_THRESHOLD, + BLIND_SIDE_IMPACT_PERCENT, + BLOCK_POISE_PERCENT, + BODY_COLLAPSE_COOLDOWN_TICKS, + BODY_COLLAPSE_DELAY_TICKS, + BODY_COLLAPSE_MINIMUM_DAMAGE, + BODY_COLLAPSE_STAMINA, + BODY_COLLAPSE_TRAUMA, + BODY_TRAUMA_PER_DAMAGE_PERCENT, + BODY_WIND_PERCENT, + BOX_PAUSE_TICKS, CLINCH_DRAW_SPEED, CLINCH_HOLD_DISTANCE, COMPATIBLE_COMBO_CHAINS, + CORNER_TREATMENTS, COUNTDOWN_TICKS, + CUT_PER_DAMAGE_PERCENT, DEFAULT_ROUNDS, + DOCTOR_CUT_THRESHOLD, + DOCTOR_SWELLING_THRESHOLD, + EYE_TRAUMA_PER_DAMAGE_PERCENT, FACING_SCALE, - FACING_TURN_PERCENT, + FACING_TURN_COS, + FACING_TURN_SIN, FIGHTER_RADIUS, + FLINCH_BASE_TICKS, + FLINCH_DAMAGE_DIVISOR, + FLINCH_MINIMUM_DAMAGE, + FOUL_SEPARATION, + GET_UP_BASE, + GET_UP_PER_KNOCKDOWN, + GET_UP_STAMINA, + GET_UP_STUN_TICKS, + GET_UP_TRAUMA_DIVISOR, + GUARD_BLOCK_MINIMUM, + GUARD_DAMAGE_PERCENT, + GUARD_HELD_REGEN_EVERY_TICKS, + GUARD_LEAK_BASE_PERCENT, + GUARD_LEAK_MINIMUM_PERCENT, + GUARD_STAMINA_REGEN_PERCENT, + HEAD_TRAUMA_PER_DAMAGE_PERCENT, + JAB_CUT_PERCENT, + JAB_SWELLING_PERCENT, JUDGE_PROFILES, - KNOCKDOWN_NEUTRAL_SEPARATION, + MANDATORY_COUNT, MAX_CONDITIONING, MAX_GUARD, MAX_POISE, MAX_STAMINA, MINIMUM_SEPARATION, + PARRY_STAGGER_TICKS, + PERFECT_BLOCK_POISE_PERCENT, + PERFECT_BLOCK_REARM_TICKS, + POISE_DAMAGE_PERCENT, + POISE_REGEN_EVERY_TICKS, PUNCH_RULES, RECOVERY_CANCEL_PERCENT, REFEREE_WALK_SPEED, @@ -33,19 +73,39 @@ RING_CORNER_REACH, RING_HALF_HEIGHT, RING_HALF_WIDTH, + ROCKED_BASE_TICKS, + ROCKED_COUNTER_DAMAGE, + ROCKED_DAMAGE_DIVISOR, + ROCKED_HURT_POISE, + ROCKED_IMMUNITY_TICKS, + ROCKED_MAX_TICKS, + ROCKED_POWER_DAMAGE, ROUND_TICKS, + STUN_CHAIN_MAX_TICKS, + STUN_IMMUNITY_TICKS, + STUNNED_SPEED_PERCENT, + STYLE_RULES, + SWELLING_PER_DAMAGE_PERCENT, TICKS_PER_SECOND, + TIRED_IMPACT_PERCENT, + TIRED_RECOVERY_TICKS, + TIRED_STARTUP_TICKS, JudgeProfile, PunchRule, + StyleRule, fatigue_factor, fatigue_max_stamina, + poise_ceiling, + style_punch_rule, ) from intelstream.hands.types import ( ActionKind, CombatEvent, + CornerChoice, DefensivePose, EngineSnapshot, FighterSnapshot, + FighterStyle, FinishMethod, FoulAction, Hand, @@ -68,25 +128,6 @@ COUNTER_WINDOW_TICKS: Final = 18 ROPE_OVERLAP_PASSES: Final = 8 ROPE_OVERLAP_STEP: Final = 8 -NEUTRAL_WALK_MARGIN: Final = 6 -_COMPASS: Final = ( - (1000, 0), - (924, 383), - (707, 707), - (383, 924), - (0, 1000), - (-383, 924), - (-707, 707), - (-924, 383), - (-1000, 0), - (-924, -383), - (-707, -707), - (-383, -924), - (0, -1000), - (383, -924), - (707, -707), - (924, -383), -) CLINCH_STARTUP_TICKS: Final = 8 CLINCH_TICKS: Final = 45 FOUL_RECOVERY_TICKS: Final = 60 @@ -97,6 +138,16 @@ GET_UP_WINDOW_END_OFFSET: Final = 13 TAUNT_TICKS: Final = 60 MOVEMENT_FIXED_SCALE: Final = 1000 +# Conditioning a punch costs is worked out in thousandths of a point, so a style's saving survives +# the rounding of per-punch losses of only a few points. +CONDITIONING_FIXED_SCALE: Final = 1000 +# A fresh fighter's walking speed in units a tick, and the fastest velocity a snapshot reports. +MAX_SPEED: Final = 7 +CORNER_INSTRUCTIONS: Final = { + ActionKind.CORNER_CUT: CornerChoice.CUT, + ActionKind.CORNER_SWELLING: CornerChoice.SWELLING, + ActionKind.CORNER_BREATH: CornerChoice.BREATH, +} @dataclass(frozen=True, slots=True) @@ -105,8 +156,8 @@ class EngineConfig: round_ticks: int = ROUND_TICKS rest_ticks: int = REST_TICKS countdown_ticks: int = COUNTDOWN_TICKS - doctor_cut_threshold: int = 700 - doctor_swelling_threshold: int = 820 + doctor_cut_threshold: int = DOCTOR_CUT_THRESHOLD + doctor_swelling_threshold: int = DOCTOR_SWELLING_THRESHOLD flash_ko_enabled: bool = True def __post_init__(self) -> None: @@ -165,6 +216,29 @@ class RoundPerformance: deductions: int = 0 +@dataclass(slots=True) +class PunchCount: + """A fighter's punches over the whole bout, as CompuBox counts them for the result card. + + Landed means clean: a punch the guard took, blocked or parried, did not land. The counts follow + from the event history, which the checksum already covers, so they are not hashed again. + """ + + thrown: int = 0 + landed: int = 0 + jabs_thrown: int = 0 + jabs_landed: int = 0 + + def add(self, punch_class: PunchClass, *, landed: bool) -> None: + jab = 1 if punch_class is PunchClass.JAB else 0 + if landed: + self.landed += 1 + self.jabs_landed += jab + else: + self.thrown += 1 + self.jabs_thrown += jab + + @dataclass(slots=True) class FighterState: player_id: str @@ -172,6 +246,7 @@ class FighterState: y: int facing: int stance: Stance + style: FighterStyle = FighterStyle.BALANCED facing_x: int = 0 facing_y: int = 0 velocity_x: int = 0 @@ -182,14 +257,24 @@ class FighterState: position_remainder_y: int = 0 stamina: int = MAX_STAMINA conditioning: int = MAX_CONDITIONING + conditioning_remainder: int = 0 + """Thousandths of a point of conditioning spent on punches and not yet taken off.""" guard: int = MAX_GUARD poise: int = MAX_POISE trauma: Trauma = field(default_factory=Trauma) defense: DefensivePose = DefensivePose.NONE defense_started_tick: int = -1000 + guard_held_tick: int = -1000 + """The last tick the fighter was holding a guard (his input, whatever the stun or slip did).""" + guard_raised_tick: int = -1000 + """When the guard he has up was raised, if it can perfect-block: after being let down a while.""" evasion_ticks: int = 0 stunned_ticks: int = 0 stunned_at_tick: int = -1 + staggered_until_tick: int = -1 + stun_chain_ticks: int = 0 + stun_immune_until_tick: int = -1 + rocked_immune_until_tick: int = -1 counter_ticks: int = 0 clinch_startup_ticks: int = 0 clinch_ticks: int = 0 @@ -219,7 +304,12 @@ class FighterState: last_action_until_tick: int = -1 performance: RoundPerformance = field(default_factory=RoundPerformance) damage_dealt: int = 0 + punches: PunchCount = field(default_factory=PunchCount) movement_load: int = 0 + corner_choice: CornerChoice | None = None + body_collapse_ticks: int = 0 + body_collapse_at_tick: int = -100_000 + body_collapse_action_id: str | None = None def __post_init__(self) -> None: if self.facing_x == 0 and self.facing_y == 0: @@ -229,6 +319,10 @@ def __post_init__(self) -> None: def maximum_stamina(self) -> int: return fatigue_max_stamina(self.conditioning, self.trauma.body) + @property + def style_rule(self) -> StyleRule: + return STYLE_RULES[self.style] + @property def fatigue(self) -> int: return fatigue_factor(self.conditioning, self.trauma.body) @@ -240,22 +334,32 @@ class RoundScores: player_two: int +RING_CONTROL_SECONDS: Final = 30 +"""The most ring generalship a judge counts in a round, in seconds.""" + + def score_round( player_one: RoundPerformance, player_two: RoundPerformance, profile: JudgeProfile, ) -> RoundScores: + # Ring generalship counts in seconds, up to half a minute, and only in a round in which a clean + # punch landed: it settles a close round of boxing but never outweighs a clean punch, and a + # round in which nobody lands is even however long one man held the centre of the ring. + boxed = player_one.clean_hits + player_two.clean_hits > 0 + one_control = min(player_one.control // TICKS_PER_SECOND, RING_CONTROL_SECONDS) if boxed else 0 + two_control = min(player_two.control // TICKS_PER_SECOND, RING_CONTROL_SECONDS) if boxed else 0 one_value = ( player_one.damage * profile.damage_weight + player_one.clean_hits * 18 * profile.clean_weight + (player_one.blocked_hits + player_one.evasions * 2) * 7 * profile.defense_weight - + player_one.control * profile.control_weight + + one_control * profile.control_weight ) two_value = ( player_two.damage * profile.damage_weight + player_two.clean_hits * 18 * profile.clean_weight + (player_two.blocked_hits + player_two.evasions * 2) * 7 * profile.defense_weight - + player_two.control * profile.control_weight + + two_control * profile.control_weight ) margin = one_value - two_value if player_one.knockdowns != player_two.knockdowns: @@ -292,6 +396,11 @@ def _separation_along_axis(across: int) -> int: return root if root * root == needed else root + 1 +def _rounded_divide(numerator: int, denominator: int) -> int: + magnitude = (abs(numerator) + denominator // 2) // denominator + return magnitude if numerator >= 0 else -magnitude + + def _away_divide(numerator: int, denominator: int) -> int: magnitude = -(-abs(numerator) // denominator) return magnitude if numerator >= 0 else -magnitude @@ -326,6 +435,25 @@ def _rounded_fixed_velocity(velocity: int) -> int: return rounded if velocity >= 0 else -rounded +def _report_velocity(fighter: FighterState, limit: int) -> None: + """Rounds the fixed-point velocity to the whole units a snapshot reports, within `limit`. + + The footwork stays in fixed point, so a style's few percent of footspeed survive, but the + velocity a snapshot reports keeps within the walking speed: a quick fighter's 7.7 would round + to 8, past what clients accept. + """ + fighter.velocity_x = _rounded_fixed_velocity(fighter.velocity_fixed_x) + fighter.velocity_y = _rounded_fixed_velocity(fighter.velocity_fixed_y) + while ( + fighter.velocity_x * fighter.velocity_x + fighter.velocity_y * fighter.velocity_y + > limit * limit + ): + if abs(fighter.velocity_x) >= abs(fighter.velocity_y): + fighter.velocity_x -= 1 if fighter.velocity_x > 0 else -1 + else: + fighter.velocity_y -= 1 if fighter.velocity_y > 0 else -1 + + def _consume_fixed_position(velocity: int, remainder: int) -> tuple[int, int]: total = velocity + remainder delta = _symmetric_divide(total, MOVEMENT_FIXED_SCALE) @@ -358,6 +486,47 @@ def _ring_point(x: int, y: int) -> tuple[int, int, bool, bool, bool]: return sign_x * cut_x, sign_y * cut_y, False, False, True +def _bearing(x: int, y: int, opponent: FighterState) -> tuple[int, int] | None: + """The unit vector, in facing units, from (x, y) to the opponent.""" + dx = opponent.x - x + dy = opponent.y - y + distance = isqrt(dx * dx + dy * dy) + if distance == 0: + return None + return _symmetric_divide(dx * FACING_SCALE, distance), _symmetric_divide( + dy * FACING_SCALE, distance + ) + + +def _turn_toward(fighter: FighterState, target: tuple[int, int], cos: int, sin: int) -> None: + """Turns the facing toward `target` by at most the angle whose cosine and sine are given, in + millionths, and onto it when it is that close.""" + target_x, target_y = target + facing_x, facing_y = fighter.facing_x, fighter.facing_y + cross = facing_x * target_y - facing_y * target_x + if facing_x * target_x + facing_y * target_y > 0 and abs(cross) <= sin: + fighter.facing_x, fighter.facing_y = target_x, target_y + else: + # Facing straight away, he turns the same way every time. + turn = sin if cross >= 0 else -sin + turned_x = facing_x * cos - facing_y * turn + turned_y = facing_x * turn + facing_y * cos + length = isqrt(turned_x * turned_x + turned_y * turned_y) + # Rounded rather than cut short, which would shave a little off every tick's turn. + fighter.facing_x = _rounded_divide(turned_x * FACING_SCALE, length) + fighter.facing_y = _rounded_divide(turned_y * FACING_SCALE, length) + fighter.facing = 1 if fighter.facing_x >= 0 else -1 + + +def _pad_stop(x: int, y: int, *, along_x: bool) -> tuple[int, int]: + """Where a push along one axis stops at the ropes and corner pads, the other coordinate held.""" + if along_x: + reach = min(RING_HALF_WIDTH - FIGHTER_RADIUS, RING_CORNER_REACH - abs(y)) + return max(-reach, min(reach, x)), y + reach = min(RING_HALF_HEIGHT - FIGHTER_RADIUS, RING_CORNER_REACH - abs(x)) + return x, max(-reach, min(reach, y)) + + def _canonical(value: object) -> object: if isinstance(value, Enum): return value.value @@ -371,6 +540,9 @@ def _canonical(value: object) -> object: class BoxingEngine: + checksums = True + """Off, snapshots carry no checksum: for offline simulation that never reads one.""" + def __init__( self, *, @@ -381,6 +553,7 @@ def __init__( player_two_id: str, seed: int, config: EngineConfig | None = None, + styles: tuple[FighterStyle, FighterStyle] = (FighterStyle.BALANCED, FighterStyle.BALANCED), ) -> None: if player_one_id == player_two_id: raise ValueError("a match requires two distinct players") @@ -399,8 +572,8 @@ def __init__( self._rng = random.Random(seed) # nosec B311 self._player_ids = (player_one_id, player_two_id) self._fighters = { - player_one_id: FighterState(player_one_id, -180, 0, 1, Stance.ORTHODOX), - player_two_id: FighterState(player_two_id, 180, 0, -1, Stance.ORTHODOX), + player_one_id: FighterState(player_one_id, -180, 0, 1, Stance.ORTHODOX, styles[0]), + player_two_id: FighterState(player_two_id, 180, 0, -1, Stance.ORTHODOX, styles[1]), } self._events: list[CombatEvent] = [] self._tick_events: list[CombatEvent] = [] @@ -413,6 +586,9 @@ def __init__( self._knockdown_count_ticks = 0 self._foul_recovery_target: str | None = None self._paused_fight_ticks = 0 + self._neutral_corner: tuple[int, int] | None = None + self._box_tick: int | None = None + self._count_at_rise = 0 @property def players(self) -> tuple[str, str]: @@ -507,11 +683,24 @@ def _advance_fight(self) -> None: two = self._fighters[self._player_ids[1]] one.movement_load = 0 two.movement_load = 0 - # The facing blends toward the opponent here and again after each fighter's footwork - # (`_move_fighter`), so in the fight phase it turns about 58% of the way per tick. + # Each turns toward the other here, at the turn rate, and with his own footwork in + # `_move_fighter`. self._update_facing(one, two) self._update_facing(two, one) + # Seat order alternates by tick, as for punches, so a trade of body shots favours nobody. + for fighter, opponent in ((one, two), (two, one))[:: 1 if self.tick % 2 == 0 else -1]: + if fighter.body_collapse_ticks > 0: + fighter.body_collapse_ticks -= 1 + if fighter.body_collapse_ticks == 0: + self._knock_down( + fighter, + opponent, + detail="body", + action_id=fighter.body_collapse_action_id, + ) + return + if one.clinch_ticks or two.clinch_ticks: self._advance_clinch(one, two) else: @@ -542,6 +731,16 @@ def _process_fighter(self, fighter: FighterState, opponent: FighterState) -> Non if fighter.combo_ticks > 0: fighter.combo_ticks -= 1 + held_guard = fighter.held_input.defense in ( + DefensivePose.GUARD_HIGH, + DefensivePose.GUARD_LOW, + ) + # A perfect block needs a real raise: a guard let down for a moment first, not one flicked + # down and back up, switched from high to low, or coming back up after a stun. + guard_rested = self.tick - fighter.guard_held_tick > PERFECT_BLOCK_REARM_TICKS + if held_guard: + fighter.guard_held_tick = self.tick + attack = fighter.attack imminent_trade = ( fighter.stunned_at_tick == self.tick @@ -550,7 +749,13 @@ def _process_fighter(self, fighter: FighterState, opponent: FighterState) -> Non ) if fighter.stunned_ticks > 0: fighter.stunned_ticks -= 1 + fighter.stun_chain_ticks += 1 + if fighter.stunned_ticks == 0: + self._clear_head(fighter) fighter.defense = DefensivePose.NONE + # A stun ends the slip, weave or pull it caught him in: he stumbles rather than standing + # frozen through the evasion's leftover ticks. + fighter.evasion_ticks = 0 fighter.clinch_startup_ticks = 0 fighter.pending_actions.clear() if not imminent_trade: @@ -568,6 +773,7 @@ def _process_fighter(self, fighter: FighterState, opponent: FighterState) -> Non else: if fighter.defense is not fighter.held_input.defense: fighter.defense_started_tick = self.tick + fighter.guard_raised_tick = self.tick if held_guard and guard_rested else -1000 fighter.defense = fighter.held_input.defense if ( @@ -634,13 +840,19 @@ def _can_cancel_recovery( return ( isinstance(follow_up, PunchAction) and (attack.action.punch_class, follow_up.punch_class) in COMPATIBLE_COMBO_CHAINS - # `_start_punch` refuses a punch the fighter cannot pay for in full. + # A punch the fighter cannot pay for in full is a tired one, and no combination. and fighter.stamina - >= PUNCH_RULES[(follow_up.punch_class, follow_up.target, follow_up.power)].stamina_cost + >= style_punch_rule( + PUNCH_RULES[(follow_up.punch_class, follow_up.target, follow_up.power)], + fighter.style_rule, + ).stamina_cost ) - def _start_punch(self, fighter: FighterState, action: PunchAction) -> bool: - base_rule = PUNCH_RULES[(action.punch_class, action.target, action.power)] + def _start_punch(self, fighter: FighterState, action: PunchAction) -> None: + style = fighter.style_rule + base_rule = style_punch_rule( + PUNCH_RULES[(action.punch_class, action.target, action.power)], style + ) cost = base_rule.stamina_cost lead_hand = "left" if fighter.stance is Stance.ORTHODOX else "right" hand_speed_bonus = ( @@ -649,11 +861,13 @@ def _start_punch(self, fighter: FighterState, action: PunchAction) -> bool: rear_power_bonus = ( 8 if action.hand.value != lead_hand and action.punch_class is PunchClass.STRAIGHT else 0 ) - if fighter.stamina < cost: - self._emit("exhausted", fighter.player_id) - return False + # Arms too heavy for the punch still throw it: a slow, weak arm punch on what breath is left. + tired = fighter.stamina < cost combo_bonus = 0 - if ( + if tired: + self._emit("exhausted", fighter.player_id) + cost = fighter.stamina + elif ( fighter.combo_ticks > 0 and fighter.last_punch is not None and (fighter.last_punch.punch_class, action.punch_class) in COMPATIBLE_COMBO_CHAINS @@ -661,21 +875,35 @@ def _start_punch(self, fighter: FighterState, action: PunchAction) -> bool: combo_bonus = 10 cost = max(1, cost * 90 // 100) fighter.stamina -= cost - fighter.conditioning = max(0, fighter.conditioning - max(1, cost // 12)) + self._spend_conditioning(fighter, max(1, cost // 12)) speed = fighter.fatigue - startup = max(2, base_rule.startup * 100 // speed - hand_speed_bonus) - recovery = max(4, base_rule.recovery * 100 // speed) + startup = max( + 2, + base_rule.startup * 100 // speed + - hand_speed_bonus + + style.startup_ticks.get(action.punch_class, 0), + ) + recovery = max( + 4, + base_rule.recovery * 100 // speed + style.recovery_ticks.get(action.punch_class, 0), + ) + strength = TIRED_IMPACT_PERCENT if tired else 100 + if tired: + startup += TIRED_STARTUP_TICKS + recovery += TIRED_RECOVERY_TICKS rule = PunchRule( startup=startup, active=base_rule.active, recovery=recovery, reach=base_rule.reach, lateral_arc=base_rule.lateral_arc, - impact=base_rule.impact + rear_power_bonus, + impact=((base_rule.impact + rear_power_bonus) * strength // 100) + * style.impact_percent + // 100, stamina_cost=base_rule.stamina_cost, whiff_cost=base_rule.whiff_cost, - guard_damage=base_rule.guard_damage, - poise_damage=base_rule.poise_damage, + guard_damage=base_rule.guard_damage * strength // 100, + poise_damage=base_rule.poise_damage * strength // 100, combo_window=base_rule.combo_window, startup_vulnerability=base_rule.startup_vulnerability, recovery_vulnerability=base_rule.recovery_vulnerability, @@ -689,7 +917,24 @@ def _start_punch(self, fighter: FighterState, action: PunchAction) -> bool: detail=f"{action.hand.value}:{action.punch_class.value}:{action.target.value}", action_id=self._action_id(fighter, fighter.attack), ) - return True + fighter.punches.add(action.punch_class, landed=False) + + @staticmethod + def _spend_conditioning(fighter: FighterState, points: int) -> None: + """Takes `points` of conditioning off at the rate the fighter's style tires at. + + The fraction of a point is carried to the next punch, so a style that tires 20% less saves + 20% of every punch, and even a tired arm punch costs it something. + """ + owed = ( + fighter.conditioning_remainder + + points + * CONDITIONING_FIXED_SCALE + * fighter.style_rule.conditioning_loss_percent + // 100 + ) + fighter.conditioning = max(0, fighter.conditioning - owed // CONDITIONING_FIXED_SCALE) + fighter.conditioning_remainder = owed % CONDITIONING_FIXED_SCALE @staticmethod def _action_key(action: PunchAction) -> str: @@ -737,7 +982,7 @@ def _resolve_punch( or lateral_distance > effective_arc ): attacker.stamina = max(0, attacker.stamina - rule.whiff_cost) - attacker.conditioning = max(0, attacker.conditioning - max(2, rule.whiff_cost // 10)) + self._spend_conditioning(attacker, max(2, rule.whiff_cost // 10)) self._emit( "whiff", attacker.player_id, @@ -747,12 +992,15 @@ def _resolve_punch( ) return - # The facing holds still through a punch, so this is how far the defender is off the line - # the punch was thrown along. Only off that line can he weave a body hook or slip a body - # uppercut. - if self._evades(defender, action, distance_squared, rule.reach, lateral_distance): + blind = self._blind_side(defender, action) + # The facing holds still through a punch, so `lateral_distance` is how far the defender is + # off the line the punch was thrown along. Only off that line can he weave a body hook or + # slip a body uppercut. + if not blind and self._evades( + defender, action, distance_squared, rule.reach, lateral_distance + ): defender.performance.evasions += 1 - defender.counter_ticks = COUNTER_WINDOW_TICKS + defender.counter_ticks = COUNTER_WINDOW_TICKS + defender.style_rule.counter_window_ticks self._emit( "evade", defender.player_id, attacker.player_id, detail=defender.defense.value ) @@ -761,33 +1009,58 @@ def _resolve_punch( guarding = ( action.target is Target.HEAD and defender.defense is DefensivePose.GUARD_HIGH ) or (action.target is Target.BODY and defender.defense is DefensivePose.GUARD_LOW) - perfect = guarding and self.tick - defender.defense_started_tick <= PERFECT_BLOCK_TICKS + # Arms too tired to hold a real guard stop nothing, and cannot be broken again. + blocked = guarding and defender.guard >= GUARD_BLOCK_MINIMUM + perfect = ( + blocked + and not blind + and self.tick - defender.guard_raised_tick + <= PERFECT_BLOCK_TICKS + defender.style_rule.perfect_block_ticks + ) counter = attacker.counter_ticks > 0 or self._counter_vulnerable(defender.attack) # A combination flows through a blocked punch but not a parried one. attack.landed = not perfect fatigue = attacker.fatigue - counter_multiplier = 128 if counter else 100 + counter_multiplier = 100 + attacker.style_rule.counter_bonus_percent if counter else 100 impact = ( rule.impact * counter_multiplier * fatigue * (100 + attack.combo_bonus) // 1_000_000 ) + if action.target is Target.BODY: + impact = impact * attacker.style_rule.body_damage_percent // 100 + if blind: + impact = impact * BLIND_SIDE_IMPACT_PERCENT // 100 impact = max(1, impact) - if guarding and defender.guard > 0: - guard_damage = rule.guard_damage + if blocked: + guard_damage = rule.guard_damage * GUARD_DAMAGE_PERCENT // 100 if perfect: guard_damage //= 3 impact //= 8 - defender.counter_ticks = COUNTER_WINDOW_TICKS + defender.counter_ticks = ( + COUNTER_WINDOW_TICKS + defender.style_rule.counter_window_ticks + ) self._emit( "perfect_block", defender.player_id, attacker.player_id, action_id=self._action_id(attacker, attack), ) + if action.power is Power.POWER: + attacker.stunned_ticks = max(attacker.stunned_ticks, PARRY_STAGGER_TICKS) + attacker.stunned_at_tick = self.tick + attacker.staggered_until_tick = self.tick + PARRY_STAGGER_TICKS + attacker.taunt_ticks = 0 + self._emit( + "parry", + defender.player_id, + attacker.player_id, + amount=PARRY_STAGGER_TICKS, + action_id=self._action_id(attacker, attack), + ) else: guard_leak = max( - 18, - 100 - defender.guard // 12 + (100 - defender.fatigue) // 2, + GUARD_LEAK_MINIMUM_PERCENT, + GUARD_LEAK_BASE_PERCENT - defender.guard // 12 + (100 - defender.fatigue) // 2, ) impact = impact * guard_leak // 100 self._emit( @@ -799,7 +1072,8 @@ def _resolve_punch( ) defender.guard = max(0, defender.guard - guard_damage) defender.performance.blocked_hits += 1 - if defender.guard == 0: + if defender.guard < GUARD_BLOCK_MINIMUM: + # Worn too thin to stop the next one: the guard is broken, and he is told so. defender.stunned_ticks = max(defender.stunned_ticks, 8) defender.stunned_at_tick = self.tick defender.taunt_ticks = 0 @@ -811,14 +1085,28 @@ def _resolve_punch( ) else: attacker.performance.clean_hits += 1 + attacker.punches.add(action.punch_class, landed=True) damage = max(1, impact) - poise_damage = rule.poise_damage * counter_multiplier // 100 + poise_damage = rule.poise_damage * counter_multiplier * POISE_DAMAGE_PERCENT // 10_000 + if blocked: + poise_damage = ( + poise_damage + * (PERFECT_BLOCK_POISE_PERCENT if perfect else BLOCK_POISE_PERCENT) + // 100 + ) + poise_damage = poise_damage * defender.style_rule.poise_taken_percent // 100 + shut_eye: str | None = None if action.target is Target.HEAD: - self._apply_head_damage(defender, action, damage) + shut_eye = self._apply_head_damage(defender, action, damage, clean=not blocked) else: - defender.trauma.body = min(1200, defender.trauma.body + damage * 2) + defender.trauma.body = min( + 1200, defender.trauma.body + damage * BODY_TRAUMA_PER_DAMAGE_PERCENT // 100 + ) defender.conditioning = max(0, defender.conditioning - damage) + if not blocked: + # A clean shot to the body takes the wind out of a fighter as well as the legs. + defender.stamina = max(0, defender.stamina - damage * BODY_WIND_PERCENT // 100) defender.poise -= poise_damage attacker.performance.damage += damage attacker.damage_dealt += damage @@ -836,8 +1124,18 @@ def _resolve_punch( direction=attacker.facing, action_id=self._action_id(attacker, attack), ) + if blind and not blocked: + self._emit( + "blind_side", + attacker.player_id, + defender.player_id, + detail="right" if action.hand is Hand.LEFT else "left", + action_id=self._action_id(attacker, attack), + ) + if shut_eye is not None: + self._emit("eye_shut", attacker.player_id, defender.player_id, detail=shut_eye) - if self._qualifies_for_flash(attacker, defender, action, rule, counter, guarding): + if self._qualifies_for_flash(attacker, defender, action, rule, counter, blocked): chance = self._flash_chance(attacker, defender, damage) roll = self._rng.randrange(10_000) self._emit( @@ -854,35 +1152,136 @@ def _resolve_punch( if self._needs_doctor_stoppage(defender): self._complete(attacker.player_id, FinishMethod.DOCTOR_STOPPAGE) return - if defender.poise <= 0 or ( - action.target is Target.HEAD and defender.trauma.head > 850 and damage >= 40 + clean_head = action.target is Target.HEAD and not blocked + if defender.poise <= 0: + if action.target is Target.BODY: + self._knock_down( + defender, + attacker, + detail="body", + action_id=self._action_id(attacker, attack), + ) + else: + self._knock_down(defender, attacker) + elif clean_head and damage >= FLINCH_MINIMUM_DAMAGE: + rocked = ( + (counter and damage >= ROCKED_COUNTER_DAMAGE) + or (action.power is Power.POWER and damage >= ROCKED_POWER_DAMAGE) + or defender.poise < ROCKED_HURT_POISE + ) + if rocked: + ticks = min(ROCKED_MAX_TICKS, ROCKED_BASE_TICKS + damage // ROCKED_DAMAGE_DIVISOR) + if self._stun(defender, ticks, rocked=True): + self._emit("stun", attacker.player_id, defender.player_id, amount=damage) + else: + self._stun( + defender, FLINCH_BASE_TICKS + damage // FLINCH_DAMAGE_DIVISOR, rocked=False + ) + elif ( + action.target is Target.BODY + and not blocked + and damage >= BODY_COLLAPSE_MINIMUM_DAMAGE + and defender.trauma.body >= BODY_COLLAPSE_TRAUMA + and defender.stamina <= BODY_COLLAPSE_STAMINA + and defender.body_collapse_ticks == 0 + and self.tick - defender.body_collapse_at_tick >= BODY_COLLAPSE_COOLDOWN_TICKS ): - self._knock_down(defender, attacker) - elif action.target is Target.HEAD and damage >= 36 and defender.stunned_ticks == 0: - # A punch on a man who is still stunned does not start his stun again, so a run of - # punches cannot hold him past the first stun. - defender.stunned_ticks = min(90, 8 + damage // 2) + # The delayed body knockdown: the fighter stands frozen for a moment, then goes to a knee. + defender.body_collapse_ticks = BODY_COLLAPSE_DELAY_TICKS + defender.body_collapse_at_tick = self.tick + defender.body_collapse_action_id = self._action_id(attacker, attack) + defender.stunned_ticks = max(defender.stunned_ticks, BODY_COLLAPSE_DELAY_TICKS + 1) defender.stunned_at_tick = self.tick defender.taunt_ticks = 0 - self._emit("stun", attacker.player_id, defender.player_id, amount=damage) + self._emit( + "body_collapse", + attacker.player_id, + defender.player_id, + amount=BODY_COLLAPSE_DELAY_TICKS, + action_id=self._action_id(attacker, attack), + ) - def _apply_head_damage(self, defender: FighterState, action: PunchAction, damage: int) -> None: - defender.trauma.head = min(1400, defender.trauma.head + damage * 2) - eye_damage = damage * (2 if action.punch_class is PunchClass.HOOK else 1) + def _stun(self, fighter: FighterState, ticks: int, *, rocked: bool) -> bool: + """Stops a fighter for `ticks`, and says whether it took. + + A clean shot makes a fresh fighter flinch; only a big counter, a big power shot or a hurt + fighter is rocked. A punch on a fighter who is still stunned does not start his stun again, + however hard it lands, and the attacker cannot cut his recovery short into him either, so a + run of punches cannot hold him past the stun the first one started. A flinch cannot land + again in the moment after a stun wears off, and even a rocking shot cannot in the first + few ticks of it, and no stun runs past the chain limit: one clean punch cannot be strung + into a knockdown with the defender unable to answer. + """ + if fighter.stunned_ticks > 0: + return False + if self.tick < ( + fighter.rocked_immune_until_tick if rocked else fighter.stun_immune_until_tick + ): + return False + fighter.stun_chain_ticks = 0 + fighter.stunned_ticks = min(ticks, STUN_CHAIN_MAX_TICKS) + fighter.stunned_at_tick = self.tick + fighter.taunt_ticks = 0 + return True + + def _clear_head(self, fighter: FighterState) -> None: + """Ends a fighter's stun. A clean shot has to wait a moment before it can stop him again, + and a mere flinch longer, so he always gets a chance to raise his guard or move.""" + fighter.stunned_ticks = 0 + fighter.stun_chain_ticks = 0 + fighter.stun_immune_until_tick = self.tick + STUN_IMMUNITY_TICKS + fighter.rocked_immune_until_tick = self.tick + ROCKED_IMMUNITY_TICKS + + def _apply_head_damage( + self, defender: FighterState, action: PunchAction, damage: int, *, clean: bool + ) -> str | None: + """Applies a head shot and returns the side of an eye it has just swollen shut, if any. + + Whatever gets through a guard still shakes the head, but only a clean punch marks the face. + """ + eyes_before = (defender.trauma.left_eye, defender.trauma.right_eye) + defender.trauma.head = min( + 1400, defender.trauma.head + damage * HEAD_TRAUMA_PER_DAMAGE_PERCENT // 100 + ) + if not clean: + return None + eye_damage = ( + damage + * (2 if action.punch_class is PunchClass.HOOK else 1) + * EYE_TRAUMA_PER_DAMAGE_PERCENT + // 100 + ) + cut = damage * CUT_PER_DAMAGE_PERCENT // 100 + if action.punch_class is PunchClass.JAB: + cut = cut * JAB_CUT_PERCENT // 100 if action.hand.value == "left": defender.trauma.right_eye = min(1000, defender.trauma.right_eye + eye_damage) if defender.trauma.right_eye > 260: - defender.trauma.right_cut = min(1000, defender.trauma.right_cut + damage) + defender.trauma.right_cut = min(1000, defender.trauma.right_cut + cut) else: defender.trauma.left_eye = min(1000, defender.trauma.left_eye + eye_damage) if defender.trauma.left_eye > 260: - defender.trauma.left_cut = min(1000, defender.trauma.left_cut + damage) - defender.trauma.swelling = min(1000, defender.trauma.swelling + damage // 2) + defender.trauma.left_cut = min(1000, defender.trauma.left_cut + cut) + swelling = damage * SWELLING_PER_DAMAGE_PERCENT // 100 + if action.punch_class is PunchClass.JAB: + swelling = swelling * JAB_SWELLING_PERCENT // 100 + defender.trauma.swelling = min(1000, defender.trauma.swelling + swelling) defender.trauma.bleeding = min( 1000, defender.trauma.bleeding + (defender.trauma.left_cut + defender.trauma.right_cut + 49) // 50, ) + eyes_after = (defender.trauma.left_eye, defender.trauma.right_eye) + for side, before, after in zip(("left", "right"), eyes_before, eyes_after, strict=True): + if before < BLIND_SIDE_EYE_THRESHOLD <= after: + return side + return None + + @staticmethod + def _blind_side(defender: FighterState, action: PunchAction) -> bool: + """A punch from this hand arrives on the side of an eye that is swollen shut.""" + eye = defender.trauma.right_eye if action.hand is Hand.LEFT else defender.trauma.left_eye + return eye >= BLIND_SIDE_EYE_THRESHOLD @staticmethod def _counter_vulnerable(attack: AttackState | None) -> bool: @@ -945,7 +1344,7 @@ def _resolve_movement_action( fighter.conditioning = max(0, fighter.conditioning - 1) fighter.defense = pose_by_action[action.kind] fighter.defense_started_tick = self.tick - fighter.evasion_ticks = EVASION_TICKS + fighter.evasion_ticks = EVASION_TICKS + fighter.style_rule.evasion_ticks self._emit("defense", fighter.player_id, detail=fighter.defense.value) return True if action.kind is ActionKind.SWITCH_STANCE: @@ -1014,6 +1413,8 @@ def _resolve_foul( opponent.stunned_ticks = 30 opponent.stunned_at_tick = self.tick opponent.taunt_ticks = 0 + # The referee stops the action: a punch the fouled man was throwing goes no further. + self._retain_action(opponent) self._emit("foul", fighter.player_id, opponent.player_id, detail=action.foul.value) if fighter.warnings == 2: fighter.deductions += 1 @@ -1022,6 +1423,15 @@ def _resolve_foul( elif fighter.warnings >= 3: self._complete(opponent.player_id, FinishMethod.DISQUALIFICATION) return + # The referee steps between them: each is sent back from the other for the recovery. + for mover in (fighter, opponent): + mover.x -= _symmetric_divide(mover.facing_x * FOUL_SEPARATION, FACING_SCALE) + mover.y -= _symmetric_divide(mover.facing_y * FOUL_SEPARATION, FACING_SCALE) + mover.velocity_x = mover.velocity_y = 0 + mover.velocity_fixed_x = mover.velocity_fixed_y = 0 + self._clamp_to_ring(fighter) + self._clamp_to_ring(opponent) + self._separate_fighters(fighter, opponent) self._paused_fight_ticks = self.phase_ticks_remaining self.phase = MatchPhase.FOUL_RECOVERY self.phase_ticks_remaining = FOUL_RECOVERY_TICKS @@ -1033,6 +1443,12 @@ def _advance_foul_recovery(self) -> None: if target is not None: target.stamina = min(target.maximum_stamina, target.stamina + 4) if self.phase_ticks_remaining <= 0: + for fighter in self._fighters.values(): + # The two seconds were the fouled man's recovery: both box on clear-headed, so + # the fouler gets no free shot at a man with his guard still forced down. + if fighter.stunned_ticks > 0: + self._clear_head(fighter) + fighter.evasion_ticks = 0 self.phase = MatchPhase.FIGHT self.phase_ticks_remaining = max(1, self._paused_fight_ticks) self._foul_recovery_target = None @@ -1050,14 +1466,19 @@ def _advance_clinch(self, one: FighterState, two: FighterState) -> None: two.conditioning = max(0, two.conditioning - (1 if self.tick % 10 == 0 else 0)) if remaining > 0: self._draw_clinch_together(one, two) - if remaining == 0: - for fighter in (one, two): - fighter.x -= _symmetric_divide(fighter.facing_x * 45, FACING_SCALE) - fighter.y -= _symmetric_divide(fighter.facing_y * 45, FACING_SCALE) - self._clamp_to_ring(one) - self._clamp_to_ring(two) - self._separate_fighters(one, two) - self._emit("referee_break", one.player_id, two.player_id) + else: + self._break_clinch(one, two) + + def _break_clinch(self, one: FighterState, two: FighterState) -> None: + """The referee steps in and parts a clinch, each fighter sent back from the other.""" + one.clinch_ticks = two.clinch_ticks = 0 + for fighter in (one, two): + fighter.x -= _symmetric_divide(fighter.facing_x * 45, FACING_SCALE) + fighter.y -= _symmetric_divide(fighter.facing_y * 45, FACING_SCALE) + self._clamp_to_ring(one) + self._clamp_to_ring(two) + self._separate_fighters(one, two) + self._emit("referee_break", one.player_id, two.player_id) def _draw_clinch_together(self, one: FighterState, two: FighterState) -> None: dx = two.x - one.x @@ -1080,23 +1501,31 @@ def _draw_clinch_together(self, one: FighterState, two: FighterState) -> None: def _move_fighter(self, fighter: FighterState, opponent: FighterState) -> None: move_x = fighter.held_input.move_x move_y = fighter.held_input.move_y - speed = max(2, 7 * fighter.fatigue // 100) + speed = max(2, MAX_SPEED * fighter.fatigue // 100) if fighter.defense in (DefensivePose.GUARD_HIGH, DefensivePose.GUARD_LOW): speed = max(2, speed * 70 // 100) + if fighter.stunned_ticks > 0: + # A stunned fighter stumbles: still on his feet, at a fraction of his footwork. + speed = max(2, speed * STUNNED_SPEED_PERCENT // 100) + # In thousandths of a unit, so a style's few percent of footspeed survive the rounding. + fixed_speed = speed * MOVEMENT_FIXED_SCALE * fighter.style_rule.move_speed_percent // 100 if ( fighter.attack is not None or fighter.evasion_ticks > 0 or fighter.clinch_startup_ticks > 0 or fighter.taunt_ticks > 0 + or fighter.body_collapse_ticks > 0 + # Parried, he is caught off balance and rooted, so the counter it opens can land. + or (fighter.stunned_ticks > 0 and self.tick < fighter.staggered_until_tick) ): move_x = 0 move_y = 0 move_x, move_y = _normalize_move_vector(move_x, move_y) - desired_fixed_x = move_x * speed - desired_fixed_y = move_y * speed + desired_fixed_x = _symmetric_divide(move_x * fixed_speed, MOVEMENT_FIXED_SCALE) + desired_fixed_y = _symmetric_divide(move_y * fixed_speed, MOVEMENT_FIXED_SCALE) fighter.velocity_fixed_x = _blend_velocity(fighter.velocity_fixed_x, desired_fixed_x) fighter.velocity_fixed_y = _blend_velocity(fighter.velocity_fixed_y, desired_fixed_y) - fixed_cap = speed * MOVEMENT_FIXED_SCALE + fixed_cap = fixed_speed fixed_magnitude_squared = ( fighter.velocity_fixed_x * fighter.velocity_fixed_x + fighter.velocity_fixed_y * fighter.velocity_fixed_y @@ -1118,22 +1547,15 @@ def _move_fighter(self, fighter: FighterState, opponent: FighterState) -> None: fighter.velocity_fixed_x -= 1 if fighter.velocity_fixed_x > 0 else -1 else: fighter.velocity_fixed_y -= 1 if fighter.velocity_fixed_y > 0 else -1 - fighter.velocity_x = _rounded_fixed_velocity(fighter.velocity_fixed_x) - fighter.velocity_y = _rounded_fixed_velocity(fighter.velocity_fixed_y) - while ( - fighter.velocity_x * fighter.velocity_x + fighter.velocity_y * fighter.velocity_y - > speed * speed - ): - if abs(fighter.velocity_x) >= abs(fighter.velocity_y): - fighter.velocity_x -= 1 if fighter.velocity_x > 0 else -1 - else: - fighter.velocity_y -= 1 if fighter.velocity_y > 0 else -1 + # The velocity a snapshot reports stays within the base speed; the footwork is in fixed point. + _report_velocity(fighter, speed) delta_x, fighter.position_remainder_x = _consume_fixed_position( fighter.velocity_fixed_x, fighter.position_remainder_x ) delta_y, fighter.position_remainder_y = _consume_fixed_position( fighter.velocity_fixed_y, fighter.position_remainder_y ) + from_x, from_y = fighter.x, fighter.y fighter.x += delta_x fighter.y += delta_y self._clamp_to_ring(fighter) @@ -1145,11 +1567,11 @@ def _move_fighter(self, fighter: FighterState, opponent: FighterState) -> None: moving = bool(input_magnitude_squared or velocity_magnitude_squared) # Squared comparisons preserve exact deterministic ceil-bucket boundaries. above_half_speed = ( - input_magnitude_squared > 500**2 or velocity_magnitude_squared > (500 * speed) ** 2 + input_magnitude_squared > 500**2 or velocity_magnitude_squared > (fixed_speed // 2) ** 2 ) movement_load = 2 if above_half_speed else int(moving) fighter.movement_load = movement_load - self._update_facing(fighter, opponent) + self._turn_with_step(fighter, opponent, from_x, from_y) @staticmethod def _clamp_to_ring(fighter: FighterState) -> None: @@ -1168,8 +1590,7 @@ def _clamp_to_ring(fighter: FighterState) -> None: half = (outward + 1) // 2 fighter.velocity_fixed_x -= sign_x * half fighter.velocity_fixed_y -= sign_y * half - fighter.velocity_x = _rounded_fixed_velocity(fighter.velocity_fixed_x) - fighter.velocity_y = _rounded_fixed_velocity(fighter.velocity_fixed_y) + _report_velocity(fighter, MAX_SPEED) fighter.position_remainder_x = fighter.position_remainder_y = 0 else: if rope_x: @@ -1188,7 +1609,6 @@ def _separate_fighters(self, one: FighterState, two: FighterState) -> None: dy = two.y - one.y if dx * dx + dy * dy >= MINIMUM_SEPARATION**2: return - before = (one.x, one.y, two.x, two.y) if abs(dx) >= abs(dy): direction = 1 if dx > 0 or (dx == 0 and one.player_id < two.player_id) else -1 center = (one.x + two.x) // 2 @@ -1211,15 +1631,14 @@ def _separate_fighters(self, one: FighterState, two: FighterState) -> None: shift = max(0, minimum - min(one.y, two.y)) - max(0, max(one.y, two.y) - maximum) one.y += shift two.y += shift - pushed = (one.x, one.y, two.x, two.y) + for fighter in (one, two): + # A corner pad stops the push where it meets him, without sliding him along it, and + # the one with room gives way by what is still missing. Starting again from where + # they met instead kept a fighter's step off the pad each tick, so a pair pressing + # into a corner crept onto it and bounced off every few ticks. + fighter.x, fighter.y = _pad_stop(fighter.x, fighter.y, along_x=abs(dx) >= abs(dy)) self._clamp_to_ring(one) self._clamp_to_ring(two) - if (one.x, one.y, two.x, two.y) != pushed: - # A corner pad refused part of the push. Start again from where they met, so the - # same fighter gives way by the same amount every tick they keep pressing. - one.x, one.y, two.x, two.y = before - self._clamp_to_ring(one) - self._clamp_to_ring(two) self._resolve_rope_overlap(one, two) def _resolve_rope_overlap(self, one: FighterState, two: FighterState) -> None: @@ -1260,36 +1679,41 @@ def _resolve_rope_overlap(self, one: FighterState, two: FighterState) -> None: self._clamp_to_ring(two) def _update_facing(self, fighter: FighterState, opponent: FighterState) -> None: - if fighter.attack is not None or fighter.clinch_startup_ticks: + """Turns the fighter toward the other man, by no more than a tick's turn.""" + if self._facing_held(fighter): return - dx = opponent.x - fighter.x - dy = opponent.y - fighter.y - distance = isqrt(dx * dx + dy * dy) - if distance == 0: + bearing = _bearing(fighter.x, fighter.y, opponent) + if bearing is not None: + _turn_toward(fighter, bearing, FACING_TURN_COS, FACING_TURN_SIN) + + def _turn_with_step( + self, fighter: FighterState, opponent: FighterState, from_x: int, from_y: int + ) -> None: + """His own footwork turns him with it: stepping round the other man keeps him in front. + + Only the other man's footwork has to be followed at the turn rate, so circling him fast + enough takes an angle on him. + """ + if self._facing_held(fighter): return - desired_x = _symmetric_divide(dx * FACING_SCALE, distance) - desired_y = _symmetric_divide(dy * FACING_SCALE, distance) - if fighter.facing_x * desired_x + fighter.facing_y * desired_y < -( - FACING_SCALE * FACING_SCALE * 9 // 10 - ): - # Anti-parallel facings would blend through the origin; turn via the perpendicular. - fighter.facing_x, fighter.facing_y = ( - fighter.facing_x - _symmetric_divide(fighter.facing_y * FACING_TURN_PERCENT, 100), - fighter.facing_y + _symmetric_divide(fighter.facing_x * FACING_TURN_PERCENT, 100), - ) - fighter.facing_x += _symmetric_divide( - (desired_x - fighter.facing_x) * FACING_TURN_PERCENT, 100 - ) - fighter.facing_y += _symmetric_divide( - (desired_y - fighter.facing_y) * FACING_TURN_PERCENT, 100 + before = _bearing(from_x, from_y, opponent) + after = _bearing(fighter.x, fighter.y, opponent) + if before is None or after is None: + return + step_sin = abs(before[0] * after[1] - before[1] * after[0]) + if step_sin: + step_cos = before[0] * after[0] + before[1] * after[1] + _turn_toward(fighter, after, step_cos, step_sin) + + @staticmethod + def _facing_held(fighter: FighterState) -> bool: + """The facing holds still through a punch's startup and active ticks, so a slip or weave + is judged against the line it was thrown along, and while a clinch is tried for. Between + the punches of a combination it follows the other man again.""" + attack = fighter.attack + return fighter.clinch_startup_ticks > 0 or ( + attack is not None and attack.age < attack.rule.startup + attack.rule.active ) - length = isqrt(fighter.facing_x**2 + fighter.facing_y**2) - if length == 0: - fighter.facing_x, fighter.facing_y = desired_x, desired_y - else: - fighter.facing_x = _symmetric_divide(fighter.facing_x * FACING_SCALE, length) - fighter.facing_y = _symmetric_divide(fighter.facing_y * FACING_SCALE, length) - fighter.facing = 1 if fighter.facing_x >= 0 else -1 @staticmethod def _facing_components(fighter: FighterState, dx: int, dy: int) -> tuple[int, int]: @@ -1308,12 +1732,16 @@ def _recover_resources(self, fighter: FighterState) -> None: base = 1 if active else max(1, fighter.fatigue // 25) regen = max(1, base * 3 // 4) if fighter.movement_load else base if fighter.defense in (DefensivePose.GUARD_HIGH, DefensivePose.GUARD_LOW): - regen //= 2 + regen = regen * GUARD_STAMINA_REGEN_PERCENT // 100 fighter.stamina = min(fighter.maximum_stamina, fighter.stamina + regen) + guard_regen = max(1, fighter.fatigue // 30) if not active and fighter.defense is DefensivePose.NONE and self.tick % 2 == 0: - guard_regen = max(1, fighter.fatigue // 30) fighter.guard = min(MAX_GUARD, fighter.guard + guard_regen) - fighter.poise = min(MAX_POISE, fighter.poise + (1 if not active else 0)) + elif not active and self.tick % GUARD_HELD_REGEN_EVERY_TICKS == 0: + # A held guard still comes back, slowly, between the punches it takes. + fighter.guard = min(MAX_GUARD, fighter.guard + guard_regen) + recovering = not active and self.tick % POISE_REGEN_EVERY_TICKS == 0 + fighter.poise = min(poise_ceiling(fighter.trauma.head), fighter.poise + int(recovering)) @staticmethod def _ring_control(fighter: FighterState, opponent: FighterState) -> int: @@ -1332,6 +1760,7 @@ def _advance_bleeding(self, fighter: FighterState) -> None: return fighter.trauma.head = min(1400, fighter.trauma.head + fighter.trauma.bleeding // 100) fighter.conditioning = max(0, fighter.conditioning - fighter.trauma.bleeding // 250) + fighter.stamina = min(fighter.stamina, fighter.maximum_stamina) self._emit("bleed", fighter.player_id, amount=fighter.trauma.bleeding // 10, blood=25) opponent = self._other(fighter.player_id) if self._needs_doctor_stoppage(fighter): @@ -1351,7 +1780,7 @@ def _qualifies_for_flash( action: PunchAction, rule: PunchRule, counter: bool, - guarding: bool, + blocked: bool, ) -> bool: return ( self.config.flash_ko_enabled @@ -1360,7 +1789,7 @@ def _qualifies_for_flash( and action.punch_class is not PunchClass.JAB and rule.impact >= 50 and counter - and not guarding + and not blocked and attacker.stamina >= 100 and (defender.trauma.head >= 180 or defender.conditioning <= 760) ) @@ -1375,7 +1804,17 @@ def _flash_chance(self, attacker: FighterState, defender: FighterState, damage: + max(0, attacker.stamina - 500) // 25, ) - def _knock_down(self, defender: FighterState, attacker: FighterState) -> None: + def _knock_down( + self, + defender: FighterState, + attacker: FighterState, + *, + detail: str = "", + action_id: str | None = None, + ) -> None: + for fighter in (defender, attacker): + fighter.body_collapse_ticks = 0 + fighter.body_collapse_action_id = None defender.knockdowns += 1 attacker.performance.knockdowns += 1 defender.poise = 0 @@ -1391,16 +1830,37 @@ def _knock_down(self, defender: FighterState, attacker: FighterState) -> None: defender.get_up_meter = 0 self._downed_id = defender.player_id self._knockdown_count_ticks = 0 + self._box_tick = None + self._count_at_rise = 0 + self._neutral_corner = self._choose_neutral_corner(defender) self._paused_fight_ticks = self.phase_ticks_remaining self.phase = MatchPhase.KNOCKDOWN self.phase_ticks_remaining = 10 * COUNT_TICK_INTERVAL self._schedule_get_up_prompt(defender) - self._emit("knockdown", attacker.player_id, defender.player_id, amount=defender.knockdowns) + self._emit( + "knockdown", + attacker.player_id, + defender.player_id, + amount=defender.knockdowns, + detail=detail, + action_id=action_id, + ) if defender.knockdowns >= 3: self._complete(attacker.player_id, FinishMethod.TKO) + def _get_up_count(self) -> int: + """The referee's count: on past the rise to the mandatory eight, then held there.""" + count = self._knockdown_count_ticks // COUNT_TICK_INTERVAL + if self._box_tick is None: + return count + return max(self._count_at_rise, min(count, MANDATORY_COUNT)) + def _get_up_required(self, fighter: FighterState) -> int: - return 34 + fighter.knockdowns * 16 + fighter.trauma.head // 28 + return ( + GET_UP_BASE + + fighter.knockdowns * GET_UP_PER_KNOCKDOWN + + fighter.trauma.head // GET_UP_TRAUMA_DIVISOR + ) def _schedule_get_up_prompt(self, fighter: FighterState) -> None: fighter.get_up_prompt = ( @@ -1442,9 +1902,23 @@ def _advance_knockdown(self) -> None: downed = self._fighters[self._downed_id] winner = self._other(self._downed_id) winner.pending_actions.clear() - self.phase_ticks_remaining -= 1 self._knockdown_count_ticks += 1 self._walk_to_neutral_corner(winner, downed) + count = self._knockdown_count_ticks // COUNT_TICK_INTERVAL + new_second = self._knockdown_count_ticks % COUNT_TICK_INTERVAL == 0 + if self._box_tick is not None: + # Up, and taking the mandatory count: the referee counts on to eight, looks him over + # and only then tells them to box. On his feet, he turns to face the other man. + downed.pending_actions.clear() + self._update_facing(downed, winner) + if self.tick >= self._box_tick: + self._box(downed, winner) + return + self.phase_ticks_remaining = self._box_tick - self.tick + if new_second and self._count_at_rise < count <= MANDATORY_COUNT: + self._emit("count", target_id=downed.player_id, amount=count) + return + self.phase_ticks_remaining -= 1 while downed.pending_actions: action = downed.pending_actions.pop(0) if isinstance(action, MovementAction): @@ -1452,38 +1926,87 @@ def _advance_knockdown(self) -> None: if self.tick > downed.get_up_window_end_tick: self._schedule_get_up_prompt(downed) required = self._get_up_required(downed) - count = self._knockdown_count_ticks // COUNT_TICK_INTERVAL - if downed.get_up_meter >= required and count >= 1: - downed.poise = MAX_POISE // 2 - downed.stamina = min(downed.maximum_stamina, 350) - downed.stunned_ticks = 20 - self._separate_fighters(downed, winner) - self.phase = MatchPhase.FIGHT - self.phase_ticks_remaining = max(1, self._paused_fight_ticks) - self._downed_id = None + if self.phase_ticks_remaining <= 0: + # Ten: the count is over, however full the meter got on its last tick. + self._complete(winner.player_id, FinishMethod.KO) + elif downed.get_up_meter >= required and count >= 1: + downed.poise = min(poise_ceiling(downed.trauma.head), MAX_POISE // 2) + downed.stamina = max(downed.stamina, min(downed.maximum_stamina, GET_UP_STAMINA)) downed.get_up_prompt = None + eight = self.tick + MANDATORY_COUNT * COUNT_TICK_INTERVAL - self._knockdown_count_ticks + self._box_tick = max(eight, self.tick) + BOX_PAUSE_TICKS + self._count_at_rise = count + self.phase_ticks_remaining = self._box_tick - self.tick self._emit("get_up", downed.player_id, amount=count) - elif self.phase_ticks_remaining <= 0: - self._complete(winner.player_id, FinishMethod.KO) - elif self._knockdown_count_ticks % COUNT_TICK_INTERVAL == 0: + elif new_second: self._emit("count", target_id=downed.player_id, amount=count) + def _box(self, risen: FighterState, standing: FighterState) -> None: + """The referee waves them on after the count: the bout resumes where the clock stopped.""" + risen.stunned_ticks = GET_UP_STUN_TICKS + risen.stun_chain_ticks = 0 + self._separate_fighters(risen, standing) + self.phase = MatchPhase.FIGHT + self.phase_ticks_remaining = max(1, self._paused_fight_ticks) + self._downed_id = None + # The count stops where the referee left it rather than running on past ten. + self._knockdown_count_ticks = self._get_up_count() * COUNT_TICK_INTERVAL + self._box_tick = None + self._count_at_rise = 0 + self._neutral_corner = None + self._emit("box", target_id=risen.player_id) + + @staticmethod + def _choose_neutral_corner(downed: FighterState) -> tuple[int, int]: + """The neutral corner the referee sends the standing fighter to: of the two corners that are + neither fighter's rest corner, the one farther from the man on the canvas.""" + return max( + ((-REST_CORNER_OFFSET, REST_CORNER_OFFSET), (REST_CORNER_OFFSET, -REST_CORNER_OFFSET)), + key=lambda corner: (corner[0] - downed.x) ** 2 + (corner[1] - downed.y) ** 2, + ) + + @staticmethod + def _passes_clear(walker: FighterState, target: tuple[int, int], downed: FighterState) -> bool: + """Whether the straight walk from `walker` to `target` keeps clear of the downed fighter.""" + path_x, path_y = target[0] - walker.x, target[1] - walker.y + length_squared = path_x * path_x + path_y * path_y + along = (downed.x - walker.x) * path_x + (downed.y - walker.y) * path_y + if length_squared == 0 or along <= 0: + return True + along = min(along, length_squared) + nearest_x = walker.x + _symmetric_divide(path_x * along, length_squared) + nearest_y = walker.y + _symmetric_divide(path_y * along, length_squared) + gap = (nearest_x - downed.x) ** 2 + (nearest_y - downed.y) ** 2 + return gap >= MINIMUM_SEPARATION * MINIMUM_SEPARATION + def _walk_to_neutral_corner(self, winner: FighterState, downed: FighterState) -> None: - dx = winner.x - downed.x - dy = winner.y - downed.y + if self._neutral_corner is None: + self._neutral_corner = self._choose_neutral_corner(downed) + target_x, target_y = self._neutral_corner + dx = target_x - winner.x + dy = target_y - winner.y distance = isqrt(dx * dx + dy * dy) - if distance >= KNOCKDOWN_NEUTRAL_SEPARATION: + if distance <= REFEREE_WALK_SPEED: winner.velocity_x = winner.velocity_y = 0 winner.velocity_fixed_x = winner.velocity_fixed_y = 0 winner.position_remainder_x = winner.position_remainder_y = 0 self._update_facing(winner, downed) return - if distance == 0: - step_x, step_y = winner.facing_x, winner.facing_y - else: + if self._passes_clear(winner, self._neutral_corner, downed): step_x = _symmetric_divide(dx * FACING_SCALE, distance) step_y = _symmetric_divide(dy * FACING_SCALE, distance) - step_x, step_y = self._neutral_walk_step(winner, downed, step_x, step_y) + else: + # He is in the way: walk round him on the corner's side, easing out if too close. + away_x, away_y = winner.x - downed.x, winner.y - downed.y + gap = max(1, isqrt(away_x * away_x + away_y * away_y)) + round_x, round_y = -away_y, away_x + if round_x * dx + round_y * dy < 0: + round_x, round_y = away_y, -away_x + if gap < MINIMUM_SEPARATION: + round_x, round_y = round_x + away_x, round_y + away_y + length = max(1, isqrt(round_x * round_x + round_y * round_y)) + step_x = _symmetric_divide(round_x * FACING_SCALE, length) + step_y = _symmetric_divide(round_y * FACING_SCALE, length) winner.velocity_fixed_x = step_x * REFEREE_WALK_SPEED winner.velocity_fixed_y = step_y * REFEREE_WALK_SPEED winner.velocity_x = _rounded_fixed_velocity(winner.velocity_fixed_x) @@ -1494,79 +2017,30 @@ def _walk_to_neutral_corner(self, winner: FighterState, downed: FighterState) -> delta_y, winner.position_remainder_y = _consume_fixed_position( winner.velocity_fixed_y, winner.position_remainder_y ) + from_x, from_y = winner.x, winner.y winner.x += delta_x winner.y += delta_y self._clamp_to_ring(winner) + self._turn_with_step(winner, downed, from_x, from_y) self._update_facing(winner, downed) - @staticmethod - def _neutral_walk_step( - winner: FighterState, downed: FighterState, step_x: int, step_y: int - ) -> tuple[int, int]: - """Direction for the walk to neutral distance. - - Straight away from the downed fighter while the spot that leads to is inside the ropes. - A winner with his back to the ropes or a corner walks to the nearest open spot at that - distance instead, by a line that does not take him over the man on the canvas. - """ - radius = KNOCKDOWN_NEUTRAL_SEPARATION + NEUTRAL_WALK_MARGIN - - def spot(direction_x: int, direction_y: int) -> tuple[int, int]: - return ( - downed.x + _symmetric_divide(direction_x * radius, FACING_SCALE), - downed.y + _symmetric_divide(direction_y * radius, FACING_SCALE), - ) - - def is_open(point: tuple[int, int]) -> bool: - x, y, _, _, _ = _ring_point(point[0], point[1]) - return (x, y) == point - - def clears_downed(point: tuple[int, int]) -> bool: - path_x, path_y = point[0] - winner.x, point[1] - winner.y - length_squared = path_x * path_x + path_y * path_y - if length_squared == 0: - return True - along = (downed.x - winner.x) * path_x + (downed.y - winner.y) * path_y - along = min(max(along, 0), length_squared) - nearest_x = winner.x + _symmetric_divide(path_x * along, length_squared) - nearest_y = winner.y + _symmetric_divide(path_y * along, length_squared) - gap = (nearest_x - downed.x) ** 2 + (nearest_y - downed.y) ** 2 - return gap >= (MINIMUM_SEPARATION - NEUTRAL_WALK_MARGIN) ** 2 - - if is_open(spot(step_x, step_y)): - return step_x, step_y - chosen: tuple[int, int] | None = None - nearest = 0 - for clear_path in (True, False): - for direction_x, direction_y in _COMPASS: - point = spot(direction_x, direction_y) - if not is_open(point) or (clear_path and not clears_downed(point)): - continue - gap = (point[0] - winner.x) ** 2 + (point[1] - winner.y) ** 2 - if chosen is None or gap < nearest: - chosen, nearest = point, gap - if chosen is not None: - break - if chosen is None: - return step_x, step_y - path_x, path_y = chosen[0] - winner.x, chosen[1] - winner.y - length = isqrt(path_x * path_x + path_y * path_y) - if length == 0: - return 0, 0 - return ( - _symmetric_divide(path_x * FACING_SCALE, length), - _symmetric_divide(path_y * FACING_SCALE, length), - ) - def _finish_round(self) -> None: one = self._fighters[self._player_ids[0]] two = self._fighters[self._player_ids[1]] + for fighter in (one, two): + # Saved by the bell: a body shot that has not yet put him down never will. + fighter.body_collapse_ticks = 0 + fighter.body_collapse_action_id = None for profile in JUDGE_PROFILES: scores = score_round(one.performance, two.performance, profile) one_card, two_card = self._round_cards[profile.name] one_card.append(scores.player_one) two_card.append(scores.player_two) self._emit("bell", detail="round_end") + if one.clinch_ticks or two.clinch_ticks: + # The bell ends a clinch too, and the referee parts them as he would one that ran its + # time: cleared without the break, they stood overlapped into the rest. + self._break_clinch(one, two) if self.round_number >= self.config.rounds: self._finish_decision() return @@ -1585,25 +2059,70 @@ def _finish_round(self) -> None: fighter.clinch_startup_ticks = 0 fighter.clinch_ticks = 0 fighter.stunned_ticks = 0 + fighter.stun_chain_ticks = 0 + fighter.stun_immune_until_tick = -1 + fighter.rocked_immune_until_tick = -1 fighter.taunt_ticks = 0 fighter.defense = DefensivePose.NONE + fighter.corner_choice = None def _advance_rest(self) -> None: self.phase_ticks_remaining -= 1 one = self._fighters[self._player_ids[0]] two = self._fighters[self._player_ids[1]] + for fighter in (one, two): + self._take_corner_instruction(fighter) self._walk_to_corner(one, -REST_CORNER_OFFSET, -REST_CORNER_OFFSET, two) self._walk_to_corner(two, REST_CORNER_OFFSET, REST_CORNER_OFFSET, one) self._separate_fighters(one, two) for fighter in self._fighters.values(): fighter.stamina = min(fighter.maximum_stamina, fighter.stamina + 3) fighter.guard = min(MAX_GUARD, fighter.guard + 2) - fighter.poise = min(MAX_POISE, fighter.poise + 2) + fighter.poise = min(poise_ceiling(fighter.trauma.head), fighter.poise + 2) if self.tick % TICKS_PER_SECOND == 0: fighter.trauma.bleeding = max(0, fighter.trauma.bleeding - 2) if self.phase_ticks_remaining <= 0: + for fighter in (one, two): + if fighter.corner_choice is None: + self._treat(fighter, CornerChoice.BALANCED) self._start_next_round() + def _take_corner_instruction(self, fighter: FighterState) -> None: + """The corner takes one instruction per rest; anything else pressed between rounds is dropped.""" + if not fighter.pending_actions: + return + actions = tuple(fighter.pending_actions) + fighter.pending_actions.clear() + fighter.pending_action_expires_tick = 0 + if fighter.corner_choice is not None: + return + for action in actions: + choice = CORNER_INSTRUCTIONS.get(action.kind) + if choice is not None: + self._treat(fighter, choice) + return + + def _treat(self, fighter: FighterState, choice: CornerChoice) -> None: + treatment = CORNER_TREATMENTS[choice] + trauma = fighter.trauma + if trauma.left_cut >= trauma.right_cut: + trauma.left_cut = max(0, trauma.left_cut - treatment.worse_cut) + trauma.right_cut = max(0, trauma.right_cut - treatment.other_cut) + else: + trauma.right_cut = max(0, trauma.right_cut - treatment.worse_cut) + trauma.left_cut = max(0, trauma.left_cut - treatment.other_cut) + trauma.bleeding = trauma.bleeding * treatment.bleeding_kept_percent // 100 + trauma.swelling = max(0, trauma.swelling - treatment.swelling) + trauma.left_eye = max(0, trauma.left_eye - treatment.eyes) + trauma.right_eye = max(0, trauma.right_eye - treatment.eyes) + fighter.conditioning = min(MAX_CONDITIONING, fighter.conditioning + treatment.conditioning) + trauma.body = max(0, trauma.body - treatment.body) + if treatment.refresh: + fighter.stamina = fighter.maximum_stamina + fighter.poise = poise_ceiling(fighter.trauma.head) + fighter.corner_choice = choice + self._emit("corner", fighter.player_id, detail=choice.value) + def _walk_to_corner( self, fighter: FighterState, corner_x: int, corner_y: int, opponent: FighterState ) -> None: @@ -1643,6 +2162,9 @@ def _start_next_round(self) -> None: fighter.pending_actions.clear() fighter.clinch_startup_ticks = 0 fighter.stunned_ticks = 0 + fighter.stun_chain_ticks = 0 + fighter.stun_immune_until_tick = -1 + fighter.rocked_immune_until_tick = -1 fighter.taunt_ticks = 0 fighter.evasion_ticks = 0 fighter.counter_ticks = 0 @@ -1680,6 +2202,9 @@ def _complete(self, winner_id: str | None, method: FinishMethod) -> None: return self.phase = MatchPhase.COMPLETE self.phase_ticks_remaining = 0 + for fighter in self._fighters.values(): + fighter.poise = max(0, fighter.poise) + fighter.stamina = min(fighter.stamina, fighter.maximum_stamina) self.result = self._build_result(winner_id, method) self._emit("result", winner_id, detail=method.value) @@ -1756,7 +2281,7 @@ def snapshot(self) -> EngineSnapshot: ) assert len(fighters) == 2 typed_fighters = (fighters[0], fighters[1]) - checksum = self._checksum(typed_fighters) + checksum = self._checksum(typed_fighters) if self.checksums else "" return EngineSnapshot( tick=self.tick, phase=self.phase, @@ -1819,6 +2344,7 @@ def _fighter_snapshot(self, fighter: FighterState) -> FighterSnapshot: velocity_x=fighter.velocity_x, velocity_y=fighter.velocity_y, stance=fighter.stance, + style=fighter.style, defense=fighter.defense, stamina=fighter.stamina, maximum_stamina=fighter.maximum_stamina, @@ -1839,7 +2365,7 @@ def _fighter_snapshot(self, fighter: FighterState) -> FighterSnapshot: warnings=fighter.warnings, deductions=fighter.deductions, stunned_ticks=fighter.stunned_ticks, - is_downed=fighter.player_id == self._downed_id, + is_downed=fighter.player_id == self._downed_id and self._box_tick is None, action=action_class, action_hand=action_hand, action_target=action_target, @@ -1856,10 +2382,11 @@ def _fighter_snapshot(self, fighter: FighterState) -> FighterSnapshot: clinch_ticks=fighter.clinch_ticks, is_foul_recovery_target=fighter.player_id == self._foul_recovery_target, taunt_ticks=fighter.taunt_ticks, + corner_choice=fighter.corner_choice, get_up_prompt=fighter.get_up_prompt, get_up_meter=fighter.get_up_meter, get_up_required=self._get_up_required(fighter), - get_up_count=self._knockdown_count_ticks // COUNT_TICK_INTERVAL, + get_up_count=self._get_up_count(), get_up_window_start_tick=fighter.get_up_window_start_tick, get_up_window_end_tick=fighter.get_up_window_end_tick, last_input_sequence=fighter.last_sequence, @@ -1882,25 +2409,32 @@ def _checksum(self, _fighters: tuple[FighterSnapshot, FighterSnapshot]) -> str: fighter.position_remainder_y, ], "stance": fighter.stance, + "style": fighter.style, "resources": [ fighter.stamina, fighter.conditioning, + fighter.conditioning_remainder, fighter.guard, fighter.poise, ], "trauma": fighter.trauma, "defense": fighter.defense, "defense_started_tick": fighter.defense_started_tick, + "guard_timing": [fighter.guard_held_tick, fighter.guard_raised_tick], "timers": [ fighter.evasion_ticks, fighter.stunned_ticks, fighter.stunned_at_tick, + fighter.staggered_until_tick, fighter.counter_ticks, fighter.clinch_startup_ticks, fighter.clinch_ticks, fighter.combo_ticks, fighter.taunt_ticks, fighter.last_action_until_tick, + fighter.stun_chain_ticks, + fighter.stun_immune_until_tick, + fighter.rocked_immune_until_tick, ], "attack": fighter.attack, "last_punch": fighter.last_punch, @@ -1932,6 +2466,12 @@ def _checksum(self, _fighters: tuple[FighterSnapshot, FighterSnapshot]) -> str: "performance": fighter.performance, "damage_dealt": fighter.damage_dealt, "movement_load": fighter.movement_load, + "corner": fighter.corner_choice, + "body_collapse": [ + fighter.body_collapse_ticks, + fighter.body_collapse_action_id, + fighter.body_collapse_at_tick, + ], } ) state = _canonical( @@ -1951,6 +2491,9 @@ def _checksum(self, _fighters: tuple[FighterSnapshot, FighterSnapshot]) -> str: "round_cards": self._round_cards, "downed_id": self._downed_id, "knockdown_count_ticks": self._knockdown_count_ticks, + "neutral_corner": self._neutral_corner, + "box_tick": self._box_tick, + "count_at_rise": self._count_at_rise, "foul_recovery_target": self._foul_recovery_target, "paused_fight_ticks": self._paused_fight_ticks, "event_id": self._event_id, diff --git a/src/intelstream/hands/protocol.py b/src/intelstream/hands/protocol.py index b53c46b6..605fd4c5 100644 --- a/src/intelstream/hands/protocol.py +++ b/src/intelstream/hands/protocol.py @@ -2,13 +2,15 @@ import hashlib import json -from dataclasses import asdict, replace +from dataclasses import asdict, dataclass, replace from typing import Any, Never +from intelstream.hands.cpu import CpuLevel from intelstream.hands.types import ( ActionKind, DefensivePose, EngineSnapshot, + FighterStyle, Foul, FoulAction, Hand, @@ -21,7 +23,7 @@ Target, ) -PROTOCOL_VERSION = 3 +PROTOCOL_VERSION = 4 MAX_FRAME_BYTES = 4096 MAX_ACTIONS_PER_INPUT = 4 MAX_SERVER_FRAME_BYTES = 65_536 @@ -112,12 +114,11 @@ def _parse_action(raw: object) -> SemanticAction: return MovementAction(kind=kind, client_action_id=_client_action_id(action)) -def parse_client_input( - frame: str | bytes, - *, - last_sequence: int = -1, - server_tick: int = 0, -) -> InputCommand: +type ClientEnvelope = dict[str, object] + + +def decode_client_frame(frame: str | bytes) -> ClientEnvelope: + """The JSON object of one client frame. A frame is decoded once and each message type is read from it.""" encoded = frame.encode() if isinstance(frame, str) else frame if len(encoded) > MAX_FRAME_BYTES: raise ProtocolError("input frame is too large") @@ -129,8 +130,20 @@ def parse_client_input( ) except (json.JSONDecodeError, UnicodeDecodeError) as exc: raise ProtocolError("input frame is not valid JSON") from exc + return _object(raw, "envelope") + + +def _client_envelope(frame: str | bytes | ClientEnvelope) -> ClientEnvelope: + return frame if isinstance(frame, dict) else decode_client_frame(frame) + - envelope = _object(raw, "envelope") +def parse_client_input( + frame: str | bytes | ClientEnvelope, + *, + last_sequence: int = -1, + server_tick: int = 0, +) -> InputCommand: + envelope = _client_envelope(frame) _exact_fields( envelope, {"version", "type", "sequence", "client_tick", "move", "defense", "actions"}, @@ -180,19 +193,8 @@ def parse_client_input( ) -def parse_ticket_ack(frame: str | bytes) -> str | None: - encoded = frame.encode() if isinstance(frame, str) else frame - if len(encoded) > MAX_FRAME_BYTES: - raise ProtocolError("input frame is too large") - try: - raw = json.loads( - encoded, - parse_constant=_reject_constant, - object_pairs_hook=_unique_object, - ) - except (json.JSONDecodeError, UnicodeDecodeError) as exc: - raise ProtocolError("input frame is not valid JSON") from exc - envelope = _object(raw, "envelope") +def parse_ticket_ack(frame: str | bytes | ClientEnvelope) -> str | None: + envelope = _client_envelope(frame) if envelope.get("type") != "ticket_ack": return None _exact_fields(envelope, {"version", "type", "refresh_id"}, "ticket acknowledgement") @@ -208,6 +210,38 @@ def parse_ticket_ack(frame: str | bytes) -> str | None: return refresh_id +def parse_cpu_request(frame: str | bytes | ClientEnvelope) -> CpuLevel | None: + """The level of computer opponent asked for, or None when the frame is not that request.""" + envelope = _client_envelope(frame) + if envelope.get("type") != "cpu": + return None + _exact_fields(envelope, {"version", "type", "level"}, "computer opponent request") + if envelope.get("version") != PROTOCOL_VERSION: + raise ProtocolError("unsupported protocol version") + return _enum(CpuLevel, envelope.get("level"), "computer level") + + +@dataclass(frozen=True, slots=True) +class StyleChoice: + style: FighterStyle + ready: bool + """True once the fighter has settled on the style; False while still choosing.""" + + +def parse_style_choice(frame: str | bytes | ClientEnvelope) -> StyleChoice | None: + """The style a fighter is choosing before the bout, or None when the frame is not that message.""" + envelope = _client_envelope(frame) + if envelope.get("type") != "style": + return None + _exact_fields(envelope, {"version", "type", "style", "ready"}, "style choice") + if envelope.get("version") != PROTOCOL_VERSION: + raise ProtocolError("unsupported protocol version") + ready = envelope.get("ready") + if not isinstance(ready, bool): + raise ProtocolError("ready must be true or false") + return StyleChoice(_enum(FighterStyle, envelope.get("style"), "fighter style"), ready) + + def _action_dict(action: SemanticAction) -> dict[str, object]: result: dict[str, object] if isinstance(action, PunchAction): diff --git a/src/intelstream/hands/rooms.py b/src/intelstream/hands/rooms.py index fa51cbb4..acc3c0c9 100644 --- a/src/intelstream/hands/rooms.py +++ b/src/intelstream/hands/rooms.py @@ -6,19 +6,23 @@ import secrets import time from collections import deque -from dataclasses import dataclass, field, replace +from dataclasses import asdict, dataclass, field, replace from typing import TYPE_CHECKING, Literal, Never, Protocol from uuid import uuid4 import structlog +from intelstream.hands.cpu import PROFILES, CpuBrain, CpuLevel, cpu_player_id, cpu_style from intelstream.hands.engine import BoxingEngine, EngineConfig from intelstream.hands.protocol import ( PROTOCOL_VERSION, + ClientEnvelope, ProtocolError, + StyleChoice, encode_snapshot, parse_client_input, ) +from intelstream.hands.types import FighterStyle if TYPE_CHECKING: from collections.abc import Awaitable, Callable, Coroutine @@ -91,6 +95,16 @@ class RoomConfig: # this many snapshots in a row were replaced before it could be sent one. outbound_queue_size: int = 16 max_spectators: int = 20 + # Every bout in progress steps a 30 Hz engine and encodes a snapshot per viewer on the bot's one + # event loop (about 1 ms a tick for a computer bout measured on a dev machine, so ~30 ms of + # every second), and one person alone can start a computer bout. Past these, a new room or a + # computer is refused as service_busy rather than slowing every bout down. + max_rooms: int = 64 + max_cpu_rooms: int = 16 + style_select_seconds: float = 10.0 + """How long the fighters have to pick a style once both corners are filled; 0 starts at once.""" + rematch_seat_seconds: float = 60.0 + """How long an instance's next room holds the seats of the people who fought its last bout.""" engine_config: EngineConfig = field(default_factory=EngineConfig) def __post_init__(self) -> None: @@ -115,6 +129,10 @@ def __post_init__(self) -> None: raise ValueError("room durations are invalid") if self.max_spectators < 0: raise ValueError("spectator bound must not be negative") + if self.max_rooms < 1 or self.max_cpu_rooms < 0: + raise ValueError("room bounds are invalid") + if self.style_select_seconds < 0 or self.rematch_seat_seconds < 0: + raise ValueError("style select and rematch seat times must not be negative") if self.max_input_frames_per_second < self.max_inputs_per_second: raise ValueError("input frame bound must not be below the accepted input bound") @@ -157,10 +175,13 @@ class PlayerSlot: rating: int connection: PlayerConnection | None grace_remaining: float + record: FighterRecord | None = None + style: FighterStyle = FighterStyle.BALANCED + """The style this fighter has picked, or would like: it is fixed when the bout starts.""" last_sequence: int = -1 input_budget: float | None = None input_budget_at: float = 0.0 - deferred_frame: str | bytes | None = None + deferred_frame: str | bytes | ClientEnvelope | None = None frame_times: deque[float] = field(default_factory=deque) flood_started: float | None = None reconnect_deadline: float | None = None @@ -168,10 +189,79 @@ class PlayerSlot: pre_match_grace_task: asyncio.Task[None] | None = None +@dataclass(frozen=True, slots=True) +class FighterRecord: + """A fighter's professional record in this server, as the ring announcer reads it.""" + + wins: int + losses: int + draws: int + knockouts: int + + def payload(self) -> dict[str, int]: + return { + "wins": self.wins, + "losses": self.losses, + "draws": self.draws, + "knockouts": self.knockouts, + } + + +# The computer's boxers come with a record of their own. +CPU_RECORDS: dict[CpuLevel, FighterRecord] = { + CpuLevel.ROOKIE: FighterRecord(wins=3, losses=4, draws=1, knockouts=1), + CpuLevel.CONTENDER: FighterRecord(wins=19, losses=5, draws=1, knockouts=12), + CpuLevel.CHAMPION: FighterRecord(wins=36, losses=1, draws=0, knockouts=29), +} + + +@dataclass(slots=True) +class CpuOpponent: + """The computer in the second seat. It is not a PlayerSlot: it never disconnects or forfeits.""" + + level: CpuLevel + brain: CpuBrain | None = None + style: FighterStyle = FighterStyle.BALANCED + + @property + def player_id(self) -> str: + return cpu_player_id(self.level) + + @property + def name(self) -> str: + return PROFILES[self.level].name + + @property + def rating(self) -> int: + return PROFILES[self.level].rating + + @property + def record(self) -> FighterRecord: + return CPU_RECORDS[self.level] + + +# Seeds the computer's own dice from the match seed, so a bout replays from its inputs. +CPU_SEED_SALT = 0x5DEECE66D + + +@dataclass(slots=True) +class StyleSelect: + """The fighters picking their styles: the bout starts when both are ready or at the deadline.""" + + deadline: float + seed: int + ready: set[str] = field(default_factory=set) + task: asyncio.Task[None] | None = None + + @dataclass(slots=True) class SpectatorSlot: identity: AuthenticatedPlayer connection: PlayerConnection + # What seating him needs, if a fighter's seat opens before the bell. + rating: int = 0 + record: FighterRecord | None = None + reconnect_ticket_factory: Callable[[], str] | None = None @dataclass(frozen=True, slots=True) @@ -179,10 +269,20 @@ class RoomMembership: room: HandsRoom player_id: str role: ConnectionRole + """The role the connection joined in. A spectator can be seated before the bell, so the room + itself decides what each frame may do.""" connection: PlayerConnection reconnect_ticket: str | None +@dataclass(frozen=True, slots=True) +class _HeldSeats: + """The people who fought an instance's last bout, whose seats its next room holds.""" + + fighters: frozenset[str] + until: float + + @dataclass(slots=True) class UserRoomReservation: room: HandsRoom @@ -204,6 +304,9 @@ def __init__( on_finished: Callable[[HandsRoom], Awaitable[None]], match_id_factory: Callable[[], str], seed_factory: Callable[[], int], + held_seats: frozenset[str] = frozenset(), + held_until: float = 0.0, + cpu_allowed: Callable[[], bool] = lambda: True, ) -> None: self.instance_id = instance_id self.guild_id = guild_id @@ -214,11 +317,14 @@ def __init__( self._on_finished = on_finished self._match_id_factory = match_id_factory self._seed_factory = seed_factory + self._cpu_allowed = cpu_allowed self._slots: dict[str, PlayerSlot] = {} self._spectators: dict[str, SpectatorSlot] = {} + self._cpu: CpuOpponent | None = None + self._select: StyleSelect | None = None self._engine: BoxingEngine | None = None self._tick_task: asyncio.Task[None] | None = None - self._persistence_task: asyncio.Task[HandsMatch] | None = None + self._persistence_task: asyncio.Task[HandsMatch | None] | None = None self._finish_task: asyncio.Task[None] | None = None self._cleanup_task: asyncio.Task[None] | None = None self._closed = False @@ -229,6 +335,16 @@ def __init__( self._lock = asyncio.Lock() self._admission_lock = asyncio.Lock() self._background_tasks: set[asyncio.Task[None]] = set() + # The last bout's fighters, whose seats this room holds for a rematch until held_until. + self._held_seats = held_seats + self._held_until = held_until + self._seated_ids: set[str] = set() + self._rematch_ids: tuple[str, ...] = () + self._held_seats_task: asyncio.Task[None] | None = None + if held_seats and held_until > monotonic_clock(): + self._held_seats_task = self._spawn( + self._release_held_seats(), name=f"hands-held-seats-{instance_id}" + ) @property def engine(self) -> BoxingEngine | None: @@ -254,9 +370,15 @@ def member_ids(self) -> tuple[str, ...]: def finished(self) -> bool: return self._finished + @property + def cpu(self) -> CpuOpponent | None: + return self._cpu + def retire(self) -> None: """The manager no longer routes joins here; a join already on its way must not land.""" self._retired = True + if self._held_seats_task is not None: + self._held_seats_task.cancel() async def add( self, @@ -264,6 +386,7 @@ async def add( socket: SocketLike, rating: int, *, + record: FighterRecord | None = None, reconnect_ticket: str | None = None, reconnect_ticket_factory: Callable[[], str] | None = None, ) -> RoomMembership: @@ -303,7 +426,7 @@ async def add( role = "fighter" elif existing_spectator is not None: role = "spectator" - elif self._engine is None and len(self._slots) < 2: + elif self._seat_open_for(identity.user_id): role = "fighter" else: role = "spectator" @@ -331,20 +454,27 @@ async def add( existing_fighter.pre_match_grace_event.set() fighter = existing_fighter else: + if self._cpu is not None: + # A person takes the computer's seat before the bell: the computer's pick + # is dropped and the two of them pick afresh below. + self._cancel_select() + self._cpu = None fighter = PlayerSlot( identity=identity, rating=rating, connection=connection, grace_remaining=self.config.reconnect_grace_seconds, + record=record, ) self._slots[identity.user_id] = fighter + self._seated_ids.add(identity.user_id) seat = tuple(self._slots).index(identity.user_id) + 1 welcome: dict[str, object] = { "role": role, "player_id": identity.user_id, "seat": seat, "rating": fighter.rating, - "players": self._public_players(), + "players": self._public_players(identity.user_id), "server_tick": self._engine.tick if self._engine is not None else 0, "next_sequence": fighter.last_sequence + 1, } @@ -352,8 +482,13 @@ async def add( if existing_spectator is not None: existing_spectator.connection = connection existing_spectator.identity = identity + existing_spectator.rating = rating + existing_spectator.record = record + existing_spectator.reconnect_ticket_factory = reconnect_ticket_factory else: - self._spectators[identity.user_id] = SpectatorSlot(identity, connection) + self._spectators[identity.user_id] = SpectatorSlot( + identity, connection, rating, record, reconnect_ticket_factory + ) welcome = { "role": role, "player_id": identity.user_id, @@ -364,8 +499,7 @@ async def add( if reconnect_ticket is not None: welcome["reconnect_ticket"] = reconnect_ticket self._enqueue(connection, self._message("welcome", **welcome), uncompressed=True) - if (existing is not None and self._engine is not None) or role == "spectator": - assert self._engine is not None + if self._engine is not None and (existing is not None or role == "spectator"): self._enqueue( connection, encode_snapshot( @@ -375,6 +509,12 @@ async def add( ) if self._final_payload is not None: self._enqueue(connection, self._final_payload) + elif not final_recovery and self._select is not None: + if existing_fighter is not None: + # Back during the pick: the other corner sees him connected again. + self._broadcast_select(bounded_update=True) + else: + self._enqueue(connection, self._select_message(None)) elif ( not final_recovery and role == "fighter" @@ -413,22 +553,14 @@ async def add( grace_ms=max(0, int(opponent.grace_remaining * 1000)), ), ) - elif not final_recovery and len(self._slots) == 1: - self._enqueue(connection, self._message("waiting", open_seats=1)) elif not final_recovery and self._engine is None: - self._start_match() - disconnected = [ - current for current in self._slots.values() if current.connection is None - ] - if disconnected: - opponent = disconnected[0] + if len(self._slots) == 2: + # An opponent still away shows as disconnected in the pick itself. + self._begin_select() + else: + # A fighter waiting for an opponent, or someone watching an empty corner. self._enqueue( - connection, - self._message( - "paused", - player_id=opponent.identity.user_id, - grace_ms=max(0, int(opponent.grace_remaining * 1000)), - ), + connection, self._message("waiting", open_seats=2 - len(self._slots)) ) return RoomMembership( self, @@ -462,6 +594,86 @@ async def refresh_ticket( connection.ticket_refresh_queued = True connection.outbox.put_nowait(_OutboundMessage(ticket_refresh=True)) + def _seat_open_for(self, player_id: str) -> bool: + """Whether this person can take a fighter's seat. + + Seats are taken before the bell. The computer holds its seat only once the bout is on: + until then a person who arrives takes it, since bouts against the computer are unrated. + A seat held for one of the last bout's fighters is his alone until the rematch window ends. + """ + if self._engine is not None: + return False + held = 0 + if self._clock() < self._held_until: + held = len(self._held_seats - self._seated_ids - {player_id}) + return len(self._slots) + held < 2 + + @property + def rematch_fighters(self) -> tuple[str, ...]: + """The people still in the ring when the bout reached its result.""" + return self._rematch_ids + + async def _release_held_seats(self) -> None: + """The rematch window is over: a held seat nobody came back for goes to whoever waits.""" + await self._sleep(max(0.0, self._held_until - self._clock())) + async with self._lock: + self._held_seats = frozenset() + if not self._closed and self._engine is None: + self._fill_open_seats() + + def _fill_open_seats(self, *, collapsed: bool = False) -> None: + """Seats whoever waits for an open seat, then tells the room where the pick stands.""" + seated = self._seat_waiting_spectators() + if self._engine is None and self._select is None and len(self._slots) == 2: + self._begin_select() + elif collapsed or seated: + self._enqueue_all(self._message("waiting", open_seats=2 - len(self._slots))) + + def _seat_waiting_spectators(self) -> bool: + """A fighter's seat that opens before the bell goes to whoever has watched longest. + + The spectator keeps his connection and is welcomed again as a fighter; whoever else is in + the room hears of it from the waiting or select that follows. Returns whether anyone was + seated. + """ + seated = False + while not self._closed: + player_id = next( + (waiting for waiting in self._spectators if self._seat_open_for(waiting)), None + ) + if player_id is None: + break + spectator = self._spectators.pop(player_id) + if self._cpu is not None: + self._cancel_select() + self._cpu = None + slot = PlayerSlot( + identity=spectator.identity, + rating=spectator.rating, + connection=spectator.connection, + grace_remaining=self.config.reconnect_grace_seconds, + record=spectator.record, + ) + self._slots[player_id] = slot + self._seated_ids.add(player_id) + welcome: dict[str, object] = { + "role": "fighter", + "player_id": player_id, + "seat": tuple(self._slots).index(player_id) + 1, + "rating": slot.rating, + "players": self._public_players(player_id), + "server_tick": 0, + "next_sequence": 0, + } + if spectator.reconnect_ticket_factory is not None: + welcome["reconnect_ticket"] = spectator.reconnect_ticket_factory() + self._enqueue( + spectator.connection, self._message("welcome", **welcome), uncompressed=True + ) + logger.info("Hands spectator seated", instance_id=self.instance_id) + seated = True + return seated + def _new_connection( self, player_id: str, role: ConnectionRole, socket: SocketLike ) -> PlayerConnection: @@ -507,7 +719,7 @@ async def _writer( except (ConnectionError, RuntimeError, asyncio.CancelledError): if not self._closed: self._spawn( - self.disconnect(player_id, role, connection), + self.disconnect(player_id, connection), name=f"hands-disconnect-{role}-{player_id}", ) @@ -525,17 +737,46 @@ def _message(message_type: str, **payload: object) -> str: sort_keys=True, ) - def _public_players(self) -> list[dict[str, object]]: - return [ + def _public_players(self, viewer_id: str | None = None) -> list[dict[str, object]]: + players: list[dict[str, object]] = [ { "id": slot.identity.user_id, "name": slot.identity.display_name, "avatar": slot.identity.avatar_hash, "rating": slot.rating, "connected": slot.connection is not None, + **self._public_style(slot.identity.user_id, slot.style, viewer_id), + **({} if slot.record is None else {"record": slot.record.payload()}), } for slot in self._slots.values() ] + if self._cpu is not None: + players.append( + { + "id": self._cpu.player_id, + "name": self._cpu.name, + "avatar": None, + "rating": self._cpu.rating, + "connected": True, + "cpu": True, + **self._public_style(self._cpu.player_id, self._cpu.style, viewer_id), + "record": self._cpu.record.payload(), + } + ) + return players + + def _public_style( + self, player_id: str, style: FighterStyle, viewer_id: str | None + ) -> dict[str, object]: + """Both styles are revealed together at the bell. + + During the pick a settled style is shown only to its own fighter: showing it to the other + corner would reward waiting to pick its counter. + """ + if self._engine is not None: + return {"style": style.value} + settled = self._select is not None and player_id in self._select.ready + return {"style": style.value} if settled and player_id == viewer_id else {} def _enqueue( self, @@ -626,9 +867,9 @@ async def _drop_slow_connection(self, connection: PlayerConnection) -> None: ): connection.slow_drop_started = False return - for player_id, role, current in self._connected_members(): + for player_id, _role, current in self._connected_members(): if current is connection: - await self.disconnect(player_id, role, connection) + await self.disconnect(player_id, connection) return finally: if connection.slow_drop_task is asyncio.current_task(): @@ -643,30 +884,198 @@ def _refresh_grace(slot: PlayerSlot, now: float) -> None: if slot.reconnect_deadline is not None: slot.grace_remaining = max(0.0, slot.reconnect_deadline - now) - def _start_match(self) -> None: + def _begin_select(self) -> None: + """Both corners are filled: the fighters pick their styles, then the bout starts.""" + seed = self._seed_factory() + ready: set[str] = set() + if self._cpu is not None: + self._cpu.style = cpu_style(self._cpu.level, seed) + ready.add(self._cpu.player_id) + if self.config.style_select_seconds <= 0: + self._start_match(seed) + return + select = StyleSelect( + deadline=self._clock() + self.config.style_select_seconds, seed=seed, ready=ready + ) + self._select = select + select.task = self._spawn( + self._close_select_at_deadline(select), name=f"hands-style-select-{self.instance_id}" + ) + self._broadcast_select() + + def _select_message(self, viewer_id: str | None) -> str: + select = self._select + assert select is not None + return self._message( + "select", + deadline_ms=max(0, int((select.deadline - self._clock()) * 1000)), + players=self._public_players(viewer_id), + ready=sorted(select.ready), + ) + + def _broadcast_select(self, *, bounded_update: bool = False) -> None: + """The pick as it stands, to everyone: each fighter's own settled style is his alone.""" + for player_id, role, connection in self._connected_members(): + self._enqueue( + connection, + self._select_message(player_id if role == "fighter" else None), + bounded_update=bounded_update, + ) + + def _seated_styles(self) -> dict[str, FighterStyle]: + styles = {player_id: slot.style for player_id, slot in self._slots.items()} + if self._cpu is not None: + styles[self._cpu.player_id] = self._cpu.style + return styles + + async def _close_select_at_deadline(self, select: StyleSelect) -> None: + await self._sleep(max(0.0, select.deadline - self._clock())) + async with self._lock: + if self._select is select and self._engine is None and not self._closed: + self._start_match(select.seed) + + def _cancel_select(self) -> None: + select = self._select + self._select = None + if ( + select is not None + and select.task is not None + and select.task is not asyncio.current_task() + ): + select.task.cancel() + + async def choose_style( + self, player_id: str, connection: PlayerConnection, choice: StyleChoice + ) -> None: + """A fighter's style during the pick; outside it, or once settled, it changes nothing.""" + async with self._lock: + spectator = self._spectators.get(player_id) + if spectator is not None: + if spectator.connection is not connection: + raise RoomError("connection_replaced") + raise RoomError("spectator_read_only") + slot = self._slots.get(player_id) + if slot is None or slot.connection is not connection: + raise RoomError("connection_replaced") + # Counted with the fighter's input frames, so a flood of picks is throttled and then + # refused like a flood of inputs. + now = self._clock() + while slot.frame_times and slot.frame_times[0] <= now - 1.0: + slot.frame_times.popleft() + slot.frame_times.append(now) + limit = self.config.max_input_frames_per_second + if len(slot.frame_times) > limit: + if slot.flood_started is None: + slot.flood_started = now + if ( + len(slot.frame_times) > FLOOD_HARD_LIMIT_FACTOR * limit + or now - slot.flood_started >= FLOOD_GRACE_SECONDS + ): + raise RoomError("rate_limited") + return + slot.flood_started = None + select = self._select + if select is None or self._closed or player_id in select.ready: + return + slot.style = choice.style + if not choice.ready: + # Still choosing: the other corner learns nothing until this fighter settles. + return + select.ready.add(player_id) + if set(self._seated_styles()) <= select.ready: + self._start_match(select.seed) + else: + self._broadcast_select(bounded_update=True) + + def _start_match(self, seed: int) -> None: + self._cancel_select() players = tuple(self._slots) + if self._cpu is not None: + players = (*players, self._cpu.player_id) assert len(players) == 2 now = self._clock() for slot in self._slots.values(): if slot.connection is None: self._refresh_grace(slot, now) slot.pre_match_grace_event.set() + styles = self._seated_styles() self._engine = BoxingEngine( match_id=self._match_id_factory(), activity_instance_id=self.instance_id, guild_id=self.guild_id, player_one_id=players[0], player_two_id=players[1], - seed=self._seed_factory(), + seed=seed, config=self.config.engine_config, + styles=(styles[players[0]], styles[players[1]]), + ) + if self._cpu is not None: + self._cpu.brain = CpuBrain( + self._cpu.player_id, + players[0], + self._cpu.level, + seed ^ CPU_SEED_SALT, + self._cpu.style, + ) + logger.info( + "Hands bout started", + instance_id=self.instance_id, + match_id=self._engine.match_id, + cpu_level=None if self._cpu is None else self._cpu.level.value, + player_one_style=styles[players[0]].value, + player_two_style=styles[players[1]].value, ) self._enqueue_all(self._message("ready", players=self._public_players())) + absent = [slot for slot in self._slots.values() if slot.connection is None] + if absent: + # A seat still empty at the bell: the bout opens paused until he is back or forfeits. + self._enqueue_all( + self._message( + "paused", + player_id=absent[0].identity.user_id, + grace_ms=max(0, int(absent[0].grace_remaining * 1000)), + ), + bounded_update=True, + ) self._tick_task = asyncio.create_task( self._run_match(), name=f"hands-match-{self._engine.match_id}" ) + async def request_cpu( + self, player_id: str, connection: PlayerConnection, level: CpuLevel + ) -> bool: + """Puts the computer in the empty seat and starts the bout. + + Only the one fighter waiting alone can ask. If a second fighter got there first the bout + is already starting, so the request is dropped rather than treated as a protocol error. + """ + async with self._lock: + spectator = self._spectators.get(player_id) + if spectator is not None: + if spectator.connection is not connection: + raise RoomError("connection_replaced") + raise RoomError("spectator_read_only") + slot = self._slots.get(player_id) + if slot is None or slot.connection is not connection: + raise RoomError("connection_replaced") + # A request is a frame like any other, so a stream of them is a flood like any other. + if not self._account_frame(slot, self._clock()): + return False + if ( + self._closed + or self._finished + or self._engine is not None + or self._select is not None + ): + return False + if not self._cpu_allowed(): + raise RoomError("service_busy") + self._cpu = CpuOpponent(level) + self._begin_select() + return True + async def submit_frame( - self, player_id: str, connection: PlayerConnection, frame: str | bytes + self, player_id: str, connection: PlayerConnection, frame: str | bytes | ClientEnvelope ) -> None: async with self._lock: spectator = self._spectators.get(player_id) @@ -674,40 +1083,34 @@ async def submit_frame( if spectator.connection is not connection: raise RoomError("connection_replaced") raise RoomError("spectator_read_only") - if ( + ended = ( self._closed or self._finished or self._persistence_task is not None or (self._engine is not None and self._engine.result is not None) - ): - # Inputs already in flight when the bout ends arrive after the result on any - # real connection; dropping them keeps the final on the player's screen. - return + ) slot = self._slots.get(player_id) if slot is None or slot.connection is not connection: + if ended: + return raise RoomError("connection_replaced") + now = self._clock() + if ended: + # Inputs already in flight when the bout ends arrive after the result on any + # real connection; dropping them keeps the final on the player's screen. Each one + # still draws on the connection's allowance, so a flood through the result hold + # is cut off like any other. + self._account_frame(slot, now) + return engine = self._engine if engine is None: raise RoomError("match_not_started") paused = any(current.connection is None for current in self._slots.values()) # A paused bout drops inputs; none is held back to fire once it resumes. held = None if paused else frame - now = self._clock() - while slot.frame_times and slot.frame_times[0] <= now - 1.0: - slot.frame_times.popleft() - slot.frame_times.append(now) - limit = self.config.max_input_frames_per_second - if len(slot.frame_times) > limit: - if slot.flood_started is None: - slot.flood_started = now - if ( - len(slot.frame_times) > FLOOD_HARD_LIMIT_FACTOR * limit - or now - slot.flood_started >= FLOOD_GRACE_SECONDS - ): - raise RoomError("rate_limited") + if not self._account_frame(slot, now): slot.deferred_frame = held return - slot.flood_started = None if not self._spend_input_budget(slot, now): # A stalled connection delivers its backlog in one burst. The frames past the # budget are not applied, but the newest is kept and goes in as soon as there is @@ -729,6 +1132,28 @@ async def submit_frame( if not engine.submit_input(player_id, command): raise RoomError("input_queue_full") + def _account_frame(self, slot: PlayerSlot, now: float) -> bool: + """Counts one frame from a fighter's connection. + + Past the per-second limit the frame is not acted on; a flood that lasts, or one far past + the limit, ends the connection. + """ + while slot.frame_times and slot.frame_times[0] <= now - 1.0: + slot.frame_times.popleft() + slot.frame_times.append(now) + limit = self.config.max_input_frames_per_second + if len(slot.frame_times) > limit: + if slot.flood_started is None: + slot.flood_started = now + if ( + len(slot.frame_times) > FLOOD_HARD_LIMIT_FACTOR * limit + or now - slot.flood_started >= FLOOD_GRACE_SECONDS + ): + raise RoomError("rate_limited") + return False + slot.flood_started = None + return True + @staticmethod def _forget_connection_input(slot: PlayerSlot) -> None: """Input pacing belongs to a connection: a new one starts with a full allowance.""" @@ -770,19 +1195,16 @@ def _apply_deferred_inputs(self, now: float) -> None: slot.last_sequence = command.sequence engine.submit_input(player_id, command) - async def disconnect( - self, player_id: str, role: ConnectionRole, connection: PlayerConnection - ) -> None: + async def disconnect(self, player_id: str, connection: PlayerConnection) -> None: async with self._lock: - if role == "spectator": + slot = self._slots.get(player_id) + if slot is None or slot.connection is not connection: + # Not (or no longer) a fighter on this connection: a spectator leaving, if any. spectator = self._spectators.get(player_id) if spectator is None or spectator.connection is not connection: return self._spectators.pop(player_id, None) else: - slot = self._slots.get(player_id) - if slot is None or slot.connection is not connection: - return # The pause and its deadline are settled before the socket is touched: closing # a stalled socket can wait or fail, and the bout must not depend on it. slot.connection = None @@ -803,6 +1225,9 @@ async def disconnect( name=f"hands-waiting-grace-{player_id}", ) slot.pre_match_grace_event.set() + if self._select is not None and not self._closed: + # The pick goes on; the other corner sees him disconnected in it. + self._broadcast_select(bounded_update=True) elif not self._finished: self._enqueue_all( self._message( @@ -856,6 +1281,12 @@ async def _expire_pre_match_fighter( if slot.grace_remaining <= 0: self._slots.pop(player_id, None) expired = True + collapsed = self._select is not None + if collapsed: + # The corner is empty again: the pick is off. + self._cancel_select() + self._cpu = None + self._fill_open_seats(collapsed=collapsed) else: delay = slot.grace_remaining if expired: @@ -905,7 +1336,12 @@ async def _run_match(self) -> None: ticks_due = self.config.max_catch_up_ticks next_tick = now - (ticks_due - 1) * interval self._apply_deferred_inputs(now) + brain = self._cpu.brain if self._cpu is not None else None for _ in range(ticks_due): + if brain is not None: + command = brain.decide(engine) + if command is not None: + engine.submit_input(brain.player_id, command) snapshot = engine.step() next_tick += interval if ( @@ -959,8 +1395,11 @@ def _ensure_finish_task(self, result: MatchResult | None) -> asyncio.Task[None] if result is None: return None if self._persistence_task is None: + self._rematch_ids = tuple( + player_id for player_id, slot in self._slots.items() if slot.connection is not None + ) self._persistence_task = asyncio.create_task( - self.repository.record_hands_match(result), + self._persist(result), name=f"hands-persist-{result.match_id}", ) if self._finish_task is None: @@ -975,6 +1414,20 @@ def _ensure_abandon_task(self) -> None: self._finish_abandoned(), name=f"hands-abandon-{self.instance_id}" ) + async def _persist(self, result: MatchResult) -> HandsMatch | None: + # A bout against the computer is unrated and leaves no record. + if self._cpu is not None: + return None + engine = self._engine + assert engine is not None + return await self.repository.record_hands_match( + result, + styles=( + engine.fighter(result.player_one_id).style, + engine.fighter(result.player_two_id).style, + ), + ) + async def _finish_abandoned(self) -> None: self._finished = True self._enqueue_all(self._message("error", code="match_abandoned")) @@ -1036,7 +1489,25 @@ async def _drain_outboxes(self) -> None: except TimeoutError: logger.warning("Hands final delivery timed out", instance_id=self.instance_id) - def _final_message(self, match: HandsMatch, result: MatchResult) -> str: + def _final_message(self, match: HandsMatch | None, result: MatchResult) -> str: + engine = self._engine + assert engine is not None + if match is None: + ratings = { + player_id: {"before": rating, "after": rating} + for player_id, rating in self._unrated_ratings().items() + } + else: + ratings = { + result.player_one_id: { + "before": match.player_one_rating_before, + "after": match.player_one_rating_after, + }, + result.player_two_id: { + "before": match.player_two_rating_before, + "after": match.player_two_rating_after, + }, + } return self._message( "final", match_id=result.match_id, @@ -1051,18 +1522,20 @@ def _final_message(self, match: HandsMatch, result: MatchResult) -> str: } for card in result.scorecards ], - ratings={ - result.player_one_id: { - "before": match.player_one_rating_before, - "after": match.player_one_rating_after, - }, - result.player_two_id: { - "before": match.player_two_rating_before, - "after": match.player_two_rating_after, - }, + ratings=ratings, + # The engine's own count over the whole bout: what a client saw can miss a reconnect + # or a late arrival, so the result card shows these. + punches={ + player_id: asdict(engine.fighter(player_id).punches) for player_id in engine.players }, ) + def _unrated_ratings(self) -> dict[str, int]: + ratings = {player_id: slot.rating for player_id, slot in self._slots.items()} + if self._cpu is not None: + ratings[self._cpu.player_id] = self._cpu.rating + return ratings + async def _stop_connection( self, connection: PlayerConnection, *, code: int, reason: bytes ) -> None: @@ -1175,6 +1648,7 @@ def __init__( self._seed_factory = seed_factory self._rooms: dict[str, HandsRoom] = {} self._user_rooms: dict[str, UserRoomReservation] = {} + self._held_seats: dict[str, _HeldSeats] = {} self._lock = asyncio.Lock() self._closed = False @@ -1218,7 +1692,19 @@ async def _join_once( if reservation is not None and reservation.room.instance_id != player.instance_id: raise RoomError("already_in_room") room = self._rooms.get(player.instance_id) + if room is None and len(self._rooms) >= self.config.max_rooms: + logger.warning( + "Hands room refused at the cap", + rooms=len(self._rooms), + cpu_rooms=self._cpu_room_count(), + ) + raise RoomError("service_busy") if room is None: + # A rematch: the last bout's fighters keep their seats for a while, so nobody who + # arrives first can take one; everyone else watches until then. + held = self._held_seats.pop(player.instance_id, None) + if held is not None and held.until <= self._clock(): + held = None room = HandsRoom( instance_id=player.instance_id, guild_id=player.guild_id, @@ -1229,6 +1715,9 @@ async def _join_once( on_finished=self._room_finished, match_id_factory=self._match_id_factory, seed_factory=self._seed_factory, + held_seats=frozenset() if held is None else held.fighters, + held_until=0.0 if held is None else held.until, + cpu_allowed=self._cpu_allowed, ) self._rooms[player.instance_id] = room if reservation is None: @@ -1257,6 +1746,12 @@ async def _join_once( player, socket, rating.rating, + record=FighterRecord( + wins=rating.wins, + losses=rating.losses, + draws=rating.draws, + knockouts=rating.knockouts, + ), reconnect_ticket=reconnect_ticket, reconnect_ticket_factory=reconnect_ticket_factory, ) @@ -1281,6 +1776,19 @@ async def _join_once( raise return membership + def _cpu_room_count(self) -> int: + return sum(room.cpu is not None for room in self._rooms.values()) + + def _cpu_allowed(self) -> bool: + if self._cpu_room_count() < self.config.max_cpu_rooms: + return True + logger.warning( + "Hands computer refused at the cap", + rooms=len(self._rooms), + cpu_rooms=self._cpu_room_count(), + ) + return False + def _raise_unavailable(self, player: AuthenticatedPlayer, room: HandsRoom) -> Never: if self._closed: raise RoomError("server_shutting_down") @@ -1296,7 +1804,7 @@ def _retire_room(self, room: HandsRoom) -> None: async def _room_finished(self, room: HandsRoom) -> None: async with self._lock: active_member_ids = set(room.member_ids) - if not room.finished and room.player_ids: + if not room.finished and active_member_ids: for player_id, reservation in list(self._user_rooms.items()): if reservation.room is room and player_id not in active_member_ids: self._user_rooms.pop(player_id, None) @@ -1304,17 +1812,26 @@ async def _room_finished(self, room: HandsRoom) -> None: for player_id, reservation in list(self._user_rooms.items()): if reservation.room is room: self._user_rooms.pop(player_id, None) + if room.finished and self._rooms.get(room.instance_id) is room: + self._hold_rematch_seats(room) self._retire_room(room) + def _hold_rematch_seats(self, room: HandsRoom) -> None: + now = self._clock() + for instance_id, held in list(self._held_seats.items()): + if held.until <= now: + self._held_seats.pop(instance_id, None) + fighters = frozenset(room.rematch_fighters) + if fighters and self.config.rematch_seat_seconds > 0: + self._held_seats[room.instance_id] = _HeldSeats( + fighters, now + self.config.rematch_seat_seconds + ) + async def leave(self, membership: RoomMembership) -> None: - await membership.room.disconnect( - membership.player_id, - membership.role, - membership.connection, - ) - if membership.role == "spectator" or ( - not membership.room.started and membership.player_id not in membership.room.player_ids - ): + await membership.room.disconnect(membership.player_id, membership.connection) + # A fighter keeps his place through the grace; a spectator, or a fighter whose place is + # already gone, leaves the room. + if membership.player_id not in membership.room.member_ids: async with self._lock: reservation = self._user_rooms.get(membership.player_id) if ( @@ -1335,6 +1852,7 @@ async def close(self) -> None: rooms = list(self._rooms.values()) self._rooms.clear() self._user_rooms.clear() + self._held_seats.clear() results = await asyncio.gather(*(room.close() for room in rooms), return_exceptions=True) first_error = next( (result for result in results if isinstance(result, BaseException)), None diff --git a/src/intelstream/hands/rules.py b/src/intelstream/hands/rules.py index 8f7b91d2..5cdefb97 100644 --- a/src/intelstream/hands/rules.py +++ b/src/intelstream/hands/rules.py @@ -1,9 +1,11 @@ import json -from dataclasses import dataclass, replace +import math +from collections.abc import Mapping +from dataclasses import dataclass, field, replace from importlib import resources from typing import Any -from intelstream.hands.types import Power, PunchClass, Target +from intelstream.hands.types import CornerChoice, FighterStyle, Power, PunchClass, Target def _load_manifest() -> dict[str, Any]: @@ -20,25 +22,91 @@ def _load_manifest() -> dict[str, Any]: MINIMUM_SEPARATION = FIGHTER_RADIUS * 2 ROUND_TICKS = 120 * TICKS_PER_SECOND REST_TICKS = 15 * TICKS_PER_SECOND -COUNTDOWN_TICKS = 3 * TICKS_PER_SECOND +COUNTDOWN_TICKS: int = _MANIFEST["countdown"]["opening_ticks"] DEFAULT_ROUNDS = 3 MAX_STAMINA = 1000 MAX_CONDITIONING = 1000 MAX_GUARD = 700 MAX_POISE = 600 FACING_SCALE: int = _MANIFEST["facing"]["scale"] -# Share of the remaining turn toward the opponent taken per facing update. A fight tick makes two -# updates (before the exchange and after footwork), about 58% per tick; knockdown and rest walks -# make one. -FACING_TURN_PERCENT: int = _MANIFEST["facing"]["turn_percent_per_update"] +# How fast a fighter turns to follow the other man's footwork. His own footwork turns him with it, +# so a man who circles keeps the other in front of him while the other has to turn to follow: fast +# enough, and close enough, circling takes an angle on him. +FACING_TURN_DEGREES_PER_SECOND: int = _MANIFEST["facing"]["turn_degrees_per_second"] +_FACING_TURN_RADIANS = math.radians(FACING_TURN_DEGREES_PER_SECOND / TICKS_PER_SECOND) +# The most a fighter turns in a tick, as the cosine and sine of the angle in millionths. +FACING_TURN_COS: int = round(math.cos(_FACING_TURN_RADIANS) * 1_000_000) +FACING_TURN_SIN: int = round(math.sin(_FACING_TURN_RADIANS) * 1_000_000) RECOVERY_CANCEL_PERCENT: int = _MANIFEST["combos"]["recovery_cancel_percent"] -KNOCKDOWN_NEUTRAL_SEPARATION: int = _MANIFEST["knockdown"]["neutral_separation"] REFEREE_WALK_SPEED: int = _MANIFEST["knockdown"]["referee_walk_speed"] +MANDATORY_COUNT: int = _MANIFEST["knockdown"]["mandatory_count"] +BOX_PAUSE_TICKS: int = _MANIFEST["knockdown"]["box_pause_ticks"] +TIRED_STARTUP_TICKS: int = _MANIFEST["tired"]["startup_ticks"] +TIRED_RECOVERY_TICKS: int = _MANIFEST["tired"]["recovery_ticks"] +TIRED_IMPACT_PERCENT: int = _MANIFEST["tired"]["impact_percent"] +FOUL_SEPARATION: int = _MANIFEST["foul"]["separation"] RING_CORNER_REACH: int = _MANIFEST["corners"]["reach"] REST_CORNER_OFFSET: int = _MANIFEST["rest"]["corner_offset"] CLINCH_HOLD_DISTANCE: int = _MANIFEST["clinch"]["hold_distance"] CLINCH_DRAW_SPEED: int = _MANIFEST["clinch"]["draw_speed"] REST_WALK_SPEED: int = _MANIFEST["rest"]["walk_speed"] +BLIND_SIDE_EYE_THRESHOLD: int = _MANIFEST["blind_side"]["eye_threshold"] +BLIND_SIDE_IMPACT_PERCENT: int = _MANIFEST["blind_side"]["impact_percent"] +# The ringside doctor stops a bout on a cut or a swelling this bad, unless a bout's config says +# otherwise. The client reads the same values to call a cut by how near it is to his limit. +DOCTOR_CUT_THRESHOLD: int = _MANIFEST["doctor"]["cut"] +DOCTOR_SWELLING_THRESHOLD: int = _MANIFEST["doctor"]["swelling"] +PARRY_STAGGER_TICKS: int = _MANIFEST["parry"]["stagger_ticks"] +BODY_WIND_PERCENT: int = _MANIFEST["body"]["wind_percent"] +BODY_COLLAPSE_TRAUMA: int = _MANIFEST["body"]["collapse_body_trauma"] +BODY_COLLAPSE_STAMINA: int = _MANIFEST["body"]["collapse_stamina"] +BODY_COLLAPSE_MINIMUM_DAMAGE: int = _MANIFEST["body"]["collapse_minimum_damage"] +BODY_COLLAPSE_DELAY_TICKS: int = _MANIFEST["body"]["collapse_delay_ticks"] +GET_UP_STUN_TICKS: int = _MANIFEST["knockdown"]["get_up_stun_ticks"] +_STUN = _MANIFEST["stun"] +FLINCH_MINIMUM_DAMAGE: int = _STUN["flinch_minimum_damage"] +FLINCH_BASE_TICKS: int = _STUN["flinch_base_ticks"] +FLINCH_DAMAGE_DIVISOR: int = _STUN["flinch_damage_divisor"] +ROCKED_COUNTER_DAMAGE: int = _STUN["rocked_counter_damage"] +ROCKED_POWER_DAMAGE: int = _STUN["rocked_power_damage"] +ROCKED_HURT_POISE: int = _STUN["rocked_hurt_poise"] +ROCKED_BASE_TICKS: int = _STUN["rocked_base_ticks"] +ROCKED_DAMAGE_DIVISOR: int = _STUN["rocked_damage_divisor"] +ROCKED_MAX_TICKS: int = _STUN["rocked_max_ticks"] +STUN_CHAIN_MAX_TICKS: int = _STUN["chain_max_ticks"] +STUN_IMMUNITY_TICKS: int = _STUN["immunity_ticks"] +# Even a rocking shot cannot stop a fighter again this soon after a stun wears off: long enough to +# raise a guard through a round trip of latency. +ROCKED_IMMUNITY_TICKS: int = _STUN["rocked_immunity_ticks"] +STUNNED_SPEED_PERCENT: int = _STUN["moving_speed_percent"] +BLOCK_POISE_PERCENT: int = _MANIFEST["guard"]["block_poise_percent"] +PERFECT_BLOCK_POISE_PERCENT: int = _MANIFEST["guard"]["perfect_block_poise_percent"] +GUARD_LEAK_BASE_PERCENT: int = _MANIFEST["guard"]["leak_base_percent"] +GUARD_LEAK_MINIMUM_PERCENT: int = _MANIFEST["guard"]["leak_minimum_percent"] +GUARD_BLOCK_MINIMUM: int = _MANIFEST["guard"]["block_minimum"] +# A raised guard only perfect-blocks once the guard has been let down for this many ticks: a guard +# flicked down and straight back up blocks, but keeps no parry window open. +PERFECT_BLOCK_REARM_TICKS: int = _MANIFEST["guard"]["perfect_rearm_ticks"] +GUARD_HELD_REGEN_EVERY_TICKS: int = _MANIFEST["guard"]["held_regen_every_ticks"] +GUARD_DAMAGE_PERCENT: int = _MANIFEST["guard"]["damage_percent"] +GUARD_STAMINA_REGEN_PERCENT: int = _MANIFEST["guard"]["stamina_regen_percent"] +BODY_COLLAPSE_COOLDOWN_TICKS: int = _MANIFEST["body"]["collapse_cooldown_ticks"] +GET_UP_STAMINA: int = _MANIFEST["knockdown"]["get_up_stamina"] +GET_UP_BASE: int = _MANIFEST["knockdown"]["get_up_base"] +GET_UP_PER_KNOCKDOWN: int = _MANIFEST["knockdown"]["get_up_per_knockdown"] +GET_UP_TRAUMA_DIVISOR: int = _MANIFEST["knockdown"]["get_up_trauma_divisor"] +HEAD_TRAUMA_PER_DAMAGE_PERCENT: int = _MANIFEST["trauma"]["head_per_damage_percent"] +BODY_TRAUMA_PER_DAMAGE_PERCENT: int = _MANIFEST["trauma"]["body_per_damage_percent"] +EYE_TRAUMA_PER_DAMAGE_PERCENT: int = _MANIFEST["trauma"]["eye_per_damage_percent"] +CUT_PER_DAMAGE_PERCENT: int = _MANIFEST["trauma"]["cut_per_damage_percent"] +# A jab is a light punch: it seldom splits the skin and swells the face less than a hook. +JAB_CUT_PERCENT: int = _MANIFEST["trauma"]["jab_cut_percent"] +JAB_SWELLING_PERCENT: int = _MANIFEST["trauma"]["jab_swelling_percent"] +SWELLING_PER_DAMAGE_PERCENT: int = _MANIFEST["trauma"]["swelling_per_damage_percent"] +POISE_CEILING_PER_HEAD_PERCENT: int = _MANIFEST["trauma"]["poise_ceiling_per_head_percent"] +POISE_CEILING_FLOOR: int = _MANIFEST["trauma"]["poise_ceiling_floor"] +POISE_REGEN_EVERY_TICKS: int = _MANIFEST["trauma"]["poise_regen_every_ticks"] +POISE_DAMAGE_PERCENT: int = _MANIFEST["trauma"]["poise_damage_percent"] def _manifest_check() -> None: @@ -59,6 +127,9 @@ def _manifest_check() -> None: # walks feed facing vectors into, and the client's snapshot checks all count a unit as 1000. if FACING_SCALE != 1000: raise RuntimeError("combat-manifest.json facing.scale must stay 1000") + # A turn of a right angle or more in one tick could carry the facing past the opponent. + if not 0 < FACING_TURN_DEGREES_PER_SECOND < 90 * TICKS_PER_SECOND: + raise RuntimeError("combat-manifest.json facing.turn_degrees_per_second is out of range") if REST_CORNER_OFFSET * 2 > RING_CORNER_REACH: raise RuntimeError("combat-manifest.json rest.corner_offset is past the corner pads") @@ -149,6 +220,75 @@ def _punch_variant(base: PunchRule, target: Target, power: Power) -> PunchRule: ) +@dataclass(frozen=True, slots=True) +class StyleRule: + """How a style of boxer differs from the balanced fighter. Every number is neutral by default.""" + + startup_ticks: Mapping[PunchClass, int] = field(default_factory=dict) + recovery_ticks: Mapping[PunchClass, int] = field(default_factory=dict) + reach_percent: int = 100 + impact_percent: int = 100 + poise_damage_percent: int = 100 + """Poise damage this fighter's punches do.""" + poise_taken_percent: int = 100 + """Poise damage this fighter takes: the chin.""" + stamina_cost_percent: int = 100 + conditioning_loss_percent: int = 100 + move_speed_percent: int = 100 + body_damage_percent: int = 100 + counter_bonus_percent: int = 28 + """A counter lands this much harder than the same punch thrown into nothing.""" + counter_window_ticks: int = 0 + evasion_ticks: int = 0 + perfect_block_ticks: int = 0 + + +def _style_rule(raw: dict[str, Any]) -> StyleRule: + fields = dict(raw) + for key in ("startup_ticks", "recovery_ticks"): + if key in fields: + fields[key] = {PunchClass(name): ticks for name, ticks in fields[key].items()} + return StyleRule(**fields) + + +STYLE_RULES: dict[FighterStyle, StyleRule] = { + FighterStyle(name): _style_rule(raw) for name, raw in _MANIFEST["styles"].items() +} +if set(STYLE_RULES) != set(FighterStyle): + raise RuntimeError("combat-manifest.json styles must list every fighter style") + + +def style_punch_rule(rule: PunchRule, style: StyleRule) -> PunchRule: + """The punch as a fighter of `style` throws it, before fatigue: reach, cost and poise damage.""" + return replace( + rule, + reach=rule.reach * style.reach_percent // 100, + stamina_cost=rule.stamina_cost * style.stamina_cost_percent // 100, + whiff_cost=rule.whiff_cost * style.stamina_cost_percent // 100, + poise_damage=rule.poise_damage * style.poise_damage_percent // 100, + ) + + +@dataclass(frozen=True, slots=True) +class CornerTreatment: + worse_cut: int = 0 + other_cut: int = 0 + bleeding_kept_percent: int = 100 + swelling: int = 0 + eyes: int = 0 + conditioning: int = 0 + body: int = 0 + refresh: bool = False + + +CORNER_TREATMENTS: dict[CornerChoice, CornerTreatment] = { + CornerChoice.CUT: CornerTreatment(**_MANIFEST["corner"]["cut"]), + CornerChoice.SWELLING: CornerTreatment(**_MANIFEST["corner"]["swelling"]), + CornerChoice.BREATH: CornerTreatment(**_MANIFEST["corner"]["breath"], refresh=True), + CornerChoice.BALANCED: CornerTreatment(**_MANIFEST["corner"]["balanced"]), +} + + @dataclass(frozen=True, slots=True) class JudgeProfile: name: str @@ -171,5 +311,10 @@ def fatigue_max_stamina(conditioning: int, body_trauma: int) -> int: return max(330, MAX_STAMINA - conditioning_penalty - body_penalty) +def poise_ceiling(head_trauma: int) -> int: + """The most poise a fighter gets back: a beating to the head wears it down for the bout.""" + return max(POISE_CEILING_FLOOR, MAX_POISE - head_trauma * POISE_CEILING_PER_HEAD_PERCENT // 100) + + def fatigue_factor(conditioning: int, body_trauma: int) -> int: return max(48, 100 - (MAX_CONDITIONING - conditioning) // 18 - body_trauma // 35) diff --git a/src/intelstream/hands/server.py b/src/intelstream/hands/server.py index b4d937b7..7095ea08 100644 --- a/src/intelstream/hands/server.py +++ b/src/intelstream/hands/server.py @@ -19,11 +19,19 @@ import structlog from aiohttp import WSMsgType, web -from intelstream.hands.auth import DEFAULT_TICKET_TTL_SECONDS, HandsAuth, HandsAuthError +from intelstream.hands.auth import ( + DEFAULT_TICKET_TTL_SECONDS, + HandsAuth, + HandsAuthError, + validate_instance_id, +) from intelstream.hands.protocol import ( MAX_FRAME_BYTES, PROTOCOL_VERSION, ProtocolError, + decode_client_frame, + parse_cpu_request, + parse_style_choice, parse_ticket_ack, ) from intelstream.hands.rooms import HandsRoomManager, RoomError, RoomMembership, SocketLike @@ -41,10 +49,16 @@ logger = structlog.get_logger(__name__) MAX_HTTP_BODY_BYTES = 16_384 MAX_AUTH_FRAME_BYTES = 4096 +# aiohttp pings a socket after this long without a frame from it and drops it when no pong comes +# within half as long again, so a fighter who vanishes without a close (lost Wi-Fi, a phone that +# suspended the app) pauses the bout about 7.5 s after his last frame. The browser's network stack +# answers pings by itself, so a fighter whose page is blurred or hidden is never dropped for it. +WEBSOCKET_HEARTBEAT_SECONDS = 5.0 SECURITY_HEADERS = { "Content-Security-Policy": ( "default-src 'self'; base-uri 'none'; object-src 'none'; " - "script-src 'self'; style-src 'self'; img-src 'self' data: blob:; " + "script-src 'self'; style-src 'self'; " + "img-src 'self' data: blob: https://cdn.discordapp.com/avatars/; " "connect-src 'self'; media-src 'self' blob:; " "frame-ancestors https://discord.com https://*.discord.com" ), @@ -61,7 +75,9 @@ class AdmissionConfig: request_window_seconds: float = 60.0 max_tracked_callers: int = 1024 max_concurrent_upstream: int = 16 - max_concurrent_upstream_per_caller: int = 2 + # Sign-in calls are scoped to the Activity instance as well as the address (see _caller_key), + # so this is how many players in one voice channel can sign in at the same moment. + max_concurrent_upstream_per_caller: int = 4 max_concurrent_ws_auth: int = 64 max_concurrent_ws_auth_per_caller: int = 4 trusted_proxy_cidrs: tuple[str, ...] = () @@ -280,6 +296,8 @@ def verify_ticket(self, ticket: object) -> AuthenticatedPlayer: ... def activate_ticket(self, ticket: object, player: AuthenticatedPlayer) -> None: ... + def instance_for_state(self, state: object) -> str | None: ... + async def close(self) -> None: ... @@ -391,9 +409,12 @@ def __init__( ticket_sleep: Callable[[float], Awaitable[None]] = asyncio.sleep, admission: AdmissionConfig | None = None, monotonic_clock: Callable[[], float] = time.monotonic, + websocket_heartbeat_seconds: float = WEBSOCKET_HEARTBEAT_SECONDS, ) -> None: if auth_timeout_seconds <= 0: raise ValueError("auth timeout must be positive") + if not math.isfinite(websocket_heartbeat_seconds) or websocket_heartbeat_seconds <= 0: + raise ValueError("websocket heartbeat must be finite and positive") self.repository = repository self.application_id = application_id self.guild_id = guild_id @@ -402,6 +423,7 @@ def __init__( self.bound_port: int | None = None self.dev_mode = dev_mode self.auth_timeout_seconds = auth_timeout_seconds + self.websocket_heartbeat_seconds = websocket_heartbeat_seconds ticket_ttl_seconds = ( auth.ticket_ttl_seconds if auth is not None else DEFAULT_TICKET_TTL_SECONDS ) @@ -563,7 +585,20 @@ def _require_origin(self, request: web.Request) -> None: return raise web.HTTPForbidden(text="origin not allowed") - def _caller_key(self, request: web.Request) -> str: + def _caller_key(self, request: web.Request, instance_id: str | None = None) -> str: + """Who a request counts against for the per-caller ceilings. + + Discord's Activity proxy hides players' addresses, so every player reaches Hands from the + proxy's few addresses and a per-address ceiling would be one ceiling for everybody. A + sign-in names its Activity instance (the bootstrap in its body, the token exchange through + the OAuth state the server issued for it), so its bucket is the address and that instance: + one voice channel's sign-ins cannot crowd out another's, and the global ceilings still bound + the total. Requests that name no instance stay keyed on the address alone. + """ + address = self._caller_address(request) + return address if instance_id is None else f"{address} {instance_id}" + + def _caller_address(self, request: web.Request) -> str: remote = request.remote try: peer = ip_address(remote) if remote is not None else None @@ -638,14 +673,18 @@ async def _health(self, _request: web.Request) -> web.Response: async def _bootstrap(self, request: web.Request) -> web.Response: self._require_origin(request) - caller = self._caller_key(request) + if request.content_type != "application/json": + raise web.HTTPUnsupportedMediaType(text="application/json required") + payload = _strict_object(await request.read(), fields={"instance_id"}) + try: + instance_id: str | None = validate_instance_id(payload["instance_id"]) + except HandsAuthError: + instance_id = None + caller = self._caller_key(request, instance_id) if not await self._admit_scoped( self._bootstrap_limit, self._bootstrap_caller_limit, caller ): return _json_response({"error": "rate_limited"}, status=429) - if request.content_type != "application/json": - raise web.HTTPUnsupportedMediaType(text="application/json required") - payload = _strict_object(await request.read(), fields={"instance_id"}) if not await self._try_acquire_scoped( self._upstream_slots, self._upstream_caller_slots, caller ): @@ -674,12 +713,13 @@ async def _bootstrap(self, request: web.Request) -> web.Response: async def _token(self, request: web.Request) -> web.Response: self._require_origin(request) - caller = self._caller_key(request) - if not await self._admit_scoped(self._token_limit, self._token_caller_limit, caller): - return _json_response({"error": "rate_limited"}, status=429) if request.content_type != "application/json": raise web.HTTPUnsupportedMediaType(text="application/json required") payload = _strict_object(await request.read(), fields={"code", "state"}) + # Only a state this server issued names an instance; anything else counts against the address. + caller = self._caller_key(request, self.auth.instance_for_state(payload["state"])) + if not await self._admit_scoped(self._token_limit, self._token_caller_limit, caller): + return _json_response({"error": "rate_limited"}, status=429) logger.info("Hands OAuth token request received") if not await self._try_acquire_scoped( self._upstream_slots, self._upstream_caller_slots, caller @@ -725,7 +765,7 @@ async def _websocket(self, request: web.Request) -> web.StreamResponse: return _json_response({"error": "service_busy"}, status=503) websocket = web.WebSocketResponse( autoping=True, - heartbeat=15.0, + heartbeat=self.websocket_heartbeat_seconds, max_msg_size=MAX_FRAME_BYTES, compress=True, ) @@ -789,7 +829,8 @@ async def _websocket(self, request: web.Request) -> web.StreamResponse: async for message in websocket: if message.type in (WSMsgType.TEXT, WSMsgType.BINARY): try: - refresh_id = parse_ticket_ack(message.data) + envelope = decode_client_frame(message.data) + refresh_id = parse_ticket_ack(envelope) if refresh_id is not None: reconnect_ticket = ticket_refresh_state.ticket_for(refresh_id) if reconnect_ticket is None: @@ -797,12 +838,24 @@ async def _websocket(self, request: web.Request) -> web.StreamResponse: self.auth.activate_ticket(reconnect_ticket, player) ticket_refresh_state.confirm() continue - if membership.role == "spectator": - raise RoomError("spectator_read_only") + # The room decides what a frame may do: a spectator can be seated as a + # fighter before the bell. + cpu_level = parse_cpu_request(envelope) + if cpu_level is not None: + await membership.room.request_cpu( + membership.player_id, membership.connection, cpu_level + ) + continue + style_choice = parse_style_choice(envelope) + if style_choice is not None: + await membership.room.choose_style( + membership.player_id, membership.connection, style_choice + ) + continue await membership.room.submit_frame( membership.player_id, membership.connection, - message.data, + envelope, ) except ProtocolError: await self._ws_error(websocket, "invalid_input", close_code=4008) diff --git a/src/intelstream/hands/static/assets/hands.css b/src/intelstream/hands/static/assets/hands.css index 78363974..bdcc2b42 100644 --- a/src/intelstream/hands/static/assets/hands.css +++ b/src/intelstream/hands/static/assets/hands.css @@ -1,2 +1,2 @@ -:root{color:#f2f6ff;font-synthesis:none;background:#07090e;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}*{box-sizing:border-box}html,body,#app{width:100%;height:100%;margin:0;overflow:hidden}.activity{background:#080b12;width:100%;height:100%;min-height:320px;position:relative}.fight{touch-action:none;width:100%;height:100%;display:block}.topbar{top:max(8px,env(safe-area-inset-top));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);letter-spacing:.03em;white-space:nowrap;background:#07101ad9;border:1px solid #ffffff24;border-radius:3px;align-items:center;gap:12px;padding:7px 10px;display:flex;position:absolute;left:50%;transform:translate(-50%)}.topbar strong{color:#f1cc72;letter-spacing:.18em}.topbar span{color:#aebbd0;font-size:12px}.topbar .spectator-role{color:#f1cc72;font-weight:800}.spectator-role[hidden]{display:none}.topbar button,.panel button{color:#eaf1ff;cursor:pointer;background:#172334;border:1px solid #ffffff2e;border-radius:3px;padding:6px 10px}.overlay{left:50%;bottom:max(18px,env(safe-area-inset-bottom));text-align:center;pointer-events:none;justify-items:center;gap:9px;max-width:min(90vw,540px);display:grid;position:absolute;transform:translate(-50%)}.overlay[hidden]{display:none}.overlay[data-raised]{bottom:max(124px,calc(env(safe-area-inset-bottom) + 112px))}.status[hidden]{display:none}.status{color:#e8effc;background:#07101adf;border:1px solid #ffffff1f;border-radius:3px;margin:0;padding:8px 13px;font-weight:600}.hint{letter-spacing:.02em;color:#aebbd0;background:#07101ab8;border:1px solid #ffffff14;border-radius:3px;margin:0;padding:6px 12px;font-size:12px}.hint[hidden]{display:none}.primary{pointer-events:auto;color:#081018;cursor:pointer;background:#f1cc72;border:0;border-radius:3px;padding:10px 18px;font-weight:800}.primary:disabled{opacity:.55;cursor:wait}.panel{z-index:4;background:#08111cf2;border:1px solid #ffffff2b;width:min(390px,100vw - 28px);max-height:calc(100vh - 80px);padding:16px 20px;position:absolute;top:58px;right:14px;overflow:auto;box-shadow:0 14px 45px #0009}.panel h2{color:#f1cc72;margin:0 0 10px;font-size:17px}.panel ul{color:#d7e0ee;margin:0;padding-left:19px;font-size:13px;line-height:1.55}.settings{gap:12px;display:grid}.settings[hidden],.panel[hidden]{display:none}.diagnostics h3{color:#f1cc72;margin:12px 0 4px;font-size:13px}.diagnostics pre{white-space:pre-wrap;word-break:break-word;color:#c8d3e6;background:#0c1622;border:1px solid #ffffff14;border-radius:3px;margin:0 0 8px;padding:8px;font-size:11px;line-height:1.45}.settings label{justify-content:space-between;align-items:center;gap:14px;font-size:14px;display:flex}.settings input[type=range]{width:190px}.settings select{color:#f2f6ff;background:#172334;border:1px solid #ffffff2e;padding:5px}.sr-summary{clip-path:inset(50%);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.lab-controls{left:50%;bottom:max(10px,env(safe-area-inset-bottom));color:#d7e0ee;white-space:nowrap;background:#07101ae6;border:1px solid #ffffff24;border-radius:3px;align-items:center;gap:10px;padding:8px 12px;font-size:12px;display:flex;position:absolute;transform:translate(-50%)}.lab-controls button,.lab-controls select{color:#eaf1ff;cursor:pointer;background:#172334;border:1px solid #ffffff2e;border-radius:3px;padding:5px 9px;font-size:12px}.lab-controls input[type=number]{color:#eaf1ff;background:#0c1622;border:1px solid #ffffff2e;border-radius:3px;width:56px;padding:4px}.lab-controls label{cursor:pointer;align-items:center;gap:4px;display:flex}.lab-controls [data-lab-metrics]{color:#8fa3c8;text-overflow:ellipsis;max-width:40vw;overflow:hidden}@media (width<=700px){.topbar{flex-wrap:wrap;justify-content:center;left:8px;right:8px;transform:none}.topbar span:not(.spectator-role){display:none}.topbar button{padding:5px 8px}.panel{top:74px}.status{font-size:13px}.overlay{bottom:10px}.overlay[data-raised]{bottom:116px}}@media (prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important;animation:none!important}}.model-panel{width:min(430px,100vw - 28px);max-height:calc(100vh - 80px);top:58px;left:14px;right:auto}.model-clips{gap:4px;max-height:200px;display:grid;overflow:auto}.model-clips button{text-align:left;color:#eaf1ff;cursor:pointer;background:#172334;border:1px solid #ffffff2e;border-radius:3px;padding:5px 8px;font-size:12px}.model-clips button.active{color:#f1cc72;border-color:#f1cc72}.model-row{align-items:center;gap:10px;margin-top:8px;font-size:12px;display:flex}.model-row label{align-items:center;gap:6px;display:flex}.model-inventory,.model-warnings{color:#c8d3e6;white-space:pre-wrap;max-height:220px;font-size:10px;line-height:1.4;overflow:auto}.model-credit{color:#9cabc1;margin:2px 0 0;font-size:11px;line-height:1.35}.model-credit a{color:#c7d5eb}.touch-controls{pointer-events:none;z-index:3;-webkit-user-select:none;user-select:none;font-family:Inter,system-ui,sans-serif;position:absolute;inset:0}.touch-controls.disabled{display:none}.touch-controls.resting{opacity:.4}.touch-stick{left:0;top:calc(max(8px,env(safe-area-inset-top)) + 48px);pointer-events:auto;touch-action:none;width:46%;position:absolute;bottom:0}.touch-stick-base{opacity:0;background:#0a121f66;border:2px solid #fff5;border-radius:50%;width:132px;height:132px;margin:-66px 0 0 -66px;transition:opacity .12s;position:absolute}.touch-stick-base.active{opacity:1}.touch-stick-knob{background:#f1cc72cc;border-radius:50%;width:56px;height:56px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);box-shadow:0 2px 10px #0008}.touch-pads{right:max(12px,env(safe-area-inset-right));bottom:max(118px,calc(env(safe-area-inset-bottom) + 118px));pointer-events:auto;touch-action:none;gap:8px;display:grid;position:absolute}.touch-grid{grid-template-columns:repeat(2,92px);gap:6px;display:grid}.touch-pad{color:#fff;letter-spacing:.01em;cursor:pointer;background:linear-gradient(90deg,#1d4ed8aa 0 50%,#b91c1caa 50% 100%);border:1px solid #fff4;border-radius:12px;justify-content:space-between;align-items:center;height:56px;padding:0 7px;font-size:11px;font-weight:800;display:flex;position:relative}.touch-pad .touch-pad-hand{opacity:.8;font-size:10px}.touch-pad .touch-pad-label{font-size:10px}.touch-pad.pressed{filter:brightness(1.5);transform:scale(.96)}.touch-mods{grid-template-columns:repeat(2,92px);gap:6px;display:grid}.touch-mod{color:#e8effc;letter-spacing:.06em;cursor:pointer;background:#0a121fcc;border:1px solid #fff4;border-radius:9px;height:32px;padding:0;font-size:11px;font-weight:800}.touch-mod.pressed{color:#081018;background:#f1cc72}.touch-controls.knockdown .touch-pad{background:linear-gradient(90deg,#1f8a4caa 0 50%,#1f8a4caa 50% 100%)}.touch-moves{grid-template-columns:repeat(3,1fr);grid-template-areas:"sl wv sr""pl cl cl";gap:6px;margin-bottom:4px;display:grid}.touch-move{color:#d8f3ee;letter-spacing:.05em;white-space:nowrap;cursor:pointer;background:#0a1a20cc;border:1px solid #7fd3c766;border-radius:9px;height:30px;padding:0;font-size:10px;font-weight:800}.touch-move.pressed{color:#081018;background:#f1cc72}.touch-move[data-move=slip_left]{grid-area:sl}.touch-move[data-move=weave]{grid-area:wv}.touch-move[data-move=slip_right]{grid-area:sr}.touch-move[data-move=pull]{grid-area:pl}.touch-move[data-move=clinch]{grid-area:cl}.touch-controls.knockdown .touch-move{opacity:.35}@media (orientation:landscape){.touch-pads{grid-template-columns:auto auto;grid-template-areas:"moves mods""moves grid";column-gap:12px}.touch-moves{grid-area:moves;grid-template-columns:repeat(2,58px);grid-template-areas:"sl sr""wv pl""cl cl";align-self:end;margin-bottom:0}.touch-mods{grid-area:mods}.touch-grid{grid-area:grid}}@media (pointer:fine) and (hover:hover){.touch-controls{display:none}} +:root{color:#f2f6ff;font-synthesis:none;background:#07090e;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}*{box-sizing:border-box}html,body,#app{width:100%;height:100%;margin:0;overflow:hidden}.activity{background:#080b12;width:100%;height:100%;min-height:320px;position:relative}.fight{touch-action:none;width:100%;height:100%;display:block}.topbar{top:max(8px,env(safe-area-inset-top));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);letter-spacing:.03em;white-space:nowrap;background:#07101ad9;border:1px solid #ffffff24;border-radius:3px;align-items:center;gap:12px;padding:7px 10px;display:flex;position:absolute;left:50%;transform:translate(-50%)}.topbar strong{color:#f1cc72;letter-spacing:.18em}.topbar span{color:#aebbd0;font-size:12px}.topbar .spectator-role{color:#f1cc72;font-weight:800}.spectator-role[hidden]{display:none}.topbar button,.panel button{color:#eaf1ff;cursor:pointer;background:#172334;border:1px solid #ffffff2e;border-radius:3px;padding:6px 10px}.overlay{left:50%;bottom:max(18px,env(safe-area-inset-bottom));text-align:center;pointer-events:none;justify-items:center;gap:9px;max-width:min(90vw,540px);display:grid;position:absolute;transform:translate(-50%)}.overlay[hidden]{display:none}.overlay[data-raised]{bottom:max(124px,calc(env(safe-area-inset-bottom) + 112px))}.overlay[data-result]{bottom:max(26px,calc(env(safe-area-inset-bottom) + 14px))}.status[hidden]{display:none}.status{color:#e8effc;background:#07101adf;border:1px solid #ffffff1f;border-radius:3px;margin:0;padding:8px 13px;font-weight:600}.hint{letter-spacing:.02em;color:#aebbd0;background:#07101ab8;border:1px solid #ffffff14;border-radius:3px;margin:0;padding:6px 12px;font-size:12px}.hint[hidden]{display:none}.primary{pointer-events:auto;color:#081018;cursor:pointer;background:#f1cc72;border:0;border-radius:3px;padding:10px 18px;font-weight:800}.primary:disabled{opacity:.55;cursor:wait}.panel{z-index:4;background:#08111cf2;border:1px solid #ffffff2b;width:min(390px,100vw - 28px);max-height:calc(100vh - 80px);padding:16px 20px;position:absolute;top:58px;right:14px;overflow:auto;box-shadow:0 14px 45px #0009}.panel h2{color:#f1cc72;margin:0 0 10px;font-size:17px}.panel ul{color:#d7e0ee;margin:0;padding-left:19px;font-size:13px;line-height:1.55}.settings{gap:12px;display:grid}.settings[hidden],.panel[hidden]{display:none}.diagnostics h3{color:#f1cc72;margin:12px 0 4px;font-size:13px}.diagnostics pre{white-space:pre-wrap;word-break:break-word;color:#c8d3e6;background:#0c1622;border:1px solid #ffffff14;border-radius:3px;margin:0 0 8px;padding:8px;font-size:11px;line-height:1.45}.settings label{justify-content:space-between;align-items:center;gap:14px;font-size:14px;display:flex}.settings input[type=range]{width:190px}.settings select{color:#f2f6ff;background:#172334;border:1px solid #ffffff2e;padding:5px}.sr-summary{clip-path:inset(50%);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.lab-controls{left:50%;bottom:max(10px,env(safe-area-inset-bottom));color:#d7e0ee;white-space:nowrap;background:#07101ae6;border:1px solid #ffffff24;border-radius:3px;align-items:center;gap:10px;padding:8px 12px;font-size:12px;display:flex;position:absolute;transform:translate(-50%)}.lab-controls button,.lab-controls select{color:#eaf1ff;cursor:pointer;background:#172334;border:1px solid #ffffff2e;border-radius:3px;padding:5px 9px;font-size:12px}.lab-controls input[type=number]{color:#eaf1ff;background:#0c1622;border:1px solid #ffffff2e;border-radius:3px;width:56px;padding:4px}.lab-controls label{cursor:pointer;align-items:center;gap:4px;display:flex}.lab-controls [data-lab-metrics]{color:#8fa3c8;text-overflow:ellipsis;max-width:40vw;overflow:hidden}@media (width<=700px){.topbar{flex-wrap:wrap;justify-content:center;left:8px;right:8px;transform:none}.topbar span:not(.spectator-role){display:none}.topbar button{padding:5px 8px}.panel{top:74px}.status{font-size:13px}.overlay{bottom:10px}.overlay[data-raised]{bottom:116px}.overlay[data-result]{bottom:26px}}@media (prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important;animation:none!important}}.model-panel{width:min(430px,100vw - 28px);max-height:calc(100vh - 80px);top:58px;left:14px;right:auto}.model-clips{gap:4px;max-height:200px;display:grid;overflow:auto}.model-clips button{text-align:left;color:#eaf1ff;cursor:pointer;background:#172334;border:1px solid #ffffff2e;border-radius:3px;padding:5px 8px;font-size:12px}.model-clips button.active{color:#f1cc72;border-color:#f1cc72}.model-row{align-items:center;gap:10px;margin-top:8px;font-size:12px;display:flex}.model-row label{align-items:center;gap:6px;display:flex}.model-inventory,.model-warnings{color:#c8d3e6;white-space:pre-wrap;max-height:220px;font-size:10px;line-height:1.4;overflow:auto}.model-credit{color:#9cabc1;margin:2px 0 0;font-size:11px;line-height:1.35}.model-credit a{color:#c7d5eb}.touch-controls{pointer-events:none;z-index:3;-webkit-user-select:none;user-select:none;font-family:Inter,system-ui,sans-serif;position:absolute;inset:0}.touch-controls.disabled{display:none}.touch-controls.resting{opacity:.4}.touch-stick{left:0;top:calc(max(8px,env(safe-area-inset-top)) + 48px);pointer-events:auto;touch-action:none;width:46%;position:absolute;bottom:0}.touch-stick-base{opacity:0;background:#0a121f66;border:2px solid #fff5;border-radius:50%;width:132px;height:132px;margin:-66px 0 0 -66px;transition:opacity .12s;position:absolute}.touch-stick-base.active{opacity:1}.touch-stick-knob{background:#f1cc72cc;border-radius:50%;width:56px;height:56px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);box-shadow:0 2px 10px #0008}.touch-pads{right:max(12px,env(safe-area-inset-right));bottom:max(118px,calc(env(safe-area-inset-bottom) + 118px));pointer-events:none;touch-action:none;gap:8px;display:grid;position:absolute}.touch-pad,.touch-mod,.touch-move{pointer-events:auto}.touch-grid{grid-template-columns:repeat(2,92px);gap:6px;display:grid}.touch-pad{color:#fff;letter-spacing:.01em;cursor:pointer;background:linear-gradient(90deg,#1d4ed8aa 0 50%,#b91c1caa 50% 100%);border:1px solid #fff4;border-radius:12px;justify-content:space-between;align-items:center;height:56px;padding:0 7px;font-size:11px;font-weight:800;display:flex;position:relative}.touch-pad .touch-pad-hand{opacity:.8;font-size:10px}.touch-pad .touch-pad-label{font-size:10px}.touch-pad.pressed{filter:brightness(1.5);transform:scale(.96)}.touch-mods{grid-template-columns:repeat(2,92px);gap:6px;display:grid}.touch-mod{color:#e8effc;letter-spacing:.06em;cursor:pointer;background:#0a121fcc;border:1px solid #fff4;border-radius:9px;height:32px;padding:0;font-size:11px;font-weight:800}.touch-mod.pressed{color:#081018;background:#f1cc72}.touch-controls.knockdown .touch-pad{background:linear-gradient(90deg,#1f8a4caa 0 50%,#1f8a4caa 50% 100%)}.touch-moves{grid-template-columns:repeat(3,1fr);grid-template-areas:"sl wv sr""pl cl cl";gap:6px;margin-bottom:4px;display:grid}.touch-move{color:#d8f3ee;letter-spacing:.05em;white-space:nowrap;cursor:pointer;background:#0a1a20cc;border:1px solid #7fd3c766;border-radius:9px;height:30px;padding:0;font-size:10px;font-weight:800}.touch-move.pressed{color:#081018;background:#f1cc72}.touch-move[data-move=slip_left]{grid-area:sl}.touch-move[data-move=weave]{grid-area:wv}.touch-move[data-move=slip_right]{grid-area:sr}.touch-move[data-move=pull]{grid-area:pl}.touch-move[data-move=clinch]{grid-area:cl}.touch-controls.knockdown .touch-move,.touch-controls.knockdown .touch-mod{visibility:hidden}@media (orientation:landscape){.touch-pads{grid-template-columns:auto auto;grid-template-areas:"moves mods""moves grid";column-gap:12px}.touch-moves{grid-area:moves;grid-template-columns:repeat(2,58px);grid-template-areas:"sl sr""wv pl""cl cl";align-self:end;margin-bottom:0}.touch-mods{grid-area:mods}.touch-grid{grid-area:grid}}@media (orientation:landscape) and (height<=350px){.touch-pads{bottom:max(112px,calc(env(safe-area-inset-bottom) + 112px));row-gap:6px}.touch-grid,.touch-mods,.touch-moves{gap:4px}.touch-pad{height:44px}.touch-mod,.touch-move{height:26px}}@media (pointer:fine) and (hover:hover){.touch-controls{display:none}}.corner{z-index:3;left:50%;bottom:max(124px,calc(env(safe-area-inset-bottom) + 112px));text-align:center;background:#07101aeb;border:1px solid #f1cc7259;border-radius:3px;gap:8px;width:min(660px,100vw - 24px);padding:10px 12px 9px;display:grid;position:absolute;transform:translate(-50%);box-shadow:0 10px 30px #0009}.corner[hidden]{display:none}.corner-title{color:#f1cc72;letter-spacing:.18em;margin:0;font-size:12px;font-weight:800}.corner-choices{grid-template-columns:repeat(3,1fr);gap:8px;display:grid}.corner-choices button{text-align:left;color:#eaf1ff;cursor:pointer;font:inherit;background:#172334;border:1px solid #ffffff2e;border-radius:3px;grid-template-columns:auto 1fr;align-items:center;gap:2px 9px;padding:8px 10px;display:grid}.corner-choices kbd{color:#f1cc72;background:#0b1520;border:1px solid #ffffff38;border-radius:3px;grid-row:span 2;place-items:center;width:26px;height:26px;font:800 13px ui-monospace,monospace;display:grid}.corner-choices strong{font-size:13px}.corner-choices span{color:#aebbd0;font-size:11px}.corner-choices button:hover:not(:disabled){border-color:#f1cc72aa}.corner-choices button:disabled{cursor:default;opacity:.42}.corner-choices button[data-picked]{opacity:1;background:#272515;border-color:#f1cc72;box-shadow:inset 0 0 0 1px #f1cc72}.corner-status{color:#d7e0ee;margin:0;font-size:12px}@media (width<=560px){.corner{bottom:max(112px,calc(env(safe-area-inset-bottom) + 100px));padding:8px 9px}.corner-choices{grid-template-columns:1fr;gap:6px}.corner-choices button{padding:6px 9px}}@media (height<=460px){.corner{bottom:max(116px,calc(env(safe-area-inset-bottom) + 104px));gap:4px;width:min(600px,100vw - 24px);padding:5px 8px}.corner-title{font-size:10px}.corner-choices{gap:6px}.corner-choices button{column-gap:7px;padding:4px 7px}.corner-choices kbd{width:22px;height:22px;font-size:12px}.corner-choices strong{font-size:12px}.corner-choices span{font-size:10px}.corner-status{font-size:11px}}.cpu-picker{pointer-events:auto;background:#07101adf;border:1px solid #ffffff1f;border-radius:3px;justify-items:center;gap:8px;width:min(90vw,500px);padding:10px 12px;display:grid}.cpu-picker[hidden]{display:none}.cpu-picker p{letter-spacing:.04em;color:#f1cc72;margin:0;font-size:13px;font-weight:800}.cpu-levels{grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;width:100%;display:grid}.cpu-levels button{color:#eaf1ff;cursor:pointer;min-height:52px;font:inherit;text-align:center;background:#172334;border:1px solid #ffffff2e;border-radius:3px;align-content:center;gap:3px;padding:8px 6px;display:grid}.cpu-levels button strong{letter-spacing:.03em;font-size:14px}.cpu-levels button span{color:#aebbd0;font-size:11px;line-height:1.25}.cpu-levels button:hover,.cpu-levels button:focus-visible{border-color:#f1cc72;outline:none}@media (height<=480px){.cpu-picker{gap:5px;padding:6px 10px}.cpu-levels button{gap:1px;min-height:40px;padding:5px 6px}.cpu-levels button strong{font-size:13px}.cpu-levels button span{font-size:10px;line-height:1.15}}.overlay[data-touch-hint]{left:max(12px,env(safe-area-inset-left));max-width:min(44vw,300px,calc(100vw - 226px - env(safe-area-inset-left) - env(safe-area-inset-right)));text-align:left;justify-items:start;transform:none}@media (orientation:landscape){.overlay[data-touch-hint]{max-width:min(44vw,300px,calc(100vw - 360px - env(safe-area-inset-left) - env(safe-area-inset-right)))}}.overlay[data-touch-hint] .hint{font-size:11px;line-height:1.35}.panel h3{letter-spacing:.08em;text-transform:uppercase;color:#f1cc72;margin:12px 0 6px;font-size:12px}.panel h3:first-of-type{margin-top:4px}details.diagnostics summary{cursor:pointer;color:#f1cc72;font-size:13px;font-weight:700}details.diagnostics[open] summary{margin-bottom:6px}.style-picker{z-index:3;text-align:center;background:#07101af2;border:1px solid #f1cc7266;border-radius:3px;gap:10px;width:min(980px,100vw - 24px);max-height:calc(100% - 132px);padding:12px 14px;display:grid;position:absolute;top:50%;left:50%;overflow:auto;transform:translate(-50%,-50%);box-shadow:0 14px 40px #000b}.style-picker[hidden]{display:none}.style-picker header{justify-content:space-between;align-items:baseline;gap:12px;display:flex}.style-title{color:#f1cc72;letter-spacing:.18em;margin:0;font-size:13px;font-weight:800}.style-clock{color:#eaf1ff;margin:0;font:800 15px ui-monospace,monospace}.style-cards{grid-template-columns:repeat(5,1fr);gap:8px;display:grid}.style-cards button{text-align:left;color:#eaf1ff;cursor:pointer;font:inherit;background:#172334;border:1px solid #ffffff2e;border-radius:3px;grid-template-rows:auto auto 1fr auto;gap:5px;padding:10px;display:grid}.style-cards kbd{color:#f1cc72;background:#0b1520;border:1px solid #ffffff38;border-radius:3px;justify-self:start;place-items:center;width:22px;height:22px;font:800 12px ui-monospace,monospace;display:grid}.style-cards strong{font-size:14px}.style-cards span{color:#aebbd0;font-size:11px;line-height:1.35}.style-stats{gap:3px;margin:2px 0 0;padding:0;list-style:none;display:grid}.style-stats li{grid-template-columns:66px 1fr;align-items:center;gap:6px;display:grid}.style-stats small{letter-spacing:.05em;color:#8fa3c8;text-transform:uppercase;font-size:9px}.style-stats i{background:linear-gradient(90deg,#f1cc72,#d98a3a);border-radius:2px;height:5px;display:block}.style-stats i[data-value="1"]{width:20%}.style-stats i[data-value="2"]{width:40%}.style-stats i[data-value="3"]{width:60%}.style-stats i[data-value="4"]{width:80%}.style-stats i[data-value="5"]{width:100%}.style-cards button:hover:not(:disabled){border-color:#f1cc72aa}.style-cards button:disabled{cursor:default;opacity:.5}.style-cards button[data-chosen]{opacity:1;background:#272515;border-color:#f1cc72;box-shadow:inset 0 0 0 1px #f1cc72}.style-status{color:#d7e0ee;margin:0;font-size:12px}@media (width<=700px){.style-picker{top:auto;bottom:max(12px,env(safe-area-inset-bottom));max-height:calc(100% - 96px);padding:9px 10px;transform:translate(-50%)}.style-cards{grid-template-columns:1fr 1fr;gap:6px}.style-cards button{grid-template-rows:auto 1fr;gap:3px;padding:8px 9px}.style-cards kbd,.style-stats{display:none}.style-cards strong{font-size:13px}.style-cards span{font-size:11px}.style-cards button:last-child{grid-column:1/-1}}@media (height<=480px){.style-picker{gap:5px;max-height:calc(100% - 60px);padding:6px 9px;top:auto;bottom:6px;transform:translate(-50%)}.style-cards{grid-template-columns:repeat(5,1fr);gap:5px}.style-cards button{gap:2px;padding:5px 6px}.style-cards span{display:none}.style-cards button:last-child{grid-column:auto}.style-stats{gap:1px;display:grid}.style-stats li{grid-template-columns:52px 1fr}.style-stats small{font-size:8px}} /*$vite$:1*/ \ No newline at end of file diff --git a/src/intelstream/hands/static/assets/hands.js b/src/intelstream/hands/static/assets/hands.js index c7ec82a7..7ba1971e 100644 --- a/src/intelstream/hands/static/assets/hands.js +++ b/src/intelstream/hands/static/assets/hands.js @@ -1,18 +1,18 @@ -var e=Object.defineProperty,t=(t,n)=>{let r={};for(var i in t)e(r,i,{get:t[i],enumerable:!0});return n||e(r,Symbol.toStringTag,{value:`Module`}),r};(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var n=1e3,r=e=>e.reduce((e,t)=>e+t,0),i=(e,t,n)=>{if(e.measureText(t).width<=n)return t;let r=t;for(;r.length>1&&e.measureText(`${r}…`).width>n;)r=r.slice(0,-1);return`${r}…`},a=(e,t,n,r)=>{for(let i=n;i>r;--i)if(e(i)<=t)return i;return r};function o(e,t,n,r,i,a){e.fillStyle=`rgba(2,4,9,0.85)`,e.fillRect(t-1,n-1,r+2,11);let o=e.createLinearGradient(0,n,0,n+9);o.addColorStop(0,`rgba(210,220,235,0.5)`),o.addColorStop(.5,`rgba(90,100,120,0.25)`),o.addColorStop(1,`rgba(30,36,50,0.4)`),e.strokeStyle=o,e.lineWidth=1,e.strokeRect(t-1.5,n-1.5,r+3,12);let s=Math.max(0,Math.min(1,i.value/Math.max(1,i.maximum))),c=e.createLinearGradient(0,n,0,n+9);c.addColorStop(0,i.from),c.addColorStop(1,i.to),e.fillStyle=c;let l=r*s;e.fillRect(a?t+r-l:t,n,l,9),e.fillStyle=`rgba(255,255,255,0.22)`,e.fillRect(a?t+r-l:t,n,l,2),e.fillStyle=`#dfe7f5`,e.font=`700 9px Inter, system-ui, sans-serif`,e.textAlign=a?`right`:`left`,e.fillText(i.label,a?t+r:t,n-4)}function s(e,t,n,r,s,c,l,u,d){e.save(),e.beginPath(),u?(e.moveTo(t+14,n),e.lineTo(t+r,n),e.lineTo(t+r-14,n+62),e.lineTo(t,n+62)):(e.moveTo(t,n),e.lineTo(t+r-14,n),e.lineTo(t+r,n+62),e.lineTo(t+14,n+62)),e.closePath(),e.fillStyle=`rgba(3,6,12,0.82)`,e.fill(),e.strokeStyle=`rgba(190,200,220,0.22)`,e.lineWidth=1,e.stroke(),e.fillStyle=d,e.fillRect(u?t+r-5:t,n+4,5,54);let f=u?t+r-20:t+20;e.textAlign=u?`right`:`left`,e.fillStyle=`#f5f8ff`;let p=s.toUpperCase();e.font=`800 ${a(t=>(e.font=`800 ${t}px Inter, system-ui, sans-serif`,e.measureText(p).width),r-44,16,11)}px Inter, system-ui, sans-serif`,e.fillText(i(e,p,r-44),f,n+21),e.fillStyle=`#93a3bd`,e.font=`600 10px Inter, system-ui, sans-serif`,e.fillText(c,f,n+35);let m=(r-52)/l.length,h=l.length*m+(l.length-1)*12,g=u?t+r-20-h:t+20;l.forEach((t,r)=>{o(e,g+(m+12)*r,n+48,m,t,u)}),e.restore()}function c(e,t,n,r,i,a){e.save(),e.beginPath(),e.moveTo(t-168/2+12,n),e.lineTo(t+168/2-12,n),e.lineTo(t+168/2,n+58/2),e.lineTo(t+168/2-12,n+58),e.lineTo(t-168/2+12,n+58),e.lineTo(t-168/2,n+58/2),e.closePath(),e.fillStyle=`rgba(4,6,12,0.88)`,e.fill();let o=e.createLinearGradient(t-168/2,n,t+168/2,n+58);o.addColorStop(0,`#8a6a26`),o.addColorStop(.5,`#f6d57a`),o.addColorStop(1,`#8a6a26`),e.strokeStyle=o,e.lineWidth=2,e.stroke(),e.textAlign=`center`,e.fillStyle=`#ffffff`,e.font=`800 22px ui-monospace, monospace`,e.fillText(r,t,n+27),e.fillStyle=`#f6d57a`,e.font=`800 12px Inter, system-ui, sans-serif`,e.fillText(i,t,n+44),a!==`FIGHT`&&(e.fillStyle=`#93a3bd`,e.font=`700 9px Inter, system-ui, sans-serif`,e.fillText(a,t,n+55)),e.restore()}var l=78,u=50,d=e=>e<480?u:l,f=(e,t)=>Math.max(e<640?112:56,t*.1)+d(t)/2-t/2;function p(e,t,n,r,a,o=0){let s=Math.min(320,t-24),c=d(n),l=c===u,f=t/2-s/2,p=n/2-c/2+o;e.fillStyle=`rgba(3,6,12,0.88)`,e.fillRect(f,p,s,c),e.strokeStyle=`rgba(246,213,122,0.45)`,e.lineWidth=1.5,e.strokeRect(f+2,p+2,s-4,c-4),e.textAlign=`center`,e.fillStyle=`#ffd77a`,e.font=`800 ${l?17:22}px Inter, system-ui, sans-serif`,e.fillText(r,t/2,p+(l?22:34)),e.fillStyle=`#c8d3e6`,e.font=`600 ${l?11:12}px Inter, system-ui, sans-serif`,e.fillText(i(e,a,s-24),t/2,p+(l?39:58))}var m=class{fightTicks=null;round=0;ticks(e){return e.round_number!==this.round&&(this.round=e.round_number,this.fightTicks=null),e.phase===`fight`&&(this.fightTicks=e.phase_ticks_remaining),(e.phase===`knockdown`||e.phase===`foul_recovery`)&&this.fightTicks!==null?this.fightTicks:e.phase_ticks_remaining}},h=class{stats=new Map;totals=new Map;record(e){if(e.kind===`bell`){e.detail===`round_start`&&this.stats.clear();return}e.actor_id!==null&&(e.kind===`punch_start`?(this.entry(this.stats,e.actor_id).thrown+=1,this.entry(this.totals,e.actor_id).thrown+=1):(e.kind===`hit`||e.kind===`counter_hit`)&&(this.entry(this.stats,e.actor_id).landed+=1,this.entry(this.totals,e.actor_id).landed+=1))}get(e){return this.stats.get(e)??{thrown:0,landed:0}}total(e){return this.totals.get(e)??{thrown:0,landed:0}}entry(e,t){let n=e.get(t);return n===void 0&&(n={thrown:0,landed:0},e.set(t,n)),n}};function g(e){let t=e.method.replaceAll(`_`,` `).toUpperCase();if(e.method!==`decision`&&e.method!==`draw`)return t;let n=0,i=0,a=0;for(let t of e.scorecards){let e=r(t.player_one),o=r(t.player_two);e>o?n+=1:o>e?i+=1:a+=1}let o=e.scorecards.length;return o===0?t:e.method===`draw`?a===o?`UNANIMOUS DRAW`:n===i?`SPLIT DRAW`:`MAJORITY DRAW`:Math.max(n,i)===o?`UNANIMOUS DECISION`:a>0?`MAJORITY DECISION`:`SPLIT DECISION`}var _=new Set([`ko`,`flash_ko`,`tko`]),v=3.6;function y(e){return e!==null&&_.has(e.method)?v:0}function b(e,t,r,a,l,u,d,m,h=30,g=null,_=null,v=null,y=null,b=null){e.save(),e.textBaseline=`alphabetic`;let x=t<640,C=x?(t-56)/2:Math.min(300,t*.38),w=r-84;a.fighters.forEach((r,i)=>{let a=i===1,c=a?t-24-C:24,u=l[r.player_id],d=`ELO ${u?.rating??`—`} · KD ${r.knockdowns} · W ${r.warnings} · −${r.deductions}`,f=[{label:`${x?`STA`:`STAMINA`} ${Math.round(r.stamina)}`,value:r.stamina,maximum:r.maximum_stamina,from:`#ffe08a`,to:`#d9a53a`},{label:`${x?`HP`:`HEALTH`} ${Math.round(r.conditioning)}`,value:r.conditioning,maximum:n,from:`#ff8a7a`,to:`#b02a20`},{label:`GUARD`,value:r.guard,maximum:700,from:`#9ec7ff`,to:`#3d6fb8`},{label:`POISE ${Math.round(r.poise)}`,value:r.poise,maximum:600,from:`#e8c890`,to:`#8a6a34`}];s(e,c,w,C,u?.name??`Fighter`,d,f.slice(0,2),a,i===0?`#3d6fb8`:`#b02a20`);let p=w-12,m=Math.min(64,(C-52)/2);o(e,a?c+C-20-2*m:c+20,p,m,f[2],a),o(e,a?c+C-8-m:c+32+m,p,m,f[3],a)});let T=Math.floor((b??a.phase_ticks_remaining)/h),E=`${Math.floor(T/60)}:${String(T%60).padStart(2,`0`)}`;if(c(e,t/2,x?54:r-84,E,`ROUND ${a.round_number}`,a.phase.replace(`_`,` `).toUpperCase()),v!==null&&d===null&&a.fighters.some(e=>e.player_id===u)){let n=Math.round(v);e.save(),e.textAlign=`right`,e.font=`700 11px ui-monospace, monospace`,e.fillStyle=n<90?`rgba(170,200,180,0.75)`:n<160?`rgba(240,200,110,0.9)`:`rgba(255,110,100,0.95)`,e.fillText(`INPUT ${n} ms`,t-24,x?128:36),e.restore()}if(_!==null){let t=x?120:64;e.save(),e.fillStyle=`rgba(3,6,12,0.82)`,e.fillRect(24,t,196,34),e.fillStyle=`#ff3b3b`,e.beginPath(),e.arc(42,t+17,6,0,Math.PI*2),e.fill(),e.fillStyle=`#f6f7fb`,e.font=`800 14px Inter, system-ui, sans-serif`,e.textAlign=`left`,e.fillText(_,56,t+22),e.restore()}if(y!==null&&a.phase===`fight`&&(e.save(),e.textAlign=`center`,e.font=`900 46px Inter, system-ui, sans-serif`,e.lineWidth=6,e.strokeStyle=`rgba(0,0,0,0.75)`,e.strokeText(y,t/2,r*.22),e.fillStyle=`#f6d57a`,e.fillText(y,t/2,r*.22),e.restore()),a.phase===`countdown`&&p(e,t,r,`ROUND ${a.round_number}`,`Touch gloves. Protect yourself at all times.`,f(t,r)),a.phase===`knockdown`){let n=a.fighters.find(e=>e.player_id===u),o=a.fighters.find(e=>e.is_downed)??n;Math.max(...a.fighters.map(e=>e.get_up_count));let s=l[o?.player_id??``]?.name??`Fighter`;e.save(),e.textAlign=`center`,e.font=`900 44px Inter, system-ui, sans-serif`,e.lineWidth=6,e.strokeStyle=`rgba(0,0,0,0.75)`,e.strokeText(`KNOCKDOWN`,t/2,r*.16),e.fillStyle=`#ff4d4d`,e.fillText(`KNOCKDOWN`,t/2,r*.16),e.font=`700 15px Inter, system-ui, sans-serif`,e.fillStyle=`#e6ecf7`,e.fillText(`${i(e,s.toUpperCase(),t*.5)} IS DOWN`,t/2,r*.16+24),e.restore()}if(a.phase===`knockdown`&&_===null){let n=a.fighters.find(e=>e.player_id===u),i=Math.max(...a.fighters.map(e=>e.get_up_count));if(n?.is_downed!==!0)e.save(),e.textAlign=`center`,e.font=`900 36px Inter, system-ui, sans-serif`,e.lineWidth=5,e.strokeStyle=`rgba(0,0,0,0.75)`,e.strokeText(`COUNT ${i}`,t/2,r*.16+64),e.fillStyle=`#ffd77a`,e.fillText(`COUNT ${i}`,t/2,r*.16+64),e.restore();else{let o=Math.min(380,t-24),s=t/2-o/2,c=r*.24;e.fillStyle=`rgba(3,6,12,0.88)`,e.fillRect(s,c,o,150),e.strokeStyle=`rgba(246,213,122,0.5)`,e.lineWidth=2,e.strokeRect(s+2,c+2,o-4,146),e.textAlign=`center`,e.fillStyle=`#ffd77a`,e.font=`800 30px Inter, system-ui, sans-serif`,e.fillText(`COUNT ${i}`,t/2,c+40);let l=n?.is_downed===!0?n.get_up_prompt:null;if(l!=null&&n!==void 0){let r=a.tick>=n.get_up_window_start_tick&&a.tick<=n.get_up_window_end_tick,i=l===`get_up_left`?`←`:`→`;r?(e.fillStyle=`#ffe9a8`,e.font=`900 64px Inter, system-ui, sans-serif`,e.fillText(i,t/2-90,c+106),e.fillStyle=`#7dffa8`,e.font=`900 34px Inter, system-ui, sans-serif`,e.fillText(`NOW!`,t/2+62,c+100)):(e.fillStyle=`#8fa3c8`,e.font=`800 30px Inter, system-ui, sans-serif`,e.fillText(`GET READY ${i}`,t/2,c+98));let u=o-60,d=Math.max(0,Math.min(1,n.get_up_meter/Math.max(1,n.get_up_required)));e.fillStyle=`rgba(2,4,9,0.9)`,e.fillRect(s+30,c+120,u,16);let f=e.createLinearGradient(s+30,0,s+30+u,0);f.addColorStop(0,`#7dffa8`),f.addColorStop(1,`#2ea860`),e.fillStyle=f,e.fillRect(s+30,c+120,u*d,16),e.strokeStyle=`rgba(210,220,235,0.5)`,e.lineWidth=1,e.strokeRect(s+29.5,c+119.5,u+1,17)}else e.fillStyle=`#c8d3e6`,e.font=`600 14px Inter, system-ui, sans-serif`,e.fillText(`Waiting for your rhythm instruction…`,t/2,c+100)}}if(a.phase===`foul_recovery`&&p(e,t,r,`FOUL RECOVERY`,`${l[a.fighters.find(e=>e.is_foul_recovery_target)?.player_id??``]?.name??`Fighter`} is recovering`,f(t,r)),a.phase===`rest`){let n=a.fighters.map(e=>({name:l[e.player_id]?.name??`Fighter`,stats:g?.get(e.player_id)??{thrown:0,landed:0}})).filter(({stats:e})=>e.thrown>0||e.landed>0).map(({name:n,stats:r})=>`${i(e,n,t*.22)} ${r.landed}/${r.thrown}`).join(` · `);p(e,t,r,`CORNERS · RECOVER`,n.length>0?`Landed this round: ${n}`:`Conditioning governs recovery`,f(t,r))}m>0&&p(e,t,r,`OPPONENT RECONNECTING · ${Math.ceil(m/1e3)}s`,`The bout is paused`),d!==null&&S(e,t,r,d,l,a.fighters.map(e=>({name:l[e.player_id]?.name??`Fighter`,stats:g?.total(e.player_id)??{thrown:0,landed:0}}))),e.restore()}function x(e,t,n,r,o,s,c,l,u=`Inter, system-ui, sans-serif`){e.font=`${s} ${a(n=>(e.font=`${s} ${n}px ${u}`,e.measureText(t).width),o,c,l)}px ${u}`,e.fillText(i(e,t,o),n,r)}function S(e,t,n,a,o,s=[]){e.fillStyle=`rgba(2,4,9,0.94)`,e.fillRect(t*.14,n*.13,t*.72,n*.74),e.strokeStyle=`rgba(246,213,122,0.5)`,e.lineWidth=2,e.strokeRect(t*.14+4,n*.13+4,t*.72-8,n*.74-8);let c=t*.72-32;e.textAlign=`center`,e.fillStyle=`#f6d57a`,x(e,g(a),t/2,n*.21,c,800,26,14);let l=a.winner_id===null?`DRAW`:`${o[a.winner_id]?.name??`Winner`} WINS`;e.fillStyle=`white`,x(e,l,t/2,n*.27,c,700,18,11);let u=s.filter(({stats:e})=>e.thrown>0);u.length>0&&(e.fillStyle=`#c8d3e6`,e.font=`600 13px Inter, system-ui, sans-serif`,x(e,u.map(({name:t,stats:n})=>`${i(e,t,c/u.length-70)} ${n.landed}/${n.thrown} landed`).join(` · `),t/2,n*.32,c,600,13,9)),a.scorecards.forEach((i,a)=>{let o=n*.37+a*36;e.fillStyle=`#aebbd0`,x(e,`${i.judge} ${r(i.player_one)} — ${r(i.player_two)} [${i.player_one.join(`·`)}] [${i.player_two.join(`·`)}]`,t/2,o,c,400,12,8,`ui-monospace, monospace`)}),Object.entries(a.ratings).forEach(([r,i],a)=>{e.fillStyle=i.after>=i.before?`#55df9b`:`#ff7b74`;let s=`${i.before} → ${i.after} (${i.after-i.before>=0?`+`:``}${i.after-i.before})`;x(e,`${o[r]?.name??`Fighter`} ${s}`,t/2,n*.61+a*27,c,600,13,9)})}var C=[{name:`decapitation`,sampleRate:12e3,frames:7920,wav:`UklGRgQ+AABXQVZFZm10IBAAAAABAAEA4C4AAMBdAAACABAAZGF0YeA9AAAGAGAAd/9r/1b/Dv+d/kH+Of69/vL/gAGaAhUD9wKOAmICHwPSBLYGQ/pC+4r72/rQ+Sr5sPle+7X+MQM2BmQHHwfgBSEFpAU+CKkLYA649Q331faL9fnzlfMw9ST5+/5iBV4JDAtaCuYIVgdWB0kKSw/iE2HuFvGL8lnxVO/g7TXt4+6L8wT7agMsCvAN0g4gDb0K4QjzCOEMBhO+GHQdcuis667sVOsZ6dTmDeaH54Hr4fE9+0wECgylELYRuBDhDU0LfAnWCb8OABXvG6QiDyiLKwjk3uUl5fziMuAc3v/cvtx23kbj2Op98xH8EQYQD/ETiBVzFgkUHxCYDc0MWAtYC4sPrhWTHE4kpitIMV41DDlm11XZV9pG2ZPXOtVN0yrRjc/wzkXPstA51HDae+DX6I/xjfq1BBYNYRMgGAYaSRwnHU8dkxoAFz0USRJLECwP5A0ZDyUSzha7HdclaS2nNJw6o0JgRw9LVE6kUMZROMnOyWDLQMlWxRzEScGKv0u+eLx5uq+5xbnxuii8ub7nwOLDMMrT0IXWnt1r54DwWPp2A/YLiRBdF9wbex6gIAkibyGNH3IdGBpMFdESOA45C08JEQoUC1gMIBImGl0fFiogM7A6FEMZS1ZThlfAXGhgxGHyY1S6Cbszuki4dLensvmwaq+crAesQqy5rfiv1bR/un/B2swO2f3mdfQbAw8OrRS+HC0duyDeHmwZUhXJDocJfAmGC0ATOBemJCoytT+YTKJX0V1qYl9lbWeztN+z4K8JrOeo/ahZq6SvurVXwVLQGOOf94YKuBgvHU4f5B3rFjkS1gyMCgcQKhoGKkw9PEzkWhhiZ2aZZ2St4ai1pG6h6qIKphixgsTW3e/4/guGGmweeRq0FCwHEwP2BwwZyTACRwBZg2M7Z4ZoVaiRpKCgDKHkqFq4iNHe7h8LXxibGUARPArNA8UFWhWgMIBLOl9CZ9JqEKzepjah0KBBqfS3VdGH9OwNIxVxDgUG8v/C/lETLjDrS1Fe62gLa6anjKD8nTmgU61jxsDlmwFlD4gNHQJ38/TykQOGJq9GQFxCZn1oE2URnQiaVqC3rDrKK+k9AtkKPADV9k30rfo5Fk44x1WZY4pnGWf1nCqZO5vhplO9seDF/1MQsQykA9r3VfRl/yUePTy3VaNiJWZzYtCYSZc1nHGr+MZo5q4ADwxeCLj9DPOK82X9UBr9PRdWqWTKaS5oh2PYmZmZfp5xq4rEPd5T9a3+yfzA8/npoOdf8XsFYiXlQutYMmIYZphjS1/pmPGY7pw1pvm30NKu6Jf7GgCr+5vuDufK5Czm6va+C6IlfT29T4laJF5mXdFajlE4kjyUlZdxnamq9by30VXmivRW/Y35QfJO7hjsuuZh6ub2DAioGBssJUFPTD9T0Fg8WEVVFFU5VENTzJXSl82Z8J9Eqv+3CMZq09Ph3/AL9TH5QfVM7vDnROO44VnfyN3X5C7t7/xeERwiSC+qPM1HlFIkWFxaWVsAXFVaUllVVjpRD0phRoBIppSulcqXy5v0oDypUbV0wNvHk9Bu2cLcTuC946rjsd4L3UTdGt1X2SnYlNTn1MLaMuDm5drwK/dnAJUMvRmgKM8utTgwPzpE/0oNT81TOVazVPlWPlZMVRtSqU0CTMtIA0qLS3NKZprGnROgl6SRqvGwf7W7u2/G9dA92Ink4+uZ8CfzkfFT7TzmEOHK3RnZ99TczxHQjNO02d7gfupp9hMAbg8yHIoqEzWlO6RD1ElkSplNHkxOSS1GA0PjQEtAEz/mmiudmKBhqHGv7Lqyx3PV0OB57Fruqe827T3l8dwe27HWIdgS2nflQ/O5ATEXQSq5NpE/X0CJQNY96TpyNpwyAy6SM9mbyKJurdS6kclG2vPl0+8R7BHpjuDh2lbYYN936sL+ehW3KuY7UUU6SB1Hj0HyOzU4iDsXo5WpKLZgxHjV9N/q4jne2dYx0TjU89x38boMsCYwOtlE8kQWP3M2pzKRNm+k3q9qwDHShd/E4bza0NLp0J/YoulEBQwlgTlAQUI/bDdrLhAt+Tcoq/C4WMoh2cXal9M3zQPN69os+C4c2DXWPJw7GTO1LsQ0aUPusxDHwNnS4U/brNGqynPQMev1D2MtjzboNdwvFSgVMOtBe7ZCy0Tbgt1Z1DXKN80a3rX/ACD2Mv03IzIpKe8qMznGsPbDQdZe3QTXyc6PzDPZO/XVF8MukzVYMZMmfySgMh9HpcCG05HcBNcmzK7JCNMH7cYNyycmNP4ywC1lKPcxIkLFUQzOKt183gbUvsxDzyXd5PwxHL4u2jKTLTMloCBLKcQ75U2txafSxdQ2zT7Gt8cy1dnuSgx9KKIzTTKbKukiICSDL25BH1CSxi7QotBDy2PE+8F9yXHZ5PZmEPkjZiywJzEiPxrOGOQlvDinSVa+xMhR0LbMcsbMwj7BUcaZ19fu9guHH+IpXixGJ6UgQxrAF/AePS2VPvlMxFS+xxfM9MmaxRPCvr+WwenIK9nK7IYBDRZ5I1koZijvI3scyBc0FVQcniZyMylCyEySVDrE4si6ytjHxcM3v0u8WLu6vD/Et89q4HbzlAjIFwEhySaGKagmECLGHJIXgRJqFB0dYijKMzY/30hlUBVWQ1m8xg3HCcZAw4HAHb0du6e4/rnpvfHCF8sq1+vjMPQqAc0OahivG7Yfmh+8HFwYEBWuETsQCBFnFAwZgyEqLFs4vkGTSnhRV1fpWinElsepyPLFS8IJvw68D7fKtSi0sLXyuD7Aoshc1dXi8/KcAP8OSxiZHgEfZx+4HE8ZwRV7EN0OcgxRDoYX2CLnLYw6OUVKT41WC1wSX4LBrsGewKa8Dbm+tsGzzLKVs/K35L5jymTZH+3Z/DgMFBnZIPYiVCHAG3EUpAwQCvIKEhE8HNArBDxQSo1V+VyDYAa/bb04ug22OLNusZey27cBwxPSxOi7/zoT9RxHIXogNRlmEawLBAzUE88imjbnSN1VQ16ZYsS6jbdKslKuKK3Zr9i6IM0m5UQAoBNKHU0bLRR6DHUI9wseHMkyGEhgV0Bh1GN+tHyteahVplCqz7ix0G3tXgn7FzsYGxGPCUoHdg/BKUVDJFdqYYFlxLDnqZ2lfai+s7TKfO3aBxoWJBVWDFgCZQF2EGIwW0xsXsxkP2XlqGGjnaQ4sGbLwO7kCp0UIQsR/Bf4RgVEIxtE61nmYvRiy6RqoEuhm63pyZnwOQv4EoYJy/xG/NIS3TXHUjlhQGaAY8CiVaGDqH6+gOH+AWYRxAsh/I/16gDNIudGDl3RZLhibZ7hmzmgkLF50tz1DQcEBaf4iu8Q+qsbNEMkXa1mPGexYXqc255qrf/JaO5aBrcHufyL8MnyQwpgLiRQ+F9bZEBiG5vOmDGf5a8BzlvuLQMRAcD0Z+ko7BX+UCPGRb1aHWN1Y9NbmJaLmHujRbow3Ij3jAWQAkT5qu2h7yb/fx5JP9NWc2FzY7xf0liBlYWZYaQ7uoDW/PBa//X+RvRT5lDh9Ohx/V0dpTxkUcNdPGEXXm1Xa0+okEiUB51ZrYXEzd8n8yT8cvyw9VDoNuNI5jvzTgiFJLw9nFAgWUBfLl4+WjlVclJNkxWXp532qWK5ps7M5Dvy8vl3+sD1FekA43veDd+O5mX1ZggoHNMssUOJTxJX8VmgWKFWqFRiUudOVk08keySxZbknZmnObLfvR7O9tu15XzuafEY9CPs++hp47jeWdqV4W7lEOwr98EIKhjxKsA6qka0TA9UB1oyWiBa5FfGVVZSl0/vTHZMpEsdk8eU35fcmzChOKkXsTS6ZsL5yvTVHN9w4kbnAOgX6WbknuKk3afax9ng2ZzXLNmE3lrj/+4rAdkPSR3BLLE8WkWGSzJUB1dRV2RVuFQmU1NPH01vTCxIXEbjRI5Ip5QTl86aYZ8npPyucrr3xsrS5d1O5oLsCe+F7gPnFuA/3UzXstEOz6/P5dZx4GrwkwNBFSArzT+TTCFTlFYwWQ1aT1gdVS5Pa0ngRnVI0ZlJnkKjDq1yukTKVNlQ5TnuC/Ff7RXoet6W07XO0M9y1Frg2vS3DYgjBjixRStPclAwUCpOaEZmQotBckX4nSml0bLxw5rSK+MA6W3rrubD3CHZCNvQ4gr4LBJLKo87pkZDUOlO0UfEQQ47rDrDmxKji7H0wrPUFeEn5NfcxNIDzPXS3eGE+dYZ4zRZQhhHBUX3QBI6gzq2RFSni7YvyaDeqOW+3I/UmNC71d/pzAkcK5s8kUMuQJE4vTA9M0A+S6hSu1bOPdir2xPRisqQzmrfzvxCH9o5T0ORQbg7wjWaN01Jw7Xsy1PhaOcP3gnSctCp4C78LhxnNrc7QjgzLiop2DFsQlSyvMi92jnhGtmB0zjW+OUeCLom3TiBOuAxOCmGJ2g2fap9vnXU8t3K2FrNtcej0D7rVxNOLQY4gTYrLYInlTFNRGe3F8wM3E7cKtIpzRXQU+P9Bp0objZ8OXYyACdeI0AwyUMltf3DRdAv0O7GRsCowDnOQO1rFBMrOzRGMb0rnikGMCdAUE56wHLRS9PuzDvEEMLTyWXcz/oOGLIslTUqMwUt/CTUIgUuz0EnUfnFktTK1hbPq8crxCnI3NNB7N0FiRp3Ky8uOin1IR0bkhtiJYM2pUceubDGPc9izyzL/sWcwljDEc0K3eP1eg57IFMoZizsJ2okzRz8HX0gAS0YPqRL3lNSy5vTUtY30xXRz8mpxobH8M422E/rdwKyFFkhwyoELHsqlySEHcEWJxQzF4Md6Cl6OKJGKE+fVTnI98xTzZbL78gQxFPAbMEExJzGhs8r3Z3uQfwDD+AZmCLdJ+MuKywNJtIhZSCWG1EeQyBpIwEp7jMjQLBI8k5RVEDDZMf2yv7KSMkxxLfAi70XvEW62rxfwRLHEtEU3w7wcgHrDUMboSEnJ4EqlSnxJT4gux3wG8wZ/hoxHCMjuSrBNglA6EZnThNTulbrwvzGRModyRrItMVWwkLBQMCzw0DGn8um1k3ia+91/jUM4BccHTwikyToHtYaTBj3Eq4SWRdrIAEr1zOzQBZL9FLRWMFbx8bGxUjCBr0Vu623s7UvuPG8ose71qrpNgJ5Er4dZSSPIzkf5RhhE9QNChFTGvQnqjlzSSVVhlr1wf7EysDNulW3NbWNtWK7iczl4o/8IRMgISslySI5HJwVzBDMFYQjcDUGSItWOl1Qw0DCHbtJtROyObcZwS7TBO75CgsbkiEXHewTfAgTDqIe9jYkS6FYv13qvIi4IrM7rwuzh8Bp3PL9uxYKHfwXRQwMBmcMmyIXPbFRA10CYFy21rA0rp6yz8Jz3xsDhhOSFUANZwMCCLgeIz8rWNdgbmMht+uun66+usPW4fn3EHgX/A5VBEwFPRxFPUNWzmALYiOv3qeOq0i8aN0fABYRLA2d/JP4rwtTLb1OOF6qYeytOqiYqnO7595FBTwVEQ4L/yr6FwzRM7xRsV5BYDCn4aJFp/O4C96A/5IJmACQ8pL01xCROaNWwF+VXlSjNqGQqW3DyepmBUIKNv3K8tL7hB79RDhaLGE3XvWeTZ9dq/rI6fAtC1YNtf3r82r9ch8oQ0latWCiXSCdHp3UpSO/FuQvAW8Ebvfm69HwzQlhMxhSN19MYBtatZnNmrCkortZ3Sb1nPq/8OnkJOXI+Y4gE0SVVrtcG1m5UAWV35lgqGLEoeVa+hD8c/LA57zntvhrFnE5gVEvXjhgslrtVMWVpJkWp5C++dxq8Sr3OfI15Rvd5OHv86ERIzH0SblYZl1AW7pV+U4Yk9uWYqLitj/Rc+hs9Ub20ux84e7cE9+Q6jYAix8XPFxQAlvEXPxaRVYVUQdOy5Lhl2Whca/DxVjXiOTG7EjrW+Fe2JfTO9cQ3Xvw6QgsIpA7WU1KWJdcrl3zW0VVE08JTJ1MV5JRl5qf9KqLulHPy+Gz7HfvrO4g6Njg3dk21qbVBdwu6QX+3hTVK4Q9sUpLU7pYIFk7VvBRg03aSH5G+Ef0j0mTY5mRo2exwcFP0sHgeuqy72TxoerX4zrZZNKL0F7RVdXY4SrycQllH6cz30PXTl9W5FoSW9NYYVQFUDFNXErySnRPPZYKnI2lTrKLwBjO3Nlu4rDlUOTq4NHZfNMaz4/PTdTk3Jrr1/51FAYqRD6pSSBRN1OGUwhQfUuvR1NFWkP8SWiVOp6BqSW67ssg2ozk6ucL5HHc9dPozTfM8tCf3TPvqwo7JRQ6yUbUTJtOR0usReFAVj5yQWBNQZxEqke72c372YbeLt1d1YzM3crEzgzd3fM/EwwxKEJlTdpN7UglQNw5IjsfRduZNanQun/Pz9sL3ubUQs0EyqzQC+IbAu8jIDz+SDpMDkVVOUU0fjmOSkWjlLdMyxDX9tYdz4fGjsfY02/vmBXVM2ZB2UP3POQ00DItPgeeQ7GPyaPZmNhuzszDLMTT1jT65B4aOb1CD0CeNv4yZD43UjqykszV2pPYC8slxATKY+G2CGUrvzs0Pc4xUivSLrNEEKr0wAfS+tDRxne+s8JV24cDoyfzOMw31y2tJYI0rEypsxnKqNNazlnEEMMH0RrxERv6Nb49mjXLKf8pPjviUz67ycwizpfC6r3jw8/c8wT1Jk82SjKzJ2Ikiy98SPxby8iB1OLPZ8VwwkrOlOsoE/ssRzKXK30hISFRM0JM+12ezL7T48wew/LCEtHB7toW1C7dMvgrRCRVIa0tskJDVgDCXsy9zF/Epr1Ow5LULveEGTwsQy96KC8esxqvJLE6a1BGXBTK88wVxS6+JLzjwijXl/VSFHomQSwlKC8gUhwCI8QzyUTNUzFchsrgyaLDurwOumm/lM0d5ccAThcVJDcmbSBUFqMOjhC3HIkvAEIZUKpX08OkxSzBobuJtzu2uLsIxyHcxvR3Cxsc+ySRI4UdkBcGEy8N6hJ5IWMw5kCJTD1VnFhDxIjFWsGfvHG5Kbm0vEXE7M873/TyOwZTFvUhbCPOI98e8xceEUwNkgxMDt4WSCUYM7o++khHUclU8lZFxWHEt8CLu8y5ebd0tmS4vLtRwhbM8trC7Yv7tQrqEzAcGCGhHbIcfBcUEtoNKAxRC6kOJRT4Hj0o1DNqPpZHkk3cUVVTwVVZwRy/Pb6Jul24yrYSt3q5qblVvQbFT8xx2Gbkv/Dl/okHuhGjFhkYHBe+FC4Q0wxaCgsI4AdWCr8UZxyoJ2k17j4ERuJMsVC8Uy1Uf77svAS6vriCtSS11rWOtti53sF6zMfXrud1+X4Fzw70FiQYDxb6EKwNAwYrAT4EkAppFZwkejF2P71Hm0+mUyVV3FU4uwS3cbW/szC2F7iSvz/M6N1D8a0BgAsyECoPVAuNA7H/gPxhBq4SniUXNzBEnU4CVfBUrbm9s1WxyK8+sgK7usaG2p3zogTnDVsLBgW3+7P17fjHBEAZgDEYRe5QDlVfVKCzbrAsr4qyOb/+01HtRwJrDk4L8f/b9xb4CwEJGukzoka1UARU0FKFrvmqmq5tuSXSuO9oBq0NFgk+/c75wQLrG7I4w06aVlxW4VCWqi2rjLLGxKvj9voGBIUAyPag8EwBeyFAPt9Ns1P1UdenOqVBq065J9ZZ9T4C0/kD7qbog/rVHXA/M1CUUxxOMqXbpTazCM7r7RUETABm8YnqofYBGxE8glBJU+JMqKGro82uPciP53n+RPnC8YHtHfskHPw+xE+tUghL3kY3ooqufcy878kAXfuY7rTu4wJJJ/tGsFXWVntQ9UqIpP60ntN69f8CEfmS6BbnIf5rIw5FKFF0URpJQUAQnU2o18El5EPvMu1g3g7dEe2cET05/U+YUj9NDkS1Q62gAray02nuSvBA5wDdT9xf8eMWrjU/Sv5NA0jRPhc9L5tJp/y/Q9wM62/nANzY1JfgDfzCJBRBQlIgVbxQtUkCRn2ZCKTru27W3Ol67UvnKNyS2PndK/bHFcE3r0ieUSVNcEZ0QTxG5JnopJG2VdAJ3+Hmc9/l1HLM7c5M22b1rheRM+dEo0wFSi1CHTquNztAREkAnc2rur2I0mTcwts208PI2sZ1zRDavvJvDYIr2T7BTH5P6k2tRs8++DhkPPpBn07Xnr6sPbwCy53YcNqu0tbMPMi0xGTGB9Eh5pL3FxS8LQ4/BkhISN9JJUTxOnE54TPENg1BRE8gnr6rGLwgzRLY3t4j3Q/aV9H5yVPCvMJmxkHRSufT/t8SLSrGOYhEE0mkSHlEKjtoNwU1ATUtOvlFO1PInRqpTbhxxMfMBtFL0h3Ld8PqvEK96sCCy4Lau+wOBB8dyzH4QRhIN0rxQsQ+WzkIMwMxmDZUPr9N71qspfaz+MTW0MvWWNOkzQLE/8DnwNDDyszf3V/48hTuLJE8eUPfQ2hA3zmBMkwtDy+/Ot1Nnl3qqJy5E8aVz57Pess+xJm8Pru/w8vV0vC4DDAlCTM6NzA3PDBbJ3skcixXP9xUCGV/tBPAE8hJxA++pLjgt3rBlNeX9dcYpTFhO344vDBcJ90knjDXR2pe2635wK3JycQIvLy1wLrtzaruTxScMEg81jtmMUMnWCkfPJVUO2iQvAHGbr4/tGmvArZRy3jzUBr0Lv0v+yRtGjEhvTntVTFp3r7pwcS5pbEjtLjFM+xfF68t8C39In0ZjyIrP6lcY2uHvQ66PbAwrbm5UN2/CG8lpSwgIz8Y3R/VPZtbDGqtuy219avCrE3B5OypGCEqeCTQFRwamDIhVLtner5wutmwza+kwVLmuRO+KaghMBN1EMcoDU5IY1q3trPSqb6rHb/96C4UoiOGHnMPrw5pLMdQI2NSt96wv6hzq+LBj/D6F/Ej9xrbC98RkDJaUgFjPLTIrHWmiKq0wXXtkhCnHFERqAY6DiwsrU57YA9kqq9SqBer37zZ4xwMEhvME4oGEAcbHuVCAlvVY8+1qK6cq7y039DY+oQYHh3zDgMDWgQEHco+mlNLXWldpao+pwarorvg2jD7sA0nDDj9ffIZ/UEavzwPUtFaW1rGp6yku6e/s4fNz+xJBb8LoAUN+TH1WgKWHWg7JlDHWN5YzFNjpRalSaqEuVjSFe0sAZwGDwHG+Dfz6vnYC/YmnED6TnpWFFdOVL+pV6fUqRCzVcTf3ObyFAWeCFoAqPUR8J/vDvknDFgmPjq8R0FReFQKUldNxqYGp1OpOK/2vJ/N1+Ky9fYB0wWD/zL2J+/66jDsUPbrBz4eRzIwQeNKxE/KUAtPwEqeRu2lQaaEqQuwtbp5yFnac+vJ92sAUgKz/Uv0Au0+6Tbp6u/4/DoNPB6xMbs/ZUjdTaNPa099TARJnkVDqDKoDKqRrbWzNL57y1XaV+nN8an4mPkX9sjyS+z/5rHjKuXJ7Hf51wfpF1ApjzbLQd5H3EpaS2RIbUYGRRxCjKlYqoKtB7JLucjFbNKE4ObrYvMX9EbzPu9v66Lk/uIJ42vp7fYDCkcbJy7MORZEoEceSQFIy0MhPus56jchqA+rmbGpuufH19ZL5VjwcfOR8CbqDuMU4I3gT+Zt9CgGvhmpK5s4sUCUQihBuD49Oj053qgarTK1z8Fy0nvijuy28DLu/+X93WPbq+L88dYIxyIZNSZAX0S3REtAATkmNk+oqa0NuezKbt+/6hvuVuqS4qXdRuGV8ZQHwySfOMlBg0IqPdI1mDKmOBauX7tkz9zj4+vm6ubhZ9qw20vscgp2K4Q8fkImQNI5hDVoPCOw1cC01fDoqu/15fDaNtrR430BOSExNzA/sTzQNeoxDznGr0zBQ9si6gbjB9aJ0f7dIPiNG+M2SD3qOHAy9DCBOmWvecSA2wbiFttg0fbS4+RqCl4saz1XPLwyHyzKNKurU8GN2MHhHdqt0DDRHuTmCAAt2TmsOOUuoCqxNn6sXMI81zDa4dHdyG/NK+Y6C1grqTgLNVgtBCneOd+tMMIz1QvYEMxMxz3NDe5NFswwhjkQMUMmuid0OoGvIMXF08DUi8lIxWPPke61FZswTzluMpcngyfoOAdRRMC50QDQKcTSvqvE1txgBT0lwDV/NcwteyhyLw1Ew1erxhfRksxRwLe7KcG41e/2hxl2JmUqGiP0Gasb9y8ySbiyosFiyTLDGLh/toe9htEh9FkY9S2zMOUoOB9MGzon7TzhUoVfhsJVyHvEJrxWuH+49sYN4Lz/WBzMKNIp4iVvHzkggSfHOblQwl1Av5HKwsxJxD+/vbvCv5zI89/9+4QY1SniMwwzjSvZIFYaoxuCKdE3hkvsW+Vii7wAwT68fLXZrmSv+LAQt37HqN4p9SsN/R2oJNQh4R1OFRESUBIzF5Qk0zisS2RZjmOPaFG//L6UvyK7vbSAsmq0GrZcv6nKJ9y27yUGRBiuIMIlbCaJI30eqxiqGAEZsB/9K3k5/EhkVcpfwWf7ab24xrhwtYuw8qoiqI2o96kcsfK7ncnu2qHs1wPIEH4bayFSHnIWtwqNB4QFcwY+D+oZ0ihYOjVNm1sAZHNpuWsst3Ozfa19qWKleqPao0anKLDuuxPNdeaXADgUdiDHJ7oo/yIZGP0V0g5UDQMTViK8L3JBMk9aXotjDmcxqp+oUKRAo9ihJaRop5ivPsNY2hf5FA0WGi8gryJVHGIaShGvC4QMpx92OXROVVv1ZR1ry20OsNKsLaaooqSmzrNFxoLh9/xjERUajB2yFlQOawadDzQfZDZwUINhO2tibwewo6rzorKjNKwvu67bVPs7D6IZAhWyDPH8wPivCiAnwkjRXd5mwWfRnoCZoZpHo4+0xtO5+l4SvBg5ENEEJf7mC1Ex61CxY+dqgWq/oGacMKC8scHRnviSD4MUaAcn+WkDVSLlRWlgNWqXagqf/pplobG1gd0TBL8SCQ2n+1n2EAv+MyFWemOPZiydOZqJnle3QN/IA0QPHAf2+P76YhWKQxpcUmWFY6iZP5p4qNrNvvlrCiQEe/ES8P8P4jy7WX5iql83l/GXYai8zUX1awbt9j3oFezpFcdFOFt7Xy1XPpPjmP2tkdWS9afyfuV531H3eCkfUGpeKVyNVKWYiaWDxrHuE/7Z9efnXe3mDZ488VeZXcBX+VAWnJauJtgB+4D++O6M4b/vIRYWQBdVt1aCTAWUrpgZrZXQV/HK9gfsKOJF8m4ajkSUV49avFIxSsyYKqhBytvrSPin7ZPfC+MiAH0sq0oQVZBS/UY4RBqbFKx2y4HozvG551namN/I9S4bOUC2ToRRVkpJQ19I86BvtxbYDe4/9ebo491q4G/0zBrsPOBOJFMGTdJDqUD0mBOjG7d20FfhkOOC3W7RYc9V4C/7Hx8UOYFIWE0pRbs+wzzvQnKjQ7Nuy8vdLuPu4YXZXtGe0Sjd2fY0F+szb0NfSEFFYj5UONE4JkGPotOuEcFO0+7fy98m2/PR9MvBzFbZhu+7CSAjEDcDQFJBnD4rOGoxfzGEOc5FZamvtrHG/dQo3T3cdNYX0IfLocod0rThzfgxEW0p1Dc/Pl9Amz2hOVo1pTE2NcE8NEgCrhC6Vsp11rzchd7n3A7Xc9OC0GfUUt577O4DNhqkKlU3hjzPPu881zatMgUuOTAcNnBCZ6sjtWPCMs501izYdNQJ0VTKYsdWyVrQnd248TsIBh6wLO80TDczNggv4Ck6JVor7TTmQdqulLvKyPDWNd/b4fDapNI5zxfO79Ml4Xf2ZA7IIpUyrDnSOIo04iwqKScq1jATPhpL+LoCyUvU+9mw1fbO98lnyVrRpuAp+OkSvSalMx414zBMKtshHyPWLqw/QE6bwCbOsNVA0YnKvsSqxbHOsuVxAuseziyrMa0vEycnIXsmhTdHSpe+o87x1XzRcsqhxlzL2dnl9YAWUCuSMV4sDSPQHUQmODoXTZDEINBD0LjI+8Gcxg7YUvnrHOgtuy9pJ1EfNSTaMxJKksIE0BvQtcdAwrXFetoc/wIdqCmuJMwbwhv8KkJD/lPGzWHQ5Menw8vJa+HQBuwjSSu/I10ZBR4iMyJL98MTzGXGAr9TwSbUFvkkGzgm0CG6F/AbEDItS/BXocwexrm+jsHB2CoA8B2eIoMVpA2aGM02VE4pxIbE270ZvEjJHuqTET0kfh/jEfUVHi3rSLlXLsuAwyG7TMEq2kgB8hoJHS8PFQdfF2E4Hk8bV/K967cjuAfIl+x6EVsgphn/DswUci3XSMVVz8EmvCu2CLu+0QH4WBVRG/0RgQrlFTQz/0sUVy3A7rh/s3e4Fs0S8VwNqRNVCjcAdgqzJwJFzlSuWEG5LbN7sr/AXN4cAEgTng8PAkf/Kg1ZLspH2FSCWGa36bHbsVK8XNdw+k4RthUEDUcBngQSGn03D03bVypaOrc5sY2wkrgRzkfsUQeQEDEJ0fvo8+T7fxPdMEdJvVN8Vj5UZKsiqQGt5rc5zaHp8gLuC4AKCwI7+lH4XQhvI3k7qEyqVbJXnVSxqcWnPKhyrky8ptK567P9wQVWA3T5tfG+7rj2iQlSIZc5GEvDVA1ZiFj9VaeoBqYxqA+wJL0yzijmaPwMCQ8LVwmh/9PzW+5089T79w2jIgs5B0lqUlBXZ1gXVmxSdqI2oT6izaYQr169AND345b2BARmCi4I/ACX+lXzu+4Y83z95wsvIPs1u0V1UMxXjVvVWjlYbFTRn4CejZ/VodKmrq/ZvXjOw+Fl9WMAFQMqAvL8tfjz8TXxE/PB+PMDbhXLKo48hkmIVPFY3Vl7WHpWp1L2mmWaD5wunyCnrLKmwmrVtuiW+Cf+z/up+NruJulQ6DPotfDWAL8Xfyy1PlpN5lbKWf9ZkVeXU7NP7ZkqnKmhaqsSvKXSGuiE+Dn/pP2Z9AHrcORQ4krpGvmhDxortUPVUtlZAFvZV8FRykt0lNCXs54nrR/Efdps7WP0HfLn6Vzhfd895rT5JBlaNnNNJljlWktWBk0IR8WRzZe/pbm7e9Y06s/wi+044nLbkt2l8ywUhDa9TrFX9Ff4UbVNKJNymPKnmMNe4JnyXfO05S7WGddb6goSBDleUNhXklV6UOtP6ZXupM2+Nd538zP1VuSY1iHYlPQ7Hbw/y1HYUeRJhEG4jneYarEU14Lq6+Xo2uTT9OXKD/09MlZRV71S7EiNSU6WeKlLyPLb/Ns5zr/Jz9mpB6Y1jlGZU5FHMjyPSNSbxblB1IbgQdPCyUjUXfjPKLlIhFG+R5g9f0Eul2CyetKB2ozRIsTvy6HuBST2Q9ZK5z8ZOWo+xJYarzXLUdQuyyHAnctd8PAhC0J5S61D2Tb5PQiZ97OezwDW5csnwt3N3fX6KN1CekbxOGgx2jjplD+qJMXhzcHFGb8EyY7sfh+mP3RHXz74Mdk3Mk75p0bBZ9CQzzLEssFR1C0BczAVQ01FVjb2KRQ4SFFpq/DCvM7Gx3q9EMIo1jr9vyspQtREtTn8L+cxkEEnWtSvpcTPxr299bQiuPnNYvgbJFI77z9/OYMv9yuIOkJQz6WYuqHGG8XjvYq3zrvyynfsLRpANs8/MDpQLtYltioOPwlVwqg+u27IwMi8v/G4n7gIwVLYkf1/Gl4y+DdvM5AqcCSMJt4wfUTUV2WrcbxCxlfDuLrJtHm1oboVz37r0QuzJe41+jsnOZ40cS43KS4uzzkzTDVbjmSPvbnErsFwvqy4Y7TXs0S7ec9w5mQC8Ry2LUo29zboMUErYR7KHI4exy3/PvRPY13Hts++n8MiwRu96LYus/qxy7ScwObRmedB/4UTzyDrJYsoACPZHGoYnBkiHbApYzs3Sl9Ur10CtzW8GL/Qvdu5Z7NXsue2E7/pzKXe8vGhCAMXjSIrKDojVh1+F9AUABcgH6AwykLZUZZdqWJrxpTMDstbxvC9+bqquMK81spT3rzyHQkCHMImryd6I6sdLBilF5ofrCswOZZLwFYHXhq/Er90ulS1ubIEs/O3b8U42tDzhgo2G+odcxzXFm0PKAwDFPInED1nTARY81xFv7+4RLN/sbezk8GC2ob2LhJwIPYkfyIzFywRnhVcJL09C0+RWetdkcL6vVu4cLhzwCvUN/ITENweuBroDvYGNAcRHZA48027V8bBYL7kt6K2Sr4g00v14xLpISUd+hWgDZITcivlRaxThFllvTu2lLKPuyXREvVcEPIZDBIbBooH1RtJONhOEVe3vIS1wrLNu6vT+vnHFL0WYgaPAMwOPSyRR+1TUFSls3KvtrdGzg7zuw5mDr8ASf4tEtQ1I01GVIy4BbGEshnB8uCwAU8J0P0h9tkD7ij0RsBRyVATsfywsr1W3QP/YQUI+fTyxAMFKmZH71HUUOOxWbVHyTLsWAnCCxD9qfeXDIkzR0qxTxZM5a/3tJvN5fOzCiYEzfaQ+Y8aXzw4TVFO169ZrQy34tMc957/Y/RF6hX4wBuqPyNMdEqZrHSu/L1b3xj8vgK98nXtYP0MJLJBnEovRweriKzhul3YmfZ/+cfuvOjr9lsbhjrwRSNEFz0sqQSxRcfK55j3JfKR6HnsywQGKQpC5UjmRNs9SKpYtVrNW+tW+WfweeSq5Ov7uyEMP0VJD0hGQa0/LK8swJ/cR/T09ELqOd/E44H8MR5CO5dGxEVuPp86k6h5s4/IP+OF72/sz+Gz267hMPo8G/I130J+REI9yTYPON+ptbejzPviSe716wLk9Nsx4wP0PRBuL60/KUWCQSs6LzaZNyKoRLShxhbcBOit6ivkyNsW2ZPfx/MYEmEspD9HRphFSUGjO+U4AT5DqTq2XciF3e/sN+1F5mDeM9YL1gff5/ImDuYmpjd+QC5C6EBYOrg1xTRMOVuln7AWwA7Sy90X5iLhXdlt1DrQKNTh4Qr2vA+RJO83XkINRaZB3TwINsIzzDaZQU+nRbMCxN7V6N0U45/e99hN0a/NFM4O1vzpUwbkHoozMz+FQ7lBsz7aOOUyDTMmOvJFZKh6tQnHH9TA3ezdE9jH0E3Npc1V1oHoEAFUGq4vYTx/QlRBDTvtMjAtHi4iN/xG36emt17JsdcG3pHXydBGyiXKaNGf4rn98BpkMV9AbESjQao4yDKAMqQ5dkk+qBS6m8xn2ELbQNQyyUHEjsj81tXznRV1MLg+PUFCPMMzli6aNc1F6FjdtnbLL9aa1F7MxcRbw3PQQO1lE0cuajzGPNwzNyojLJ46M1AurfTBnc/hznrE0bzZwDbWu/vkISs6Qz6XMyEnxybHN09S561zwjDK6MKluiO6iMnh7RAYvzRyOSowySVdKTJBUl2PuVHL/8jmu3m2acGJ4zEQsC5ONoYrcyBAKKRCDV/ruY7Gx70Zstixkcgx98Egsi9UKOYXoB2LPFxeILjew2K+o7XCt8PUDQMlKfIyBCYAGl4lb0cbZQ69isBqtHOvgr9+6y4cnjKOLCcdoR2hPHZdmG4XvXGz5quTtvbc3hQyMbUyqyCNHCA4rVk5bf+6x7HFp6Os+M0EA/QjKCabFN0MjiS5UEdp0bTlrQamgqtaxwD78CJpKKYZdQ7aI3xKMGZBcOiyRqkkqO28mu0XGs4p8R45DlcVEzezWx9sv7HfqPehiKlCxvj52RwRIdAUgAiEFok8JV5QbPetWKbAoKqnnsNS8j4WhhvRENL+bwNGIytK+GA4aCmkjZ19nSerV8sO+FAUVRpLDlT//gVnIQVH5V/zaOJpEaHJm2ygu7MX2B7+dxS3FI0EpPosAuod9ECWWsFmzGiSocma7Zi7oKG1/Nju+dwMfw0XAtf3BfgoChso5kVmXO9ltmYgYiGbtJpmoE+wRs2T7fUGbhJCEVIGkfmi94kCNhv1Oy5TRWAWZaNjSl/bmU6YEJoNoRmx9ceg4tX4owTJAtf7m/J28UD3UAsRKJNC8VQbYeJk0WbBY9tf5pyOm8+eY6fJtajK/OKu9pICIAUK/4X0G+766EHu7fnRDUIlez3gTtZZiF26XbRaGVfvmJGYXZrinwWqC7oc0hbnIvfQBeEExv7x9qvrkeaA5UvyGQQrGvQ0kkf2VKxckl5UXr9Zc1bwU4+c9p3WoR6rq7n5y87jXPKF+u38v/lx8VLoCOaF5pLtu/0oE58qKUBtTPlUgVaiVQpR6kqMRleaepwvo1GtwL090a/l2vUE+zr6t/KA6xnlM+cQ9LIGrB63NtpGMVE9VOVTWlDVSgVL9KHjppCxCsIQ1f/pfvRn94jrR+dv4LzgjOyGA7EdrjcERrtNY0pIREo8bTp2oO+nZ7QlylXfTvBf9LHvk+Cc3BvgUPNBD3csNz89R6pG70OUPY0/RamQth/OdudY8/3whuYX3xbjXvWtFRQ1GkXySh1JekFyP4Cqerd6zlTmmfdw9c3nPuGx5t/4ShkrNKZBKkEWO5M2nTwdskDJCuV98OnrU95X2mLqkAvNKnY8SDsBNXwsYzaFsZ3F4NvF5KrcS9Zz3Cj0DRkuM8o7RzgGMnc1f7F8xaniZ+qR40HaK95J+LcdHjaJOqQw1SpFNJO2ys6P5LbmM94u2E/qMxC6L8o6MDbMKycwjkJoysDjIeor4K3ZP+eSC+QqrjcjNfcraysWPfjCe9cQ2wrT4c3G2Wr7UCB3LhQvCSf9K+o9Vsja3gvle9jD0xzh+wNdJRUyvCtnI3UlSTyHybTfN+Hc1WvR2d1a/g8gTi7GKTQd8SCIMwvArNIg1hnO8saSzsXo4gmxHi8jixrLGGYmZD2syg7W1dTbzqXLNNlr97UWTCbbII4Z+BhUKRQ/LMv31fvRT8iEx5XSBO4fDjkgmyJVGlEXvyRxOCtHUNeA2zPUEcyrzUjeqfpoF10iKiLiGYITZx6NMfZBadBt1iPUw8qEx5TRR+OT/rQWPx9EHwkXmhX1HLEpjzsoR9rT4NOTzRbLCct/04jnJAQlFxoe7BuIEnUHrgcFFX8p6jmqQ73LMstGyKTEIMQSy+rYR+0/BKoTjBeVEXUMqQnpC2QZVS2QO4lEp0i/0aTNE8jgxZjIzM5O3yL0zAjjFmsaKRqcFaoO8A25EsIg9DDGPYFFbkhl0WLORMmjx7jGo8nt02fk4PipCZ4U2xnZFy0ToQ46DOYQSBtRKXI3nj8/RPtFzcp2xm3CpMCHwnnG6dBo3rjxHgM9D1oSUw9DB58C1f24BGAPeR41LnY6ZkHuRN3Jb8jUxPjBHcNJx/XPF+AJ9PMCNg/zENEPrQb+AL8BiAY0EXUj1zJvPWVCkkRaxnPCOsA4wLHCmsn41wbtogIZEY8UFRCICTAEpgZaDWwcdy3VOg5D1kUqRTfG98BpwBLFns5J3x3zMQN3CrAH7gEe/ML5OQWaGlEuNDzpQp9Du0FFv629ZsDcyqzdcvWlBNcKFAT/+xT6XgJwFNAs/TuMQkxCgsBXvdi98sR41Svs/gAoB5wDe/gT9lkE5h06NKw/UEGvPLe6brslw5zVRe/MAZMEWvxg+Ar+kxbMMEo/OUPyQLO8P7w7xKTY0PGs/+/8c/Qm9IUHdibvO39Cc0F7POy8McWM2Un0TwFO+1TxuvTCCswp6zo2Pjo5ebibu/rKceW2+dP2QOxz7QUFkSM4OaE9Vze9t968kM4a68P4IfQo6cTtPQ3ILNk6rTonNJO4ncM43ej1EPnm7ivrLv9hI6w4HzziNnY0U79O1NDwGPo170Lp6fkHHNY1PDqJNO8vNLvJzEjniPEp6QriB/J5FqYx4jgyM3guX7q9y6nlCPJ26O/g2e0aEUEuhTekMZwr1rfKxsvgje495gTdzuPQA48m3zR+ML4oJy6pwTzaRe/g7YLie+Sj+0gfNDNLNXQtVinjNbXJxt7Z6Ovgndgi4B79mR4yLlgvGCgWJ+U1L8sU4aPpn+MA3DrhYfpKHYQvFDH/KDklbi2NweTUOOWH5BPc4tj25PH++B4iLhouDyVOIZYo7TnTz4zhhOQR3fvVhduA7k8OxCU7LssqSiKqIEcrvzo40Ajf7+JI3IvUtNR64Gb4DRPaJF0osCSBHQAetChZN1/Ma9tn4WPfbNe0097XruV+/b0VGSODJSghVxq+Fscc5itSOg7P8dnP3aLZQdT+0VrW1t+E8iMKBBvtIxkkOx9EGq4XZx5PK8o3v81F13bdOdzh1nnSzNEW1anev+/JBVsUoBzzHl0b7BULEVEStRmKJfUyfjxQ1KfaRNvU2J7UZNE/0ZjWdOHz8bsDSBOFHIceLxy8FgMSYxCyE4QdHCkbNL87W9U72j/a5Nbj04fR0NHa1eHdr+zV/aEN4RjhG8QbsBctEqQQRxIhGYojOy7vNtM77dif2r7YhdR80bXQINO62ZvmifX5BoMTUxlCGu4VRhEbDvMNYhQ8H2orGzUnOo/a2Npu2DnUl9FM0a/UU9wm6Uj7dwpEErETkxHcDPUIhguWFR0hLi2MNb45V9z62j7WJNN+0vPVH+CT8MkCBhBAFKQStAzeB50JtBK/Hxks4zMiNz/bctjG1JvTctY74IzwNgPiDxESrQ7DCKQGgAysGhspxDFGNf3ak9Zt04zTBdr35/r6dgl8DncLCQaeAz0NORyXKTkxjDOH2ADU79Iq2C7lx/nQCaoNEghZAlcDkw8UIfcsXjHB2fnVRNSh167ktfh8CEILIAU1AOQDYRLQI6UsAS/e1jHUi9XX3gzxyQNjCj4G/P/uA6QSCSQ8LEMurtcl1YHXUOJ29SIENAX3/cH5ZwUqGGcmZytRKonVv9XV3Q7wrgG2Bq8AKPtcAkIVlSTvKY0pq9bU1qjeHvDlAIkEgP68+vcBthRoI6snWyYn1pzXzOAG8oQA9ALn+5j55ARwGKAjLCaeIyzWI9hA4QHy9P3P/Bz3fPdWBZsYwCOxJWYjytfe2pLmw/fxAJz9sfbs94AFkBjUIfkiVCDT17La3uP68rb8dvvt9hb2mAA2EwQfBSJPIBwdcNnP3rbrTfdj+pX16/Fd9UwFKBYkHp4fhB3QGvTayOH77ib5yvvW+BH08fcyBeITch3MH/sdBRr82cLdiuVt8On41/ca89zwovfvA5oS7xvYHsocyxksGVHdgeOC7tD3LPsp9+LxkPER+J4E+hBKGSIcpBoKGOYW99vg3lblN+7x9T32jPI38O3vVfWWAZIO0RcPGzsbVhnnFfMVp91L4bPn0+/s9H/3SvV/8ifwIPL8960BGQxmFYAZIhq7F58VQRM1FHLeWuIA6C3uRPOU9Mjy6vC87u7uyvFr+EEBnQppEnEWuRYHFr8U1hF1EdAVYOHp5aTrW/IU9pD3x/en9erxU/DU8CH1avySBCsM1BJ9FdcVZBXxEw8R4Q+oEu4VQOKZ5vDrFvEp9LT1jvXG8hrx2O/p8FH1sfvKA4gL+REXFgYX3RYJFPYQUhBXEW0UShdz5NPoQ+7N8/L2R/a18wvxNO8679/xqvWU/awF1AuUEC4S1RLmEYYPPw/eDkQRrxVd5VXqfe888+v0SvPK8d7vBvAZ8iD2XfybBDcMdBAEEjgRzg2NC9sKUg5aE/AXf+nO7SbxAvOz8Ufvc+0X7S7xRviYAekKsA9UEaQPzwygCwQNIBLTF1nqNvCD8/b0F/J173LuVvAp+B0CygrWD1wQFA52C1oKSw7XEw7ohO1w8ALwQu6q7OfuH/Su/WUIVA4dELgNowo2CjsOkRXw6u7v8vHL7kXsRe1e8wn+zgiJDggOhwoZCW0NWxPSGVHwpfLK78vt3+6v9asAVAu/DUQM6gcJCW0P0BYe7SnwGO4R7FnsXvOr/mMISQucCCoF2gf7D5YXkO2J7uXrOevg7n35CAa9Cj0JCwYsB0EQpRdB757vVO067Xny2P7BCU4Mggk7B+cLYhTJGW/x3u7D7P3txvZjA8EKnQg9BB8GVQ/ZFpHuCe5964brEPLj/VIGnwWRAUEDzAubFCgY4+z96ufqofAH/bQGAAd8Az8EDQx/FJQYZu+A7AzsX/EP/S0GNQc+A0cCzwjhEhAX9O3663nrOfBv+2AFTgdXA1sCqgc8EXgWahd/7Cvrg+0X9RMAbwRmAV3+SADBCdQRSxV+7JrqJOv079D5zALnBMMCEAEbBk8PpBQfFmntq+v663PwxPnUACAC+P66/BsBtArZEUAU3hNL6/PqVe0d9Cj9vwCRAJ/9Tf3bA8IMihJrFMgTo+w47MTuevXX/bACTgJC/7P8jgC1CNUPDxNXE/8R6etL7C3v2/Rl+1b/4/4m/Cz79P03BAQMihBYEvMRsxAK7RbuwvEz+Gr+JAL0ASv/E/1T/RcCCwlRDvAQdxGfELntp+3o7h3ym/Z3/AUANQD//XX7Hvtv/d4BnwhADa8PHRAkDy0O3u2u7sDwuvSD+ZP9Qf8g/9D8tPu/+/j+AQTYCHsMLw61DmwOSw2hDALv5+9l8jz2xvq7/aT+y/28+3H63fqd/coBhgb2Ck0N9w3tDUMNaAz575DwDvKK9PL3IPyD/rv+lv1f/Fz7B/zZ/dMBGAa1CbwLjAxMDB4L3An3CTzxkPL89E34kfvb/V3+Rf3m+1H7Ivs7/XcBvQXeCLUKQAvKCvYJKgnrCIfyzvMm9pD5iPzC/UD9Wfxl+7z7df2mAL4E3QdpCQYKbwmBCL0HPQhL9P/13fgo/LD9rP2a/Ij7b/um/GcAmQRVB8sIDQljCHcHWQf59CD2svjH+3/9E/3/+/b6W/ud/YUBIQVEB6oHZwdCBjIG0vU59/r5kvy3/UD96/uT++r8OgBUBIYGPAe8Bu8FmAXn9l/4x/qP/NH8vPsV+9X7wv7BAlIF/gV4BZIE6gTc94L53/sw/dv8z/vb++/9WwF8BHQF+AQHBD4E0/io+un8p/3Y/PP7nvxM/6ICagR0BHsDOAODBI/6h/yN/Qv9dfwK/ZH/lwIEBPUDNAMzA4QEtft2/dD9//y6/AX+yADnAlwDpAIkAgsDfvsc/dP9X/0O/c/9NQBuAhADigL/Aa8C/gNS/Qr+nf1R/Rj+JgD/AXACCwKzAXICiQPo/Un+6v3G/aH+dADOAfQBcgFXAScCAgNS/nn+G/4j/gT/jgB4AVwB+wAFAcwBEP6s/qL+XP6D/l3/hAASAe0AlAC8AGUB2wHz/uf+xP7q/oT/WQC2AKEAbwCEAO8AOwFN/1H/O/9P/6T/IgBoAGQAPwA9AGwAmACh/7D/q/+w/8v/+P8dACIAGAAOAA8ADgA=`},{name:`dismember_left`,sampleRate:12e3,frames:6480,wav:`UklGRsQyAABXQVZFZm10IBAAAAABAAEA4C4AAMBdAAACABAAZGF0YaAyAACXAFEABgCR/wz/nP5g/jQDyQMSBAAEtQPdA8cETPyA/NX7jPqQ+R/5UvmGCGMJcwm4CIIHkwcoCcP30fj999n1OvSP89PzFvXRDlYPrw7uDGALHQx4DvkQrfS/8/bwju5z7Zrt8O5v8ZcVvhVnFO4RjBBMEfgT3xY98Kjv1Oxg6UPnL+aF5hroqur2G5UczxvWGQoXLhWWFYYYKhwAH9nqYuqY52vjaOCu3vzdvd5N4CTjDeYoJXolgCQfIiEfmxvEGV4aix2kIWEljChXKoPkm+Ny4NvcCdlI1jPUL9Mf09vTdtVE2N/ajd0f38IxvzE9MfIvbC1+KlAnBCWWI8gjNiXxJnMq0S25McU0ODfEOLw5y9lb2arX09Ry0SbOlcsLySXHw8Raw+XBRcGnwJ/AgMBEwQvCpsOrxH7Gk8i2yQ3Mdc00zhDPlM9kR3xHd0euR4lG6kXlRExCOkCGPXE8sDoSOIc22TTOMws2HzcHOY86QDvUPrpCoUVxSPNKLE7LUFtSUFUmVjxX/VcAWYzEI8KHv+m7WraUs1iwN62zqTenuqT0okyiy6EGoluiv6JTpauooqtwrhmzqLhBu5i74L4YvFpg3l/rXp5by1ayUpJNvUgURhNFfEOTRtpLq1KeWP5dnmADYrti9LhJszytIKZgoOCc8Jrgm7edW6BFpaqrqrLyuEq60WPKXoxXeU71RD0+yj7VQ2ZOH1kAYdBkV2aktmyvqKYWn+2bk5thneujk6xXtIi1TLCFXKpRBUhXRDFJb1NzX3Zm42j9ZketiaH/mnOYdZqQokOwPrxQvtVlhlyxUIpJ806eWWpj3Wh7aBOsQZ+emBqWv5lWo1SvDLcqsaNeZFC1RfJM4VksZIhpEmjnpq6aeJX0liOf6qyHuuS29GCcUfNImk4NXRRoDm1QahqmN5t9mNSamaeot1i/RrReX0FRLkzEU+Vinmrpa49mfZ2dlQqUcpidpOKwaLOmpp9TJUc2RoVUmmS0a01sj2XJnUyVvpO0mNql/LJmuO2uFaLjUu5PQlcaYnJqDW7ba3plopsJlFqTHJhuogWw/Lf3s7ekPZkpTBpE/0zhW9JlOmuMa0NnGF2RlhKTDpMil3GfxanrsAOy0qvEoGKYIk66SDpNqVbAYb9qfm7kb/NukmfZYPxW55aflQKWnZmpnoyleK21r6+vO6goo2abdZankkND9EEtRgpPWFdFXU1lymkxbHxtZ2yCahlmW2BLWeZT9ZRvlaqUwJZomMWbd6GNp++rwbHwspu3EbnzuVO337G+rCGn1KEsngyaBJYYlNtGqESrRWhIRUhGSfdNQlPzVVla8V/VYpJlrGl4a5FsEG1bbdds/2saawlpB2aKZWRj+mD8XXxapVYgVA9Q6U9MTDpI65Pdk9OU9pSVlByX35e/mTid058no7Cm4Kr0rxaxDLSHtMm0a7Q/tYuzp7G0sPqvnK/AraKpTahIpD+iM587nLKZy5bSlHNBz0AZRFFF9kiDTgxUhFplYCtkqWfQaQtr9WvqasNp3GbUY+5fEVrsUgVOrkgCRkeXr5gQm92di6LXphqrlrDEt6u6oLc8tA2tw6Zcon6eqJvzmoZIsE3HUjRb5F+3ZNVml2X7YKNcd1VBS3FEnEN0m5ufYqaFsWm5c75IwP62LKugoxKfBEubTRZYw16iYThjCWEEWkZPsEO/QKacKqMWrz25Mb0etyStO6QinwtHn08lWjFhZWNtYP9W2krdQv2fXKa8sDa9br50spCmEqE+oUNQvVxlYuhhF1zrT9xExUOHp3qz4rzxvcmwTqQDobtHIlX1XU5g+FxRUKpEtEWcq3+6hcSxv7+wr6RdohBK/lZsXX9d+lVWRp0/TkbIrrW7l7y+sVyl1aCmpEpRpltcXppYKkz2QfZEcq/7vNfCPLqUrs2lfqbnTsVZ5FyAWc1O5D9RObtF4bEvve67FrFnp6GjYKXwTExW6FnuVLVKLEBuPe9G07Y6wprGf7yVr+moH6jErQlVB1v3WqpVN0wKQ8k9qkREUGO/kcWgv+yzNau6p3upaLC4U9xY0VlzVu9P40XbPZo+y0WPTii+OsOVxIy7vLG4qsynG6dRqiCx71A1Va1WVVQXT15Fhz3oODU5BUCZR/BOf78UxgnGZsLTuvuy0K3fqvCpeKqzrBOys7flvt9W7FdGV8FUVFEdS35FFD+jO707lzwyQVJHLkwjUW1UEchHygbHvcQGwCO827cRtLqwZa2Rqz6rAKsNrEet7a9Usuy0hrnUvXRUF1arVmFWoVXBVJJTY1HGTtxJREfUQ0NApjwgO1U6djkaOQ89jT9DRI9HlkwnUF9S/1TpVq/LfswLy6TK5MUawpW+wboruHq0CbKor3KtlKybqzCrharZqY2q06vtrVOxwLRPuKu898A7xrxXAFncWN1X9FYcVYlSrU+JS9xHqEJwPp08/DlLOww9H0CURD5Ijkw8UB1TX1XrxcLGD8M8v1W6RrXKsXCuEatIqQipMqtori6y6LcYvO3AuFdqWOBWOFMzTkFIHkLmPbw7wj4ZQwZLYVEeVnRY58WhwSm6FrJ1rFGp36gQqqCumbY5v+jHulrqWJhTrEy6Q749ND1/RH5NEFVgWPrCTbuzsrGrXKhxqJSrcrJfvdHCM1loVD9KPz7NOTg/AUqeU3hZy8L7uQ2w76hzpkGpu7FuvDDCgljeUIpE1DoRPdlI41MXWrK/pbWjqrSkJqU3qzW2GsB2WwNVDkk+QJNCRk4OWMtcO774sBqniKOzpp+xc775w9RaW1CjRP9BykxGWddeEV5Fs+ym3KEspDmtXLmgvKhYvEztPi89Y0mTVtpdzF0asjimXaFho5Sr37gSwDC52lNlRfY/4kZQVNddW2DqXNSqHaKooOaj662Qunm/nbiLVQFJTkNZRzNVPV+YYglhsq51o4eeL56DoyatlrfFvCK2Z1fMTMJEcUSGTQNZJGD3YvlhT1sopLydMpxxnZuisqu9tfy6sbmGsVpW4kstRKNCakmWUndbn2FJZIpkJWF8XAGkyZ4unL+bZJ6Mo7CqerQ/uqK92LoftPuseqWTUHdJJkRMRF9GTEz5UzlbxWC6ZOJmFmgTZ/5laWIIX0daCqDzm/uYCpcCl3KX85hcmmKcOqDrpC+qYa8Is4q0Lre2t6C37bP9sECtoamSpl2jlZ/aUyVPeEyaScdHvkZrRZdGDElGS9pNo1FjVCBXill2W6ldr1/eYH5jJmQ0ZVdmVmcbabZpiWrRapVqemt6a8BrjWuvamhqxmjWZwFngmWnY1Bi6GCUXk5ci1vlWDBWoZnQmC+XCZZClaGUdZQGlLeThJNCk72T4ZPXlL2UQpVCl3aYjprdnOaeSKKnpJOo5apSrDSvXbDfscGzCbSHs6G077EzsMmtr6vJqF6kyaALnqtYVFUOURhON0ypS2BNvU+eUwtYLV2EY1xnUWonbJdtX21Hbd5qc2ipYzJepFjilmuUGpMrkiyTB5bumqOh3KdPruez27ZEs3OuaaRnnVtVtE8JTfpLVFDAWtpiQWoBbThtWmuMZ2pg6FeGlICSXJNOmF2ggaphsjO19682pyOdXZcoSy5LRFTPXlxonW2Hbo9rCGO8WGiV65Jel8effqxntWS38q58oIKY1U13To5ZCWVma45r2mYVXSxPoJKZlAyc6acStIO36KvfnU+VJEgwUhtfrGgFbP1nVl26T0iUTJalnYKr0rSOszOobpydlfhLYVVZYsxosWkjYaFSu0Q4lFOZwKdntdy3nqqYn3SXFUfiURpg0WfbaCFltluSTViXSJvLpQyyRLt+tbGlSJrNlSNHjlZcYitp/Gc3YtxXAk6DmUydsqdCtAa7tbXZp8OckJfAQ29OrVoTZSBoKGfxYVtWj0s4SLibFaPirGe2nbmUtWKrc6BBmg6Zu0fLUaFcA2N5ZeVkf2GxWGNO8EbZQ7Gb46DfqEexcbZNufmzGKw1pKWezJummribn0uuUsla42DtYyFlTWPBX25aBVQUTq5Ji0XLRryfgaPAqHivPbMvueK9575KvqK54bJLrFCo46Wvom2gDp/Fntmf8UqUUX5VSVpmXgtgJWNIY6BjiGMRYRNg1F1CWq1Xe1JtT7RMR0ixRxZFxkP/RBVEgUMKRiakxKVRqCSrra0YsX20U7kpvlHBFsMfxDXF8cbbxNjFdsKzvZG76rjatayw1K0hq9+oC6cppWqk5KMXo9ejTaOeok6ikqMLpW6l8ETWR1BKBUyYT1lSglRSVnJX3Fj1Wahae1s/W51bIVplWcpXXlbDVI5QkE1uS65GJkUiQZ4/YzysOxk/F0H6QkWsP7A7tAO4Fb1ewZTFf8byxa7DQ7/su1y4QbO1rp6qwKi1qNSnzKe3qWysr0tgT1tTaVb6V1JYLVgBVWNQWEsgRu1AcD0TPbM/k0R8sTq3mb0iwj7B471ytlCwd6vnqBWo4qdOq3pJUk9JU+tVoVQwUDtJ40GhOIo4K0Bns3m8AcfBybvEk7x5tBOuDqw2rj6zMFFFVoZX3FNISxlAUTYdNylCVrbQwDHE+r4ktgyu+qrnqzWyNVEJVSBVr0+XRRA9WTtZRCS5XcJMxYW8hLLBrDusoLItUQhVnVS5TtJCujZoOARFgbkmwUa9lLM2q8iqpa/fuvtUWFb+Tz1GUD0tQEtJ7L9ywzO9i7Fgqwqr3bBvUddVblQTTTdBWDuTQr5O7cT6wqu4wK0BqhesurPqUlxVdlGVR9g8Hj2+Rr1REcsTyFC8DbE4rP6tX7XHvv1VL1HRRx85YzQjPVdLKlSCxQS99rNfrDWrGK47t/fB51YwVQNOsUEPOg08F0cFUjPF/Mj8voqzJK0oqtmr27KdvJ7DFVcYVGFN4kEPPNE740EkSfRRRFVuwGS7X7MerFioM6fWqWOup7awvWLEBlmQVulRpkqFQjk+Xj/vRFdJUVCrVbdYocT3weC71LPyrFmpFacfpuKm3KherZSykrjVvjTCh8L0WBFXI1RMTp1I+kM9P709MDyFPTpBIEaKTI1RhVWzWGNah1w/XYrEI8P9vlO5FbW+sViuzan8pwan2qROo56jwaNbpL+lMag+qtKrmq+JtOq3qLtMvqm/csJowkDC4MEtXWJcb1tLWkRYOFWuUhhQMU2RSkpI8USuQQZA+T4zPSU9iD2rP5o/iELPRV9HoUrhTshPylLbVTpZtVqlW8ldBF9QYKxhp2GmYcq8WrtsuWC1kbILrsKrtqnopfWjtKF3n12fUJ6bnq+dwJ1fn6igK6MYphup7au2rzy2qbnfu92/Tr7JY49iCWFhXlRaDVeJUk1O+0pWSs9L7U8uUnZWCVtPX8NixmWUZuBl1be3sx2uUKe3o8SeI5wdnCud9aCYphStcbRGubm9/byQZOFfHVhfTwNJ8EYzSnRRQFpUYQlmqWg6aJmzrKktojObUZmImnufBqdLsLe6/7syZXJgdFbaTLZIOExRV5hfrmXKaApne6rCn5qZaJdemJyfiazatqO6VrPjXjxSJ0shTuBX5mHuaNlpS2Y0pKeacpZbl8+ec6uut5G7s7LAXeZQXUpgUQxdMWfPakto7qKRmQiVPZUAnKWpLrffuNGrAFqxTTlLxVZCZOBr7GzUZ8ihPpgLlYOZIKQNsjW3vrDwXvdQeEkeT55bsmdDbM5q/2LUmpuUBZRYmXKkArGLtVisDl2rTm1FBUwRWzxnkWzbawJmCZz5kz2Sq5TcnNunnrIss4iq9560UoZKO00eWJxkLGtabsRtE2hOXkSY4JR0lXWaraS9sJm2qLa7roGjUViCTdtJJUzHVA9gnWdBbA9trGq+ZWhc/FImkhyQwJDDkwOZR6GRqge0Q7fCtvuwsKiYn5eZGFDISvFJeUwaUgRZ1WD2ZqlrGm5+bxRv0G2LayRotmIiXaNWSpfelMKTipMnlE6VqZcLm1qekKHAprWqqa7SsHuz4LW2tfO0irOisByu2qqKp56ju5/5nA2b+JjoloJNvUr3SCdIMkcgR0NGMkaxSGdIPEqkTKpPpFKdVNlWBlmIWo5ci11LX/xfFmEHY1FkHmYbZlNn9mjCaWZq5WrcaihraGqOashq2GqeagBrMGuoaqtqSGvKa3ZrK2tEamNpxmj5aCZormZdZXhk5mOgY2Zi7GANX2xeWV3QW6lZKlZFU3BRuU4vmr+YSphPmBaYQ5gPmImYaZnCmXuaEptRnDGezaCAo16ntKncrJmwC7Spt5W5KLq1vKK7f7qpuCi1l7FYrfqpwaZAo9GfsE0VS1lJxUp1SjhN7lCOVItZsV1lYbZjl2Q7ZURkP2G1XTVYEVLRSnWc8poEm3icF6FApvOrVbJMt1e5qbcVsuuqmqNNn+9D1kI+SBNPz1ZmXcphaGNvYsheyld+TnRGFZ9noYqn6bCYuVW/bb9otsesnKRaoOFB50dsUo9bJmBhYaBe6FYDTA5BFZ+qokGqp7QPvOi6rrK0qHqjOUOISfFTgFzpYAVgp1nITmlEJaIVp02xQb6Rwyu9b7HNp7ei+kTRT1JaAV+hXSJXx0sTQdKk8aqatfHA68GguPes8qVBpctJplXcXL9dR1nUT/pDtT++qAGzs722wI+5f66RpmOksENeT+dXtltiWa5QVkSbPFSmkKygti/Bwr5Otdasv6bApcVFy1HvWe1adVh+ULFEmzxhPvqrgbVCv8XDp704tNyrsKdJqdRIb1IvWFZa1Vj7U9ZKAUJOOUQ+xq3JtMy+KcZaxPu8oLRyroqpCaljrIpMkVJiVkxYdFfkU21OSUU4P944VjxPQyeyn7ctvSDCysE4vbm3mrGrrNypUanMqsyrl68PTD9SzVR7V6xXCFb7Ua1OpkrxRJ88uDjlNvA4Dz3UQ8ZH0LQXu/TBScZ/xxbFTcTOvsi6qLdds46vLa0zq+mqNallqgmrGKz+q7quGrHcSlBNV1FrUoZUDFZEV1JX61eUV55XglYFVSpUgFGGUL5NK0puRohDhUBcP/k71DtVOu86ojrwO+s8pj+qQZ9DKkbkR+G0SLajuF67fLz9vmDBi8P9xV3FYccIxWnDfsMNwkPAt7z9uQi4/LV3tYWzqbEOr3Ctj6yFrKisAq2VrH6tFa3jrqavKLKss5W22k4KUSlS0lSBVl5YCViHVydWtVQkU+RQxE3yR4ZEFkFmP789hzwNPao/XkKCRyVMq1DEvLzB7MRWyN7G5sXGwZ+68rOHr3GrUKkMqPCpJav6rt6zZFL+VsBZLVtLWulWc1K3S5hGC0A8PYdAHkbZTXO6xMPuyWHHs8Xpu8Sw8ar+p/inKKrerja14FXrV/hYZVQSTalDeD8SQHtJDVIkwILDDcKvusCyOas+qLWom65at5NbuF3iWixTE0iEP84/VUaiUTi8z8AYu8+wsqhWpIalF6weuN1bJFt6VmdLBj+bPgBJIlUovpG/5LWUqeSjd6MxqTqz0lvUXJJXs00rQwdCV00AWe2/EL+zsJ+mKqLXpeWw1L3mYF5eUFVJS79FKU28ViVeVr9htbOplaJ+ogOpqbbUwl9g61ibSro/lkNzUPNbOV91tXKo+J/jnUyhXKvEtwS+rl4DVXFKcUXRSFRU211aYXm3BKtdoTScjJzaoiuuRbfFYNRa7VDbRDpBe0ZrU8ddq2JfY1Sx56Z1n3ec+Z3cot2r+7UJuAdh9FkUTyxFTkB0RxRR+FuwZLBmsWWms9Go4aFlnNebep0voXiqVrXJvVXD2L5mZeBeK1czTh5KNUpuTUdStlmyX7li42RSZkRkyaxLp5qhdJ19mk6Zfpn8mnWdPaItp5askrBvs7m3iLbhssuukl9XWqhVuFEgTnhIykVfRRJHMUrvTyVUc1boWU9feWOiZnFpPWr3alhsbW3sbZps12pMatVo+7Hir6Wt3ayxphalYKQon6qbwpncmD6XM5YnlzyXZ5fTmCWayZoImF+Y6JYrl++XW5gRmJaZSptvn1Ch/6QEpv6mIKiaqsKr2KsUreqsp64AtJq0zLXft9W4u7kStxy4GbaVseawCK+6rVGrbamnqdBkRmMYY4RhQV9FXmJcrVtcWGVW51UaVFJSPFHpTTpNokrZS5xLuEpZTKBNElIVVyZa0128YUpl4miDad1rX21vbYpuPm+6bZ9s5GltqimlYJ/4mv6WbpRAlCaT2ZPElVmXr5tZoD6lAKulsIyzRbNHsP6tPqeHYFtZ1lL+SQtGXEczSwZTkFvzYyhqN218bgduXGvLZh+hmZnEk+iRrpHrlCSaK6E1quKwIrQhs5mp0V7lVDlOzUv1TupWkGA4Z2Jsnm0kawBjrJvVlNySPpRimjOjBKzvseiyjKlLn7tQu0VmRZRNHVzaZrFrdWzJaFxdCpl1lfOXtp+Iq9C4l71EtbGpoFgtT9ZOxFYnYhpqX2zdaqpjlJ7/lyiYK55qqjy27LzYtJymI5uDSZVIS1PWYLNoI2u2Zv5dnZv5l0+adqEerp+73bvcsdKkoJpDRwBMrlnvYxhpT2pYZ6ZdD1J5mSeaY6E2rd+4R7sqsqmmTZ1HRoNGWE42WWxiv2WyZUtfslVwS3uY0pl/n+SoZ7V2vLe7KLKlpk2eM5lHQoJIclJfXT1kzmblZt9iN1wSVLBL6psmnAqgqKd2r7q3bbwZvEW3sq4ipw2h4529R65KTE/pVfBbOmGoZClmVmWEYzFfUVsIVQJOHEkaRpiehaBNowWoKKxWsmG5O70ywHK/SL78uty2MLJ8rQGpcqW3ohmh+58Mn1hCoEUgR3lKEU6HUTJW21dCW8Ndvl7nX1pgZGGvYYpgw2CfX6Bfsl4hXVRcfFqrWFxXMlW3U15Pw02/Ta1L70lgR45FvEPtQZ5C+kEiQA1BA0FvQJVAqaNepNykVaQopYyk56TIpomnqqeHqJiokKk2quKqHavNqyesR61Pro2vd7BhsaGyObWItsC38bgiuXq6CLwovAq+/7zvvFm+dL4qwK/APcE2wiDC4cOMw8bBHMCBvhu9Q7yXuhS55Lfptfe1h7WxtLCz6rEAsZuvJK1QrPiq9qnnqAqo3KeLp2ynMz62QFJD8kbcSfVMZk9JUmlViVepWDZZQVnpWHpYjldPVTtSf00uSXVE4z9KPW876DoqqtCsYbE6tii84ML0xrvIScmbxfu+J7lksletDapaqfipwj+kRatMlVG+VQBYrleWVS5RCEv1QyI+JzsvPqOvjrYkvoHFc8fZw4a7YbTLrTaqBKp7P+9Hyk/KVK1WFFQ8T3RG6D1JOf48l7G+ul7Eucm2xAK7oLHqrDus+0PCTMpTW1asVEVPfUWOPOM7L0OmuBTC6sawwn24sa+Dq0GsZUZ7UJxVOla/Uc5IVj/qPB1EXrmHwqzGor46tFmtVas7r5FNnVTxVhhUkksKQJE5WT84tfC/XMZbwPWzqKxoqiGt4ksWVDNWjlNnStI+XjiAPP9H7Lw+wlvAg7bVrumqtKt+shdTJlf4VmFRHkdcPXQ8+kRaT2XC2MbBwHW2f61Tqdapc64+tyBV8laCVGJNIETJPNo9SEV8TnLAcsZhxKO747LurASq2avdsFq401WfWJtYdVR1THxEujxOPJlCJUwcU3HBa8UHxWq/Vbc5r9mqEanjqNirYbFpuH9WgFl2WttYclRpUA5JIEOnPy5AWkKKRxxNlFMaV3jCJ8TEw/m/Ubv9tG2vM6rUpy6mpKVFpWmmqqjVq7uvibSLuNG8mVn+Wg9bQFrNWMxWwFTgUZdNoUuLR6RDB0LKQZVDaEOORCxIS0qUTVZRt1NHVpFXC1kLW8dbdb4qv1W/1b/uv6e+AL5xuhO4sLVgszWwMa26qauo/6Z7pYykHaNIorSiz6KSolShOKHDojijg6R1paCm8adUqVyrlayFrA6uF68SsZayjLSxt3G6+V95Yd1iRmN6Y+Ri0WA1YNBeGl0kWipXEVXWUA9NREuaR9dDWkMlQrs+xz6oQZhDzEcMTfRP31M7WHZcv14RYupjxWTXvNa/77t6t0SznK+ZqUqmc6JPn3Scops9m5Cb2px1n7eh0qWQqxixcraiuvJl7mOtYX9dh1lUUzFN90aOREBGQkycUnRbIWGEZDtm/LsOuJixZan7oRSdfJnXmQWdfaLGqZqyJrngZ7tlrGAJWbVOT0b1Q3VHZVGfXBlkJmjctRmuYKTmnLqZsZoun0Sn2rLCvHi/Wmh6YDdWikuAR7VQWl3GZkFqSbqdsTykaputmX2bAaNarme5trZFZMdYxkyxREtNpVnwZflqerTIp5iaKJWMlNuYCKbFsU61gmU4Wc9MjESuTxdfVmetaoKtwKFtmCuT45SRnHSovbVss7ljsFMBR6ZJXldMZChr02n8pWSbYJRxkkKXp6CtrvixPK3IXepKckLxSeZXd2VZafhqi6Q0mX6ThZUTmy+o1bdHv9e17GSDWkJTkFC/WpBkL21IcctvOrBooaWX7pV/mLCheqxsszW1kqzvX8BTQ0eORFtLUVRoYd1ni2qxatNlcZ2llXyRuZGMkxqaEqISqxWzxrVesWOoP17eU5NLg0j0SQFP/VU7YLlncGsibkBtf2vuZuJglJ2cl8iVpZRMlEyWiJmRnjGmCaugrwSwfrJetFCv96rPpuGfFlv9VstShU2FS/NNYU5nTg5RjVL5VjFb3V0LYSdksmdwadFrMWyJbaNtT2+ebt1tXGyparpp5We5ZWpkHGCvXHlZyJuPm8WZHZkdmHKYEpjWmK+XwJYNl1CXrJjKmOyWWZdIlraXp5jUmCiYWJcNl4CXspcRmeSYnplfmRuZ05hhmb2aSJv8mWea9pmimsacap6Znped3J1hnx6gjqBCoKCgqqENo8WjjaQ4p9KmwqcNqq6poKmNq4yuMK7FraSvwa8OsaqyG7TwssqzxLftuP28NcARv8bAbMFZwwG/S77Ovra7v7ioucK3NLfctDS0+q6nquqnD1h4UuBPkErOSOJGIUiKRyVIvkpbTP9NxlHqUylYOVzTXmRhvmKfZLdkYWQdY4Nh8F40XNBXT1Lsob2fNJ58nrmePqBHozSnC63Ysp+5pL6ewFG+J7dgr4CoIKPTReNCOEHTQq9J3VEhWddeRmJUYrFfdFy9VixPzqJZoZKhvKQnq22zVbu6vme+1bZ9rbWmEEWwQhVHtE0LV7FcF2D3YNldKleYTZGiQKJPpIqrhbXIvYLDpb7/tAKrYURBQlNGOlC6WThe2F8oXUNXeU7apbml6ajwscO8kcMWwZ+2iKxzpac8zkL5TTJX5VurW5lXtk7OQ2ukbaYfrM62JMLKxZW+dbLoqiBC00NnTCxVSVoWXLhZUlOwSDVA5qZkqsmyVL2+xErCKLkury2p+D1rQoFLOVUCWvdaYlj6Uf5Hcz3+pYen56s8s3q8BMHzvVS3PK95qTOnQz1sRbVNb1PiV5pYj1byUa5LNENQPIGopqvCsDK2cL46w/XBNr2at5Sx+aynqjeq6D4oRCxLflANVdpWIlfyVRVT606WScBEjD+FOhs61aqorO6wZ7U3ug6/+MJoxvDFJsNpvo+5G7YIsuSu26xdq5uq3qq4PppAhURKSIFKKU7vUI1TJlVnVmxXU1ctVxlXrlYGVWVTgVF3TilMbkkJRwFEwEC9P4A9VTzFPMg7hjrjOUU7Fz0JrDOt0q3zrQGviLCYsWazAbRUteu1/7eGug+8Vb2Zv96/0MHVwgvFU8XtxKfElsXyxAXFmMRgxlHFcMSOxc7EHcUtxSfFtMSXw4jBiMDevbu9/btQu926G7r4uLS5zbhSuZO4s7cEtwW1hLP+sUywEK8arv2sQKx4q/WqWar1qeKpOaqxqaqp8KkKqoWqXz5nQVVDrET5RxNKakv8TeRPv1H3UjpUvFXZVfJW9leDV3tXtFbsVY1UmFGUTlZLBEfcQ0o/HzwFOA44CjuzqYmrNa7CsT+2nLouwAfC48ODwjC+Obp4tPWvrKv6qPinsKZHqJ1DbkmjT5BUklgCWgNa31gcVXFPYEhEQZk8KDyMQNurv7KEuaLAusIAwU+6JLK+q8mnC6ZfqBNL61JfWJVazlnTVcJOaEWyPJI6skGXrNa19r/AwcC7mrFNqRal0KSgp0lRvliMXGtbglXiSn5AzjwARbut2LeVvue9PrUnq/ak1KN3pzBUmlwZXxhcmFMHR4U/2UMRrJ+4B8DzvZGyYadDorui/Kj/WHNez17hWOZNRUNEQo9MtrOhvYC/arShqB6iqKFIp0hXOV7dXrhYYE4IQ/M+e0ibr1q7EsB3uV+tbaQDonqlGq54XbBgKF75VURJG0DwQdhOZLQYvo697bTBqKyi8KAoo1iqjVsTYPFfC1rRUDBFM0AlSO5RIltXvbq92reIrbukaKCbnxWkyauFtdtgtGCdXTVXpU6hRptCs0UkT3lYzrhav7S+4re7rienTqLIn0if7KKFp/CvULlXYm9i/2AQXXVYzlDBSmlF00MeR/hO0VVjXEtgZcFiw4HCKb4DuKauq6e2orSfyJ2AnUKeYaBVo4Wo/66StCS5f77jY2RkS2NEYjBh6F4rXOBYSlRWUBZM9UmlRqBD40JkQ9JFhEjPSQdMkE+ZU+hW1lkTXIJd+14XYCe+ksD1vzrAI74Rvjm+wLzOvOe5GreKtgGzBbKNr/Ktfqz/qeOoLKjWpf2kfaMqozShQaCrn+CeCp7inYadj569noifFqCCoLOg+KAJos+jZ6T/pPCl56f1qcirpq3Nr/Gw5bENswi2s7hvuDW6zLwLYLVgeGBQYMBfJV/0X7VgFV8gXh9dKl0dXDRb+FfFVUdRBk9kTClJTUWSQ/k/FkGvP4pAUUKoQvJEgUh+TKVQY1SLV2Fa1FwxwEnE6MOywkXAJbuztrey3a7JqiqnaqWopKukVKZBqbSsCrBmtVK7ncD2xHNfk17xW8ZXBVR1TYFIikJTP9BAkUTqSo9RUlZ3W0BdxseTxMG9n7Wtrhur5aiHqLaqUK+nuI7A5cSGXDJabVUSTv1GwUCKPLBCtEvSUzpZ3sKTwvG4krCHqRumJ6b3qMCtn7iiv3lYflSHTrxDbD1IPShEJUt5UhBY8sTwvuu1aq0wqWapIK4at4G/EMZmVZtP00VZPQA61kBbS5JTTFacwzW5Ka94qc+oDKyosje7uMJSU49NQ0Z+O8M5SUAPSqdRb1THxG29uLKWri2tkLCduBjE6MzqyQhQiUfTPWo7Qz77RkxOC1K3UnTCK7o9tI+wT7J6t27AMss7z4fLYE4qRt4+DDq8PApFikwHUbZSFFEfxCO7WLQhsUOxZ7MfuWjAu8h7zNjHn0opRjY+kDdENk85xj4LRddKKE7ATgpOeEvuvzm6u7WDs3yzuLMitlG5uL1uwqbFzcdgx2LDKERmP5Q6ezWJMq4x5zLkM904iD1mQvRFiUh8SrZKBUtWSyFKj0jyxuzE+cFPvgC9DLteuRW5Orn0uSm6frv0vPK+mcDgwu3EgMYAyCXKo8tRzfLN+s7t0M3SN9OE0jbST9OF0mnSJ9KCz0jNp8zcQnxCkT+RPrM8FztUOoI5xjhOOMA2eTeDNtI1lzYrNmg16DTaNAU0WzSwNeY2CDZWNb40oDNLMwQ0rTM6Mw80DzO5NFA1iTURNRQ0+TSnNKAzijOqM/Uz5jSRNBY1+zOUM7MymjPVMvoxzjGCMWMwCi8fLSouDC9FLkwu2yx8Ldot8C1nL/QsLiv6K2MszSwwLCstYS2NK+srGSrjKOUnjyiUKcsnyCf1KZ4p9ypiLGUsQizFLaou8jDtMEAyljR6NSY3LTi9OIU6WTpkO308bDy4PIE8wDxNPD070TpNOtDSvdCUzZvL8MmKyKTHSceixsbHesiAygzNm88g0srV9dix24Tbi9rd1/nUTTSmMM8sEyk2JuMkciZrKeEsQDFwNH82WzewN5M2OzSZzsXKJcl7yK3IKcqVzPrQxdTu2CDbw9da1BAw5SuBKHwnyiptLhE0zzcZOTY56TaZMzjP+MsYy3LL5M3A0gfY69s+23XXVy+/Ka4lKyZzK/YvnjRiNSU0VzEIzsTKa8l+ygzOUtJI2APa19dT0tso9iMnIt8nOC+iMzQ1HTQdMIPOqssTy0fNg9GR2Dfdydtm1pLQ2Cb7I38lZitpMG0zWTO7MAorEczDyo7Lz86a1I7ZZNtq2D/S4c2dImwiLCeVLMAxADRxM1wxeSyUzjvN9c3D0JDVqdvO3wTeydeu0SrOPSOWJOgoni28Md0zpjTcMycxEC1tKM7N983nz+DThdkV3jPgpt9m28/WPdJ8z1wl1SSFJqAqEC7/MAAzADQPM7MxMC9BLKgoAM7azefNB8/B0fPUZthN2zneq98M3gjbN9ih1NrRwc81ztwiSSJrIsgkRSepKlstwi8FMgkz7DOTNEY09DNCMy0yLzEyL+wtXSw2KvQo5iasJJnN/Mw3zfDME80lzvnO/c8q0RPS29Nk1UXXbtjY2HrZzNrU28zdm93s3CHchdzB2yzcT9vB2iPZ/Njs2G7ZVNgE2OLWRda71ZXVwtQX1HHTLtOi0vnRPdHL0CvQic8az37OEc7nzcTNx81+IrMityL3IkMjTCMDI18jWCSpJHUlIyZCJkcnRCjxKAsqOCpUKzssHC3fLawuyi+8MNMwXDHOMb8xEzJwMm8ypTLEMlMzXzMiMyIz4TLAMmEyYjGcMO0ubS2ZK94pjSg6JhkkZiKxIHTNvs1+zkDPFdC40WrTXNUH2GLaV9y73ajecN6T3Sjc+NmU1wjV/NLv0NDPaM+LIxMk0yUCKOoqky3xL3ox7jEeMrcxmTDALhks7SiSJXIjgM+30O3SYdbX2SrcQt1d3fba1tZC03nQBc/iIEck2SgOLQEwqzGWMd0v1ivMJ80i+B88z2/RcNV92k3eHd8E21PWWtI20C3Q2yanK6ovxDH4McAvtCv/JZMiotDK0w/Zbt4Q4BbdRNep0pDQNdEnKEwtdTAcMZUvUiuBJb4hV9H41CnbqN/y31TaztQ+0e/QeCUwKzsvVzC2LgIqHiTcIfcjxNYm3Rzhp99z2WjUmtG20VwmMiwmL0QvpCyaJx4i9yBIJQPZB9/44P3dINjI00DS3NJWJqQrNi6bLv4rPidcIekeVCLB14Ldx+Cm30bbPdZY04HS29OSJvAqfi1uLTsrlCcTI9gfTCBIJO7amd894qHhyN1B2VLWtNS61HfWJie5KmMspiyRK8MoSyXeIRQf2R6VIQkm2d3c4bvkb+XZ4nDfwdum2JjWJdbL1iDYV9pcJxwpeSomKyIrCSoAKPclSyLtH0cezhyeHCAeqR89IhwkM95T4Bziq+NE5EvjWeJ64A7fFt2L21ra4tho2BfYO9id2P3Y0dl12lPb+Nzu3Uff3iWAJvUmMyebJ7YnwSfWJ8YndycXJ3wmEiaSJQklbiTDI/siNSLoIdAhmCHqIDsgrh/gHvodzR3CHHQbpBo7GjEazxkKGZgZZhl4GRMaQRphGjsaaRqnGlkaOxp+Glka8Bl7GY8Znxn7GVYatBq4GswaJBuSHLwc5xxRHQwdJx0GHWgdmB0THYsdzB3YHWUegB4YHzkfcR+RH6IfyR8W5dPkkOWk5UDmduZ25yfoJ+l36ZTq4epe6y7s2uyH7C7s5evE6/rqPOp+6VDohueY5s7lheS444jjxuIi4rDhd+Fc4ULhb+GR4anhLeLy4nHjCuTQ5MflCecVHZUdFR5LHm8eWR4JHr0d5xzAG34auxjtFsoVxBShFPYUCBbAF9gZWhtp62btyu4u77rtp+vT6cLnW+Y05fDkKeXU5QHnLun46qMbQRxjHGgbshmJF1AVGhMnEm0SwBP3FZMYgOyX7RLuh+yC6pboQufL5uHmIOgT6jrs6hkYGnYZ3heuFRITVxH2EfkTnxaOGM7vjvD67oTsE+rW6J/oaOlS6/rtwxjsGCEYXha3E4IRUxFqEukU7RZL8Jbwr+5x7DvqEukf6fTpzusk7hcXOxcUFhUUpxFkEN0QBBPTFCkWBvEd8J/uAuyC6v/pf+op7C/uRvD/FU8VWRRZEjQQig6YDgQRKhOKFAIV0fCV76ztl+wJ7Lnrbuzj7TvwF/I88kIUzBLyEBoPFw2nDH0NFQ9zES0TEhQ7FPvyzPHt78nuQu4D7iXu7u7z71LxAfNq9A30RhOREncR4Q9fDkENsQyxDP4N7A4lEHgRTRLTEgETzxLT9Knz6vL18fTwavCm7z3vDe/m7iLveO/g73HwCfEc8ufy1vN99AL1DPUj9SMRghAiENQPRg+sDgwOjg0eDf0M9gylDK0M6AyyDJcMvAyZDKwMDw0uDRsN+wwrDUANVQ17DZkN4A3lDRkONA5/DrsO3g7fDuAOuQ7UDnwOmg5vDlsOFQ4xDjgOcw6BDrMOtA6jDrAO2Q7BDrIORg4zDkIOBQ4rDu4N6Q2xDaEN0w2eDWcNdw1SDUgN/AwXDQINyQztDPEM6QzoDMgMrQzWDJkMgwxtDFMMMAw/DDcMZAwADAIMAgzwCyoMNgwODDYMGQwrDEMMWAxKDPUL4Au2C80LqwvRC98L9gsrDEgMZgyMDF8MlgybDKcMngyFDHoMbQxMDHQMOQwmDPYLR/gZ+NT3i/cb96f2ofZI9g/20fV+9WD1VPWL9Zb1k/Wx9e71VPbS9lr32fdC+L/4FvlJ+Xj5avmK+UD5VgrWCSEJnghQCNYHsQdjB5cHNwjcCIsJBApOCm4KWgpCCpD5xfgg+KP3RfcR9w/3Pfem9x341/ho+Zn5dfkg+WEIjgfQBsIGwQYwB/QHswgRCS4JDwmkCPf4cvgv+DD4WfjZ+Kf5lPr4+tP6IwhqB5gGRAZyBhMHywdNCGgIPgjCB6X5MPka+VX50fmv+pj7/vt++zsHZgaXBXoFOQb+BnYHmwdlB+AGSPoE+hP6dPou+w/8Zfwc/E77vQUTBdIEXAX6BXQGmQZgBuAFvfp1+oL6xvpe+wP8ePxS/Ln7+gRyBHYE7ASBBeoFCAbrBaEFFPy3+6n72Ps8/Nb8Pv1W/f78ZvwjBJgDmgPyA18EwQT1BPIEywRfBKX8ePx9/KP88fxe/c/9CP72/a/9Q/1QAwYDBQMeA0wDowPVA+ED4QO9A4IDMwPiAp/9nf22/d39F/5a/qL+xP7J/rj+kP53/l7+S/4UAgQCDQInAj8CWwJiAmkCbQJkAlUCQgIuAg4C7QHOAbEBQP9J/1X/ZP91/4r/nv+0/8r/4f/3/wcAFgAjACsANQA8AEMASgBRAFwAZQBwAHwAiQA=`},{name:`dismember_right`,sampleRate:12e3,frames:6960,wav:`UklGRoQ2AABXQVZFZm10IBAAAAABAAEA4C4AAMBdAAACABAAZGF0YWA2AADs/0MAogD3ADoBXQFHAcAA1/+x/pL9uPwN/I77GvvQ+rH78fwh/vL+Uv9i/5H/tf+FADUCUQTzBoMJ6QoTC9MKzQkICJsELAC9+/v3Q/WI84vyRfL18Wvzz/bn+dT7x/xV/Wf9rv1//hIBwAQDCn0PwxOvFVEVZxTrEYAN5wdRAAv4QPIl7qzrb+rt6czp/OhA7U/y5PVX98T4gfgs+V/6aP1jAeIHxg9RGOYdNSIbIs4gSR6IGfsRhwiq/Zr0He3F54bkjOJW4kHizOGd4Xbnlexd8czzJvXG8yL0BPVh+Gv82wQZDpIXoSAZKfQu6DDlL8wtRSn2IYwZgQ7xAlz2Luy34/HfMN2l2yXattkl2KzWH9rA4XPlrOeD6D7oF+gk6c7qYu2Z8CL4QQGiDVYbtiaZMK04wz5FQ8RBrT8kPG02by+sI+QXuglZ+xrwBeeC37TZEtYr1WvT09MS02fTEtKC0C7OGtOY2BjdUt5G4K3eBt9q3Kndp94i4OTiSOgc8F774QV+FLMihzABPQRHrE4sVetY5VzoW21Z5FWTUIhKGkM5OB8uLCAcEu8BzvSs52vd7dQ80OvM2ccExmPF58M1xtzFO8WXw4fBKsLnvt2897qSun6+gcPdxbjJEcv5y7jK4MmGzRjOos6X0ODPR9S413HdouJ55j3snPbp/vkIJg+/GVckxi+lOc1Ef0tOUotYLl5TYn9lJWj5acVqIGnmZ+9kfmG8XaBZ51G0SxxGnDx1My8rCiHbF04K8wJz/GvxHe585aDgvtyF1aHTGNJUzFvKjsc9xq7CqMFFxV3FNMZWybLKIshSxgHHHscUxIvCtL3FuDS1wbgXuzy7Jruouq25Ybsguym/pcBmwVjEjcVzxtPJ6shxxo/F88Xcy6vNANCg2JPbNuSa82376gWyDXEaTiXtLcY5J0XfSthQ4FfIXO5gd2X8aN5qtWtabIJta2shaJJjsF1LV0FRAkgfQwo7fi+PJGodNxV2CVb+/vZg76fniuIG4Njb7NlA2PPUSNX/1LHV1tTq0orPmMvGywbM88Ypvze5ILQetAW2trrKu129QL/Ewt3DVMOOwrrH68g0zkrPxNYQ4BnqA/flCCMX0CWmMRBEXFDgWNJgZWbtahRt7m7+bnVovV83VuBK8zqcKpgYyQvE+djsBOWX3DfVHdPx0EXTRtKi1sTWvdKrzu3LF8ZjvVOzkrV0tjG3+bgHu66577q+vQDI0tdI5db4QQirHfszkETuU/5aLmDGZTZoimp3YsRU80MVMAUecAjn9nTsHuLi3PvdWOPA5BrlKear4tjZW8riuia3nrfztUu2bre9vJXExNMY5nb9pxVyL6lEUla0XjplDGkba09hR1E5PREnlw2l+7ztOOjd5jjpsOqK6QThGta0xx6496xUqmirnK7/sU+86svj4/z8sh7IOqtO0VllYQFlGmfHW7JKpDCUFu0CtfaQ8MnwKu2z7Rztr+Wv1Fu6rqbEppGlR6hTrqC10cXi5IEMBS44Q71TH1zUYK9kQVt9RFMr/BTLB8n/1fuW91X0AO/56YLYI8OwrSCpiavUrtq1hsab3WsADCBGOfNJoVN9WmNbZFnuRWYswRQiB9MB1PyB/TgA1/cQ6njZwsSArV2q561YskG5Ns0U6B4GjyNuPfJKXFGyVE9YGUxpOQglABXODT4KOw3dCacBK/Kf2QjD0q70pOKmJqsMtFDDE9/d/2Qc2TNdQilKOE7dUPJO3TsiKV4Z2hDyCigKQgdPAvf0xN/Iyf20KKabp5qs3bbHxR/g+ABGHp84CEhOTwtSpE+pTglH4TS1JM8VnA6mD0sQihBoB6v6IOYPz8K7xKmjpWeoHK/BvQHQ8+fgBPgdmTNNPXFBxkMKQ71C7zwjL4sdThSsFTUXhBb3EYEN4wFw7drbS8UGs+mmXaWiqUKvJLy2zt3lyQBqFsUpCTcsPcZA7UAzPwRBaT35MP4oBSBBHdsZLhniFsgQ1Ahy+7/poteIxYW3sazepuGoBa4lt6LC8NJ56ZP/iRMlI08v1TXVOZY6UzojOD45wjqhM08siSYhIUkeNR1wH9wfxByaFtkLlf6F7bbcxs1OvsayPKvspQSo1qqIsMK5lsUR1JTljvsZD6oeKSvBMYI2tzfAOfc4MTdkN3g44jm6Njct2ClvJ+8m8CiZKkcsmyz+KFAofiEoFn4KgvlM6jTYUsyiwgq3cq+/qtOmjKc4qhyuNLLeuBrEQdI84MzxMv+vDIwX5yHpJogpjStELj0t7C2/LegurS3CL/oxsjUSOSE4GzJzLscsIiwMLIkryyj1KA4qCCwLKYQl+SDSGy8VAg4KA8b5re3L4mvW2ctswwS7xrSCsJyslKnXpyWpZawKrwe1MLutwgXJQdOX2x7kn+59+F0BPAl2EYYZBh3uIo8oxSmeLAUvMzH+L4gvETHVL/AsEi5MLlcwujKfNRU3oDN3MYExmjDAMOouwy6+MCYzNjGZM1IzATM2MHstsymmI6wcnhdjC4MBlfZA7cDh5dYVzR/DtLoztgmxuawAqRqnzKVeppqpTq3XsSS4X77qx4fQodkW5QvvJ/ozA3MLaRP0GuEfPSN0JeIolysFLB4tuSyyKrYq2ysgLtstjzJ+Ncs5y0CgPuU8QTkuN/A4yjWsNWkzLTOwMZouoy3TKdQhRRqEEacIe/vn7CXfLtLCxYe7GLRCrSeoTaRXoUehOaXXqvSyMLs0xd/Rl97V7hT8GwcqEboZ7CC6JkcppSr3KVEqKivPLIgtvi94NNM7tEAbP688OjucO6k8FTwXPMg4rDLoKDseUxAT/JXnHNU/xDu2E6uYpDugup34oBmo4bI7wEvSneQy9hQF5Q5iFvodACEeIyMkJifpKVouCDjfQbNJCEg8RttF9UUORE5AfzhkLSIbiwJJ69fSXbw4rdWjZZ0emuydu6k1uK3M9eZ5/AkOlxpeIEQi9SC/IbAkeipANzpFnk74TDxMikzGSvZDDDYeIfwD7+SiyFy0laZKnnOa/Jpkp528ndY98tsG9BMyGk4Z+xlUHuwiZi8XPrxQv1BCUAJPlUnAPT8sDg6c7fXMorQlpiaey5qAoB60Rc7s6boCYw6iEqMVEBf5GiIlMDcTTSlWDleLVaROWUGEKaoHguFuwAKsAqDDmgiZ4aoTxYngl/c3BS4Lgw3aEAwc5S71RahcP18LXtRYZExcNuQQpuY3xKau16F7na6dObVE0j7uR/7/BO8EqgdKD7od+zdcUrNdBV3mWJlLQTCsCNXesLvQqLWfmpzVnXy1WNZJ7YT5L//PACoIRxbVLMpKrGEfZEVhrVY6Q7cdEvAky5GxdaW9obGgOrKgzdXkCPIn84zz9/lqBeEdzz+3XD5lamEPWLlDbB9K9IfOcrVBqbCmjqdlsnbQh+XR79XzVPJk9akCWBrRPORZkWgTZiZb30aYJP353tQdvdyw+6pUqkWuqsho327qxev868Tuu/wtE2ozk1S7aUNqX2R5Vjs7bBNa7l3OabrfsAGv7K9ctJ7KINr13pLhS+G76Kf3ORB2MrNSYmcja5ZkfFb6PKMZvPIw0u+9ObXHseywQ7EQvabN89Yy2MLZhtpK4wnzrg0vLyBPtGK6a3hlWFktRDclrf9o3XDG57vptSa0ObTZsYy8bMt21vfYvNdX2u3gw/BmCUAlykJAWmFoDWziZc9a1UaSKW4Fkul4z5jBL7zGuYm41Ldksja8c8NDyb/MYMwNzvzV9eCN9BUP/S/cSb5b5Wamap1kZFpWSiky7BEW9t7fotFZyMTCYsI7w4TB6L+QumDG3cpmzC3PR8wPzBPRs9fW4ov03xA4LS1DCVUpYUFo32XeXgtUbkFqK2sSoPlj5HHXA84KzLbI/sgmx+bFS8V+v6a+L8Omxt/Ip8ckx+jH0sjd0GzbjOpc/BUVPCz1Qd1QWFwTYvZlvmMDXqtU/UheOvcmqBFO/bfubeLD2gDWGtO4zkTQHc/ezUPMNMghvpi9G8Nxx4HG8MjvyAjIRsmN0K7YluIL7r8ADw9CIsQ14kPZT/9WBFwAYBhi7V56WetQf0dtOYor9Bx5C0D6PfHn6Vni3NwK2UjVStTA07vWYtKCzsbM5ch+wrXF/MakyaTJUcxeyzzKhMkCy9nNmtdA4fvsAvpgCbEaJSzyPO9IdU9MVQhZWFuIXRRcOVfzTm1FIDn6KwcdJg2/AHvzLudW4P/Ykteg1bHWLtdd123XN9iP17/UFM54xd2/BsX/xw/F+sKYwdDABsOcxyTLa89v00zgf+0H/84OqB6CLlw7uEVtTTBTXVf5WUdZ9lRjTSZEZznOLWsh1xPZB9oAM/Zf9Dnyae8e7gftvenR4//bTtjxz73HzcD8wSHAzMHTwq7Et8fpyWjOQdMu4JPxUgFUFCMkMDHSP1hJUVHwVGFXelb5UKhIKj6CLYAckwgM+HTwqeyV5Y3kPeTi4pXkIuOk3jDQ58WwvVPA9b5YwHy//sLJypTZaOYQ92cKuyIRNUtD5UwbUdtTKFY4VJtKDj0OKtkW7wKy9vvrFeRe47Tiw+AB3QnaBtPXxjC7NbrZuQi6Y7tBwqLHWdVg6gUDGR0lMxdBDEsLUBZTXVHqRvo2uyLXD2wFDvye9uTzMfTb72rnPdvjzdi9wrcguNW6Tr2NwynSQeZGAAEbrzF0QWhKOU+wUb1HXDhTIk0RFQWV/Db3FvZ38orsZd6Gz2C8qLRhtjq4Rr0eyLbdOPgeFLsu1EHwSqZOj1DDRYAwYRrLCoT8sfSn9YH3pu6R3evKZ7jTsECyg7Z2vSDM/eXCB60kNToaRZ1KfkwOQBMr+BmzDLUGYAcQCCkE6vYW4srFELDQsAyza7ouy9nmhgmOKRc990bXSSlNOEHdKNgUlQysC6gOMgzDAF3pe83otOGs76zYsqPCI9/PCIAoRD2ySCtL90wqQoErOxavCHsK7A2qCnv7jeKPxaKsn6cMrRO3Bcpb6RIPpi8UP4RHPUhbRkYw6RtED5AQ3QyMC/0Aqe2A0Fm0IaSRp4quKsEe48MIFCkUPRlJwkknSVNDES+IHyka2BpxGDQOZv1332e9YaY0okmo+LSazwHzFxYSLdE8lUEFQktF3TfZKLogohrRGz4RTwH45pzFNatnmyOdEKcvuGHZEv/GHqoylz0GQpRCUkXtOZAtjSq7KbAptCMlDxnzaM8ms76ffZxjpHW1ttHZ+x8bADLXP9hEU0U3SKpJojtDMYouajD5J1UbdgeY6MjJSa1SnIWXAJ/Hr/DLEe8AEO4oeDViPk1AOkBoQp1DcTnPM+cx3DIELCgi3wvd7oXPi7DYn0qWTZjwo8y3Ydho+BUXuyz5Nqg6gjwhPb8+AkMUPwQ3IjURNcMzkC82IoEOO/Wk1eS5KKaHmWGU+JvHqonAnt0E/nYabyv8NjM8uD32PQRBo0ayTpxI5ERJQ9tDRELPPV01/yKNClnuZdBItvOk85kDk1OWpaArsX3GE+Fg/awQ7h8hKhErNC+DL88wTTNVOfJE2khSRn5DH0BXP6g7DDcKLXwe4wrs8cnbCMe2szinfZ0nl6SSfplcpHqxEsTg2KruygG/ELUcoCFUJH8oPiaoKO0pvS7bNf9Ay05pS8NIt0oES9dKkEk3Rt8+ITSjJWgXFwGP59bRzLzPrk2kLJzpljeSApOSmrGlA7WGxerVkuu+/sEOuRlDIo0kaijUKfIpIiezIyom9CruMq09F0X9T69Su1LtU/9Tk1NqUuJO00uYRXI8DDA4IvwQ5v316fLXe8dhuJStAKbsnpWZ9JUmk+qTDprNo9qtArplyebY++ms+O4EcQ4vFWIbwx18HaofTR++H4QgWSPIJu8qnTKFPT1GTU8RU7lSSFLGUVxQgk5BTIxK+kQCPOszCimMG/8OOf6P6//YkctvvRayJ6h4oQ+e4ppNmYCXipsxpKKtDbyrzMrZp+rE9qMEWg7eFvcaYxwAHfIceButG3AbNB5BIt0ocDHYOqBEXVCUVEpUcVKyUHdPVU3PSHlEATz/MCch6xMDAmvvrNsoy+i8vLEtqkCliKFanpCc951Xp8uytr+pz9/dyurr+GsD7gndDOsNrg+6DEQQuxF3FhkcQCU6MtBA4U34UwxTcFF3UDNOpkj8Qog3fyfGEdj73uZR0k7CqLWbq5Om3aNmoiCiW62eunjKStzR66T2mQCDBCcEGgSBA+wE5wqNEnEfZi+qQcdRuFMgVDlTDE+TSp1ADy83GN/9DOL4y9q8oLH8qTyn9KYXqNO3H8uZ4PnvjPwyBAsG9gO7B+8I7BGGHLIrSj5BT6dVE1ZxUp1MKkCyLrUXI/li3s7IqrrDsfWtQ62MrHq7RdBa4ojvUvaS+VL4sPkpAIsMGiB0NC1JnlYNVhNUEk0hQAQpbQlx6rPOB74ttr+xQrCar0TBgtOC41PvYvO+8UTxbvdFBYsYIjRYSrBW4FTeTsRD+C4kD5HrRc/avmi1PrKqs5G0zcft2pjqa/As8lr08/sLBnEcbjUATHRXNVVQTOw8wyEcAJbegcrpvnq6nLkxu3bIANmD5JPrC+vU7Wv0LwVMH4g77VAEV1lRKUUkL3oP6e1+1G7Fe8C7vre8DMSH0wPdzeHj4d7l6/HnBmwmc0KmVGlY6VDMQqgoHAPj5OPPpsblw0PB+79jy+jV+9cn2lnereZS+SoXBjYrTFdW/1ARQTkkwgFs4S7QK8goxRvFP8JpyabUy9c92nPeheg5ANAc8Dr5TVhY8lENREUpvQZi6DjZV9CGz43Q88t+zN3VYtrm2xLcqegFAOcejzykT8VYLlMQRDgqZAsZ7n3bM9KKzmXN38nJwsvLTM7+zWTPP9r17lQNAS7dRO9RNVPbRc8xUxVl9cngftiG2JHaa9UOzbrFxcj+yaDLadBf3fDxOxXcNNpIpVOTVRJKxzPYF/3/juub4gDhFd7l3HvT0cRbxn3GhMf5yXXVNenqBXUmr0DgTcpUg1RHRxIwKRa4/M7qGuMF5dvijuMw2znNRcL7wqXBgMNkyTzZG+21DH8oD0A3TTNVn1aJSEI0ahuUBLPzqOtP7Fvq4Oh65oLaB8qVvfW9h74gviTD787N4VD+Zh2jOD1IslCgVHRWP0q/NvUdBwXj72zp0Ogo6d3qAedv3tLTv8XSuja6KLrHvNjAx8tI3WLxQwn4Ivg4yENUTLxRDFJoSho7iSkmE+QB4fbo89/tDPGI7kTr2OAl2MjJm7tAskWzGLRut0a+vcj71orqqwQKHKAyM0L+SxJRp1X0VWxWlEgfN8gjpRF8Ajf5CPbx+Yb5Q/lz957zYu2g4m3R6bxHr/KrE617rwa19rnNw6LUn+z4AzkftzHAPS1Gck3cUx9VJ1bRVEpKIz48MfggHxZWCLID9P7PAvAEuwKbAi/+2PQa6Ffbxsz6uYytNabtpROmpqc+rTi18b1+y+PZKPbpDK4kJzqrRgtLc1GfVERZ41bWVs1P5j5TLyUkjBcgDtwCKgCM/JL9Af7A/XL6kPlv8YPkVdl0ycG5Sa27oe+hQaKJpTSqba5muC/DO9HC41D77RCfIowyZT4eRd9Lb1BzUx9UDlO7Sx0/WjQtK/4iYBziGzkZHRyuIsEhCB0OFh8KQf3/55bTRL5ZrAWh1Jj7md+bHZ4hpVKvGb6Z1WvtzweYHzY1g0R4T4FUyleJVVZSE1JdUgNKCjtUKCIewhVJF+cazRq1GJEUQRG0CQX64+s71by9ZKq0nD2VAJaAmZahH6w3vOjRx+zFC7Yk2jjiRi9Og06eTjBQjlI0U1xJ0TkxLZAlOyEBIygg3x3qGWQTvQjQ9JrZo8G3rAKeuZMelR2boKast1fR6fEaCVwgkTcSR6ZIbUlTST9L7kweRhQ4hS0bKTEqpCikJAEeeRIk+BrfK8M2qZCZ55A5lVKdqq3wyhTuxQ++LXw/6ElrUeFTqFKTU/tO6kDYN9sxuzC5LLAk+BSnAurl7sGkpnWYJpLYmZOoYsKF4IcBliKgMUo4DTzPOuU6+UQMP/w2DzLWNWg0lSnLFlb7ytH6reuYuI5hkUicw7QF2aP8uBn4LSs4CTj8Ol1DlklPPf83qjgcNWAp/BGH75jIxaoCmQKSCJ4KtnfbRQIHHYkwxDEQMn8ypz08SA1Aqj+3P+A3MShaB3fbA7Qdnh2TIZlqsMDRH/tsHIgsZjPxMTk37EQrTX5Jq0Y1QdYxuxH26DK/IKSXl/CY3LFD1nH9mxdNJZUnAymbL6tBtUqYSXpIiUAsKYwBFdc0s8KfOpjKotTDjPBXD6setR8zINgihzgGUmxQokzeRiczAgnd2N+1mqH8mWujxsKJ6UcEKhBXE6oW/iPZPCtVRlXyUN9FLyxP/YvO8K0LoR2c+a0l0jL0qgYZDUgMRBbrKlpIoFZ6VXlPnz3lFa7i4rqBplCgfqJkv5Xg5voLBvgJyQ04HvQ5+1Y/W45Xk0p+MD4D6NWcuBmrbqe0s8XVUPLr/UT/WwMNDLcjk0JDWnJYfFGxQHIdS+xSxw+0jqx6q8O72teR6h7zDfJb9BoCOx2NP95YL1ecTps6yhav6Y3GJ7bWrvitnbj/1T/o2u408Azyof2/GIQ5WVS9WVFTdESfI5L7JNcAwDS3trSbtOXFjNwz7MLu5e0E86kACBuNPONUvlqlVHFFJCf0/rnbBsgRvWG6YrsfvWrR0OIl6o/pBerx84MJfygcRAZYJ1zlVfNHBy37BYzi68uGwcq9zL22vNPEUNRy3ULfZuAx5jX0SQ3OKi9GDFd8XJhWPkk3MZ4QPu1k1FjHfMIrwhzCvsBxyaTTXtgW2VzZ2tsx5vz4ZBMjMdVI+lUHV1JOtT4/J4EKTu6m2QfN4cnjyD/KAMngwn/NPNMp1HnUv9Sx2iLlxPedEU8sMELWUKZYeFU/TFw8FSg/DR71weSZ2YXUKtJV0brQwMumxH/MLNAF0aHQidJY1k3gze9DBisfpzXrRRpRCVeSVk1OwkDXLcoW+gBu7xPiDt0c2R3Zitli1+XSgsuAx+bKP8s6yZjKossT0R7aJ+gr/ZoTZigkPCNH5U+2VANU+EsMPjcurxtrBij2gei+4DPcxdqW3cXc59nj1iDOcMTrxMLH2MYSxlXGS8nUzTHYL+ed+DAM0CEtM8ZBl0tKUXZVFVWcTWdCaDPtIVYQHwDE8+rpeOQs49njouPN41Pgsdrn0wbJrsGFwgrBYsJuwlTEasjqzmvaresAAAQWoyk1OrdE/UxZUatUg1ZUT2pEkjRhI1sVlQQo++TyC+zk6OPqQOpk6B/lDOGe1v7KvL6dvJa7Lr3IvkjAyMMMzHjYJumP/HkTpibPNxxEMUyEUQdVKldIUndH+TgUJ6QVEAf4+sPxvOzF6zPqQOmB5pjivNkGz33CN7avtuO3+Li3uy3Avcd81H/nH/26FEArXTy3SKFOy1O3VTNWH0xfPdEpyhTkBNr9pfcX9aD0TfRK83rwKOkp2R3I6bj1tNq0GbVht068mcah2XjxFAznJPo3hkRwTIVRjVVPUlJCAy90G5EMYANO/8j9Q/rD+C/13+lC2hfGubTSrMisn6/UtcrAfNF/7HMM1ykFP0BMbVILVepUR05KOwslJhG/BhECYgI0Bc8BGvk86BnULL0QqsKp96o3see99dRB9coWJDApQ8NM1FFjUzBRIzv1IgUSfAs0B1sFnAIM9y/mvM5at3ikg6KJphOy9cS84z0JTCptP/5IuU/cUKxPcTbyII4ROwuQDHAKrgJG8OjZeLwrpKucW6EFqwLCqeMjEXYtUESfT7BRm1BcSfkyNSEkF88Z0RdgDn38X91avO+jLZ81phC13dNp/TwkGz4YShJM3kpxTDY17CH9GN4ZGRhAChb2iNGUsU2ccJkbo/a2rt27DJYsoD9+Q95DEEX4PDIqiyCjH00epBUB/hDZCLa0nsiaX6bVvWvohBaoNL9D8kZSRbxHKT20L78q2SZUIXMKtuHruJydpY++lbelEMW+8uUZHzG4N1Y5SkENRX46nTlDN7kwKh2m+UnJc6UGlLOWoapf0D/8ICLrMbo1djYgPctH4ju0Om88RTNAH/z3jMkLp4WVmpcfrOrQaf4qHfwqKCv6K+00gkcBP/NAdkDSNr0fiPbCxrSlRpYWmsmwZNkwB0QhxizzKy8ubTb0SypH6kZ2QoY3Zx288d3A1aOelCaVk62j1I//JRmXJPgo/ihEMhRFw0lBR8JHUkEsLqME6NTMrvaaZJMZoM2/4ukgBSgYtBx9H9cjnzHKSKpNsksRTktD/ih//g7TKbGAn8mX1J/Tuvrda/8XEdMUNRRbGSUlsT7IV69ZC1k2VJhDfCQt9bzKLK8Zn2aYk6CgtyfbZPlyBawMdgsVDF0ZxDKQUSxYklUbU3dGtS6MDGHh+r/UqM6cl5t8pTjCpuGr+cQGVwk5C0IQgBaxKhtE0l3UXnVdEVgvSw8uYgl73xi+GqkcoKidypwvsc3Rpuxl/FMDAQQpB7YI6BQgKfs/VlU8X+Jg6VxtUZY+rSHx+zzXdb7xrsqkTqPZpS+z7MoK47H1JQKHCFAKCwmeClkTwSSrO/lSMmPqYqpfkVlgTnM8IyNOAezjVst+uIyvXqo9py2m07FvxNvU3OFR7vPxqfWK+pT8dwKVEIMi7DmiTDNdZ2BpXBVYNU58PzcoZgsB76zS2b/rsRKqk6cwps2nbac6si/Cfs5O3KDlJedv6qnopept8Ub8JgxMICY0ikc9Vx9gLF9ZXMNWR0y+PpYulRgK/DfoFtRQxOK8zLqUtCixgrAfszm5c8UG0RDZVNt445jkNuYI5Xzp5PMBAdQNzCEENGhFHlJFXJtho16CV4RRqkmfOx0qjxUq/fvpMdl70F/H8L5vu1G6C7v+u5+7Rb7Rx0zPgNec2t7cPdqF2S/YCNkd4Brrq/d+CHAa+C8WQWpOPFh1XalaXVYOUlpJczvmKjYY2AUI9Q7k1doazjPIA8XmwXe//7/Svqi+ZcWZ0FHVcNre4J/kKeTS44jm5u3586gEHRT7JAM2hUOjT2FYAVloVrVPmEVlOLckaBDM+jTostk+zvvHXMX7w7PElcMjw4S/WcSZzFjSYNak2MLYCdw73jTnuPO2Ao0VSiqpO81L01NjWqZZt1T3S4U/Kis7FQf7w+kM2ljRM8tJyxvLY87Ty2zIaMfbzmnU/9ay1ZfVb9dr2z7p+f2LFaYqY0D2TcxWelrKVQ1Mzz1aKIkP2fWT40PWktDuznHP8s6mzW3In8lj0HHSBtMe1GPYTt8k7SoBgBwLNftGpFLKWGVTb0jPNIIaDv9y5l3aINBAzQLQiM/JzIDG1sdfzdDMZM430pPZLOr6A5kgjTqySr5TnFZBTVU7aSAHBEbuP+Fq2pvbqtkx1oHNUcaryi3KpcpXzxDZ0+4RC7crp0KAT41VFFDAP8YmIgr286fnl+H24KDeuNYkyHHB9cL+wrrFbNCh5QcGASk4QulONlSQSiozhBNo+A3pcOIL4DHetdfYyR++3L8cwVfGRNMk7WUTITWcSVVSI1VTQpIllAZE8/zr/OeS5ufee86Qu3m7ab0ixHrSoPPoGZs3eEi8T7ZLoDLjD9T3n+t87DjuRuqg2IrBXblrup/A9dIy9JoeDzzATFRTtVBxOdMaZgFJ9mf0zfQ56enR1LcHt1+7O8fZ4HsKni/4RUxQq1IVQvsh8Ani/qz8Gf7z9Cnc7bxrsqm0hb1a1Of8Sye+QshMlk9bRLwlAQxLAlkBA/8U8rbYs7vUrEWwwbzb2D8DsipkQ7JNv074Qh8lMQ7NBm0HkgX09EDWQ7fPqzewUMAY3hwM7TFTRoxOIlDjQ2wocRWtD80OhQeh8wvQTrAjpsmsdrzA3tkKUDEURXlL2EutQVclihXlEmgUZg3/927VmrSUpFyqz7tv2ycJky3/P1dGuUYCR9YulBq/FuQVuRAd/pjdxbkyovahIa4ZyKbxah5TOdBEHUd4R2Y/FCkMHRQclRlHEIj4jNXYsq2e+aCeriHMNvh/H/I3VkJtRTNG6kfvNXkq+ye6Jzse/wVk4qy8waMXmWeh97NE1nMCBCZ9OJs/hz9vQU1FKzViLt0sNCuNIqUMj+r1xE6p45mqnFqr1cZa77sWOi67OHI7tTo2QFdFXDkwNLAzSjJ2KjATb/ESyfaqHZtWl4GhnrZe2PT/vB7NLyk4rzcHN+o9A0ihPCo5VTlcN9Atdxnn+AvT2bNnn2WUzZXNo1a7ht4LAH4aximDMdAyojLPNXhCqUMIQWpBGUB/O0kxHhwe/NbWB7jeo+2X3pSnoE609dAx80sPUyHYKBosMiwjLt80CkEqS41H4UcVSHhDlTu/KgUQbO8lyb+vWJ5ElVySw5w2rmrIeeZ7APYRTx/9I+wjaCTLKTc0MULaTNhLikluSj1HmEAMMjkZI/vt2yXAuayBnoGYepVVonW2NtLb8H0JrRpfJFglNiY0JuMpnisPN0hF3E3kS95MYUydRmY86i8dFsv4BttdwB+qApx8lROSNZOJoYm1PdH36EIA2xCSGpEapxscGokfRiQrM8ZCclSjUdtSrFHuTdZFeTn1JmkLGepV0JK4sqiQn3yYnZUKmkyr+8Gx2Xrx1wNXESsWZRaVFVUYARwjKXE6Y0i5WMpZHlqQV6lS2UpHO/witQdZ6Z7NI7mUq8+iPJ4km4CjjLhUzPbi9vLVAGgGKge1Bl4JPw8MHBkqPEC2UkdYR1hnWDhUu0lKOZQgqgEy44nHDrOrpYeeb5vumhyrIMCZ1rXrb/pHBBQHaAehB4YNcRhAKflCllfsW/FaSFjIT+5CES2IDcXpJcr+s26pIKI8oZ6fgrECzaPivPbXAlEEIAMjAcwKXxzLLLdFKFx0X4de5FmjT486tRjK9WzS8rc+qj6j5aFZoWm2Qsyj3x3sZPRx9J32cvs7CusfrT9dWXRd31oUVVFFxSvUBxXhHcWrtSCvW6wxrF7AnduJ7+P7b/yv+FT9uAq9HxVAbVcaX/BaiVHjPqwdDPOUzye52q7zrKqrmbYS0PDhgezW857z6PkiC3Io0EdHXY9fPFijRsIo0P4O2KbCZ7aCs8S0Xb+t2GznwO5L7F7tU/oGFJ02B1TzXr1aOUznMiAJD+UFzrbAh7x8ulnDqdkJ5Mnn0eRs60QGUSiMSRVd+1dxS08rQgJE3onEI71XvaC//chT1rjget5l4BLz2BOWOTNUy11NVHM5phDL5MHJFb+wvJy+/sAe0rvZ8dr/3rvvoRIXOvhSBFm0TOItBARk3wXNr8m7yInHF9Gh2MTaCtub5UUGuC2gT0JdjlQvPVAU2OyY10POk87HyvjIMdVA1qzYBuVABT0vlk5wXBdV9zqjEzLvKtw/1yzWRM78x2LMKs6E0hPnaQxrNTxRT1ziTmUxJgkN6ojZG9m52CLLBcm9y/XMRtcN9doiwEWcVsBYQkT/IGT+5ubK3kzePtVfwRfCzMT2ydLcVQIAL/hJV1VPTRA0fAsF70fgb+JQ3TvSmcJHxd3ELdEq60cWuDi2Ta1TxkIyIooCde3f6SXph+XL0QTDZcPlwz7Rd+4lGRM9H0pjT3s6FhdI/vjuuey/7FDg5cv+vG++WMKkzgHvghj4N6lHZUx4OwsZWP0A8bHsPO+P52TT/7xHvhXC9c0z6PoPODDXQxVLaEmuL+0S2/xY98XzcPIw53LQLLx4vJq9EMx850ITdjUARHdKMEolNc4cKQp8A2YDF//D83fciMRuvNW/J8iT2fb3DiDPOaFEKkjGR1sxTRo6Cm4ECQA+/LbxO951xNq2ZbhnwffSaPPaFBYxRj9yRvJGU0G3KsMYEQzcBg4FdwAw8nne9sYFtY20V7kexbLbZPoWGiUuGjosPqNAzzliKUcZJBNHEUoThg4B/0PpXs8Quo6yGLW1u3rKvOR5B6gjJjP5OiU+lz3kPCcqExuAETUPQg9dDmEIpvof4fDJ0LfRsAOzV7p8yHbf9PnhFoMrezZ0PcY+5T/KO8wrOCATGdkTfRNxEU8KIvu/667TLsHhtOuyrLZ+vEPLnuJo+2cUICXEL/wzGjasOKo6JTbXKV4guBw4G58c6RpcFJIHiPXp3m7L47rbsIixKLfAwS7S6uhnAeUWxicaMiw25Dh9OCg36Tj9M/AqoyUwIy8h+R7qHTcZCg4P/H7nCtLkv3q0T603sAy2XME206Ho1gAYFhgmYC8mNLI1gzUCNkI4mDxvMSsqFyjFJ+Ym9STAIA4XKwfn9nreWMqMuk+xFavgq/KxLLuMyafds/IvBrMXqSIqKAkqYSqKKhQtqjCGNJYr8Ca/JB8iJyRxIjAd+hNPAzPwV9nax+m61rBxq8atXrX3wXHRJOe5/tcQER9IJvArxCwMLZ0uwzEAN9Q4gjHQMNwvtC3dK3ooQyA/Ek0AeuqM0iDBnLYXr6CtWrSiv4jOWOVr/jISvB0BI7AlNiipJ9kqoi2tM8wypi6NL2Uwwi4JKvIgbRIt/QLm3M4rvjiyXqwZqwSzMcBV1J7pXgCSD4QYlxtTHbMcdR4/I7QuFTVzMzA11DXfNNEuLiOqD6X1f9phxZO3BLBirdG2lMd73vL2MQobF2sdjB5lHuohsijhNLg+CT3sO0o7nTaCLMYZiP/44EXIfbhesWOtdbcFyXniyvlzDOsVABt/G9AcwiRAMVFBij+4PlA98TanKDwRAfJw1AfA6bVEsem14skA4rL6swqgE34U0hT6GMIkITR/QUtC8UCbO9YtahVP8z3UEMD+tUWxTLg3zSnqAADfDFgRXhN9FSohBTMtRTREz0EKO7Qrzg+s7MPO9731ta2ytsHT2XzvjPy1AQECZAasEjYqOkCXQ9xBTjkRKCMKVuWNyve9ZrkLutDPzusC/KgBQAEUBe4NUSMJPEZFe0J0OnInXAeq4VzKRr9ZvF6/a9jM8L/8Y/4WAOYGKhlcMXlGMkQiPXAt3BAH7EfQxMIVwJDAidcU7zn6YvsB/cMEjxg1MmZGvEVbPm0s2QrM5d/OCcWMwg3E1tq87c30HPW69qsDJh7LOSxGrUG/M2AXiPKS1vfIIcUlxVvPaeGO68nsNe9t+8QVozMhRkBFPTvXI70A1OCs0O7LMMpdzWveCedV6IHpqPY/DuUr20AqQ7w5ZCSKAQrkuNLozkDPS8253RvlYuUv6G7x9QmaJ2U8TULQONAiMAA54/vU69Dz0dzQPtwM5SfnAunj8ccImSXHOm9EhzyZKkIMdO6R3HPWbtUw1IDTmNy33Z3eP+aQ+fMWSDHMPmU/azPwHND+AOjw3NPautm01GPXJNt42yTerulxAlwfyjRLPxg8/i0aFTD7uemr4srhpOHW2ljaNN1K3bjgc+vaAgAfPzJuPHs9rTL6HusElPKT6LTldOUq32jVqNjX2HrayuD37/8INiK0MiY6QznlK5sWXf9c8RbqSOmy6ILj49kE2H3YlNmo3ZnoMfzEFgUr3jT6OEY18SYsE/T/mvSO7oTtFe6U6WfeG9jV2HPaTd1/5kD4pw4AI8EuSDTQNfIs/B3oCzn7fPMi8EDwNu/F6ojgldUW1qzW+diS37Ps4f70FM4kFi1jMd8ySSmsGu4KXv9X96T1VPbJ9PXxquhj3OrVF9bB1kHa/+J18UYC3RXWI5wqmi64L/krgh9zEWQFKP7b+o/74Pra+iD2R+3e4LbXH9et1wHaKeF87PD6/gvpGjAkUCnMKzwt+SlhHyMU0wfsAFD+ev87ALb8tPdp8CbmM9yB1LTU5dRR183cMuYy9CMD4BEWHbIiDyekKBMplSXzGhQQ/we1AiwCNgFhAcUAR/yy9SPu0+Op2tjV8tUf137aAeE86oH20AQIEaIaOyHUIz0lUiWSJRQfNhb6DQwGRAJjAqoAvADo/wr7FPcW78flRN2W1srX99ix29DgEufP8tcA+w2xF/8dBSKGI5Ak+CMOIwYbXxM7DbkHXgX6BCcEgATjAaP8jfQ36s7iSdub1VrWGtjn2/3hsukj9oUBQQ5OGNAdwSDAILYgcSGYIQQbZRODDZQJfgYWCH4IqwX1AHP6ZvFc6MHfWNlj1arW9dhS3Qvk9+0c+o8F2xBaGI8bQB3dHQMeCR8vG7gVERHLDQAMyAy7DNoK1Aba/ePyqedb3x/ZP9di2YncpeLi7U37dwdJELIWLRpnG8EciBuSG1IWVxEsDQ0NZQ0cDLIIGAVo+3vv5OTM3ufYrdiB3DLjeu3C/P8I/xLfFvkarhsHGzcbNhwmF38TpxGTEcUQCg5lB7/99PKO5Qjes9gc2RrdjuQ+8pwAuAv5EhcW9BbgF9wXrxt8FlkSeRGxEk0RmgyUAeX1M+fo3S3Z09iA3b7mivOJAtkNthNpFfYVsBZAGrca4BgWGPMVYBVMD2oCDfIz5FfcZNip2+7kiPI7AqoNfRToFCgUxxVIGUca0xdxGI4XZxEIB8j3Yefz3SDZ19kr4a3sGvs1B04MRg1aDakQHhj6GNIZuxnDFI0Kjfsh627gwttE29fjffEIAgILhAzoDE4O7RGVGjsaYxpoFhcOvgDV7XbgH9tX2fvgrO/d/0QIXAqsCVQMSRPZHP8bQRu3FQ0Livkr6Qrfx9vy3QXrVfm0BCkIwwjMCzkUgB0LH/UdsRoAEcv/M+yi4ALdT98v7fr7owWhBxkIWwyjF20hBiKKIHgaIwuK9r7mhuAt3ujlEvTe/xwDhgLlBAgOohtlIJ0frRlsCs71xecY4bjf9uRD8yj8j/6e/4UCfgurGXAgOR4NGG0IsPR95jHhSeAa5ufycvsH/VX+eQN/D7AcdyFgHkAVHgP17lDk6ODb4I/nz/Lr9+v4IfqMAwESUx80IYgcYRDw/ebsGOWs41bj2er78j/2rfeD+ggHPBZbIAUgzBm8C7f4uurh5RrkIOSp7Nfy6/Ms9Xf7SwhcGLkhyh+IGL0IKvdn7EvoFelk59vuM/PF81P2Yv2KDIca1iHGH2wYMgl69+PtAOsD6qjoC+148LLx1/In+sQHxxaIH40fJhnYCi/6HPAR7Qjt+es07A/wn/HR8yz5RgUNFKgdryH5HckUwQT99q7wUu+U7v/rZ+we7nvuyfD29wcEbxK+G4QfBxuOEUwDzfgg8xrzZvMU8K/siO6T7oDwJ/Tf/scMvBdLHRAdHBbuCfb8oPaj9Lv04/JZ7mvrWeys7Eru4/Ov/rULQBYkGx8cdRb0C38BYflH9Sn1E/RL8Tjsxurw6sHr1u/598sDzA+bF6AadhvCFYAMcgJ//L/5c/kS+AD2VvBm7Bfs6OvA7Xzz2f03Cm8S1hYEGfUYohP+CngCWPwo+jn6W/q393ryqOsQ6xfrHewu77j1sv+SCpkR0hUkF04WsRBBCE4BQ/3J+l/7Gvu5+aL1we+C7JTsJe0m8Kn03fsOBXIMjhEbFDoVIRWcD94I7wJa/6D+fv4O/vX8lPkP9NTuz+zx7PbtiPB79fj8jgWmDLQQ3hKeE7ETWRAnC7AFZALRAIcABwCs/+H9ivnY8ybvwu3Q7eLubfGj9dX7ugM7CukN5Q/hEAMRShBaDOkHGAQcAogBQQECAYj/3Pst93Pyq+7u7WXulu898qD2SP0+BIQJ8gyGDh8Pfw94D+0LIggABZ4DDwOoAsYCBwHo/R76WPVH8dzuTe9x8DPy1fUU+yMBSgYKCkEMBg1MDWIN2QylCVYGMAQjA5ECjQKrAZD/Gfzo9zf0ffF+8CfxbfLO9Jz44v38Ak0HgAlyCvwKEws3C8YJHQeMBaEEsgSnBLADGgLp/jv7Offj8+XxPfJP81/1s/iH/WEC6gUaCB8JaAlZCaAJyAjFBisFnASKBD0EPAP0AFz9Qvng9eHzK/Mc9J31ovjs/GYB7ATKBpMHyQewBzsIqAc9Bo8FKgUKBVgEugKq/8n7FPiY9WH06fRh9uL4q/zJALYDLQXKBcsFAgafBhkGDAWaBJMELgS7AjsAm/wI+dT2rPX89Vf3CPrk/WgBrwMEBSEFFgV1BW0GQQXNBLAEhgQ1A9AAcP0G+gX4+vZf9wr5yfsm/+EBTwPrA8QDAwT2BEEF+gTXBJ8EiwNCAfP9tvrE+Oz3gPh/+oX9jABhAh8DPQNUAx4EWgUBBecEtQS9A58Ba/5o+6j5+vir+bv7jv4HAUMCbQKNAgoDMgS9BLMEoATnAwkCJv87/Ir61vlP+kD8zv6+ALMB3gHsAY0C8gNTBDYE3wPCAoMApv3C++r6sfov/GH+LgAgASYBRwHhATQD7QPRA3MDPgLo/4D9F/yI+477Dv0C/z4AxwCoAOwA4AFRA3MDOAN6AuoAg/4E/Vr8OfwL/bL+2/9GAD8AkABuAawCCAPWAi4C5QDy/p39Ff0A/ar96f7U/xMAEABzAFQBYgKNAk0CpQFCANP++P23/br9cf5N/8D/zv/r/2cASwH8AeEBiQHKAKT/yf5y/mX+kv43/6P/vP/H/woAoQAxAVMBJwG+AAYAWf8J//v+/v5N/5z/sf+z/9L/JACAAK4AlwBlAAwAuf+R/4r/kf+l/8n/1//a/+L/8v8AAAIA+P8=`},{name:`jaw_dislocation`,sampleRate:12e3,frames:8640,wav:`UklGRqRDAABXQVZFZm10IBAAAAABAAEA4C4AAMBdAAACABAAZGF0YYBDAAAGADQATwBaAFMASQA2ABQA4v+y/0j/7P49/qP90Pzt+wP7M/p++en4Xvj196b3iffH90L4XPkW+1z9MQBuA0YGrwhGCqgLmQxHDdEN5A3GDU8NkQzJC9EK9QmXCD0HWAZIBWgEiwOlAhABPf8u/cT6+/c69WTyge/a7PXqDukB6Bznheay5i/nX+gS68XusvT4+qMCwQnxDrgTmBfxGQscshxfHTYd5Bz2G3IaiBgEFjETMRFUD9cNVwy5C8YKCAnlBowDIQDo+kH1X++z6C7jIN642uXXsdVa1KfTitO51KPWPtpn32zmF/Fb/FAIURRpHekj/ig/LAAvly/3L+8vhS6wLAArOyghJdYggB4UHQQayBgOF3EUQRIODz4L5wQ5/HT0ierr35bXrM+FyUjF88GfvzC+bL3rvT2/18L7x33QjNtU53D2YQi+F/YjdCxiNNs4TTyiPylBsUBHP18+/jwMOpc37jSYMMcuTS2mLEEqcij9JBgh/hvfFwcNygDC9QDqd9xg0NPD0Lo6tBaurqq4pqWkKqRipBGmW6mTrpq3d8MU1NDogvzlEYQlszNoPxNJlk1AUS9RSVHXUElPXU3qSTlGPUI4P1w/Pz9/Pb09FjyJOmI28jU3M+YtpyY1Hk8RFwbe9rromteLyW280bJpqvKkrp9PnfWbW5wfnZeecqEKp4Kt+7eQw47WzeZv+g0MsBp7KFo0fD78RJJH6UsPTJlOpk5sTs1NVU1HTLBKLUkdSK5GeEZIRXpFJ0TXRuxGTkWBQxw/pjpOMQEpUhvzB3v1d+RF1i/HcbqgsbOmx6CYnJyYiZYklCOUF5Q0lcqXyJsqoC6o4bGJvjjMd96y704C1Q+oIKYsyjIAPZBCokUqSAZJsEqKSpRL0UphSEBJUEmBSq1MV00RTnRNPVExU8JRRlJ1UBhP3E2tSp9GGj3WNf8tBCJkGCMHYPVm5E3TvspIvO+wEanPnhmanJa0k8qSrI91j1qQ7o9wkvCUeph2nYyjnK3Dtaq/kM0z2m7kAPQKAskQWhj2IggrqDLvOsRBAULQQA8+IUMORa9GX0bHReBExEaISitNVU63TUxOrk5fUdtSElN8U59U9FHmUF1MIUsvREA+9DbJLgcgtRToCmz7+u3s4/HUW8iiuQexz6ceoLmbkZaBksiQLY9Pj7iOa4+qkKuRqJStl3mb8598pTGtZra/vYXNZ9e75Y71/gDiDlYd0COaLTkxMjeLOSA6XT/xQE0/RkP6RAFH2EaCSspNZ0s9SilLG0wOTq1OmVHwUZNQClX2VlxYsFcCVYpT9FEtT2hNEErxRIw8KDRaLO0ipRjbC/3/afA54kXYjcvLwOm1vaybpSGgTp1CnBKayZhfmPqY7ZgcmlGb2Zv8nVOgRaRrqaeuLbU6vDvGkNEF2ozkDu5J+BcAQwrkENwVnBoZIf0gTyggKF4t+TCNM5Q1Azh3NyA8FDxFP74+oUEqRYFERUX8SelOD1ENUftT/VQYVolYsVh4V8hUJ1PwUrNRsExGR8E+Nzm9L+cksxPcA47ym+ds2svR7sW8vFuzTK6LqFCkvZ+AnT+bPpoPmQ+bHJzLn0ujnKULqkiwSLSYvGTB6srfzm3VX+HU6MTvtPrD/5oHyQhbEkAXVxtrIEkimyicKt0sBTO4NK82XzYcOCc5yzi0PB5BeEOBRJxG3UlZSyZQpFDhUcxP20+zT9ZNz0sySf1E3kH1OtsyNybHGj4RDQRH92DmmNbAzFLCI7zEtGuugasTp0elUaOmoSGibKJ1o3mloqYcqW2tXbQzurTA28rY1LbegelW9Tf8nwN6C4ARfhPjFSIZ2hkqICoieCPeJ1Aq7CyJMHgvFDQoNFU5bD7qQXFH0UvFTl5SUFSyVJdUTVTMUkROzUg+Q688gDMKJ6oXpwXF8vDkQ9coyl2/M7c7s76vC6w/qziqFqpqqsyqsqyKriWxYreXuwzCz8l707LbaeJZ7Lr0JPXQ+wABdwWUBzUNCxKUFHoY9SBrJX4oqy1mM6850juwQDhF60cQSwFNok4ZTzhOmk/GTctKvUXZPms4sCw8HPUNX/xR65DaHc2GwZa49LKMsF2t9apgqrSqfK2/r7azgrjXvKnEZc3k1FTdpeO67UbxxPbm/Cf/YwN8CNoNURMDFkMgqSahLBMyoThRPjdEjkiSTeVOZFAVUW9Ru1AuTlpJA0IjN9wqlBnkBTnzU97zzn7DV7vps8Sw/q5tr5uw0rEZtKe32bw1xJDJfdGk1vTbbeVy7ADzAfd5+rz+3AGUBx0N7hIDG4UiuCpVNCY8lEIXSTNMFk/4TzJQB1DGTaRK+0MBOW0snBqFB1jy0t3RzonBn7nus3GwFa5vrXCufq9EsgS2d7olwKXH+8/t1/ffNupk7bTzyfgl/eYBygmWEdMYXSKqLuE3d0J/SpRPVlKUVL5VJFVLU3NPJUfnPJ4uKRurA9bsH9hryGa82rbnsI2uAK/osJWyRrZTu5HCB8fF0CnWLdur3h/k1ufj60PvJff4/sUK2xbOJPgwZDzLRUNOVlTRV2NZWFmDV4JTME68REw0Oh7PBhvvIdr+ydO+VrUJsNOuXq7ZriOyKLaau7jBycdQ0IXW8NyJ4KXhJef16Ynx1/urBagTYSLaL1k/N0jzUVZXcVqiW4NaslfeUx5L9T5aLEIVXv6T5sTSk8OXuCeyLq9Urp+vKbHxtJi5qL6swijHLsuU0GDSTNZ72jzg9ulZ+HoGrhUfJRU23kTLT0RYoVxwX2Bg41/iW7dUdkcrNQgdigFN6PrRcsEct8ywBa/arkqwxLINtXK5jb7cwTbHtcrLzHLOR9T62QXjtO3U/qwP3R8ONONEhVF2WrJgx2P/ZLBkeWKoXDtT/EIkL1ASN/Vo29jHHLufsvGthatMq4Cu17F4tHG4jruiu4e947+RwKrDock30iTcl+xvAokZ/C+aREdSD1yFY6ZnZGlBaZdmUWFnWHBJejX5GyYA5uQR0YzC87lLs/Wve7A3sFKzELbuuvO9ir+9wT7DqcOxxnPJZM7a0w7i7PRoCgokszniS4VZ+2IGadlqY2owaEdjZVrATBA4ECCXBE7sftcJyP297LdQtLOy5rK/s3e1rLdYugq6ILlLuYG6AbxCvibCicrS1ALkOPx2Fhku70ExU9te1Gbxao5td2y9aKlivFhsSsA3dB7ZBNno7NbAx6K/6buZt6W1urUbtci2AbY/t2+2wrONsw+y0LCSs2e4zL6UynvYQewMBNkdejiOTDVbW2Vpan1tqG7ibVBrUGU8XBlPcjtBJU4PKPeF4WHSLMdhwLm6G7pduwW7RrpeuRK3v7XUsaKw+a+Trs+tI7HPtmy/8s2Y4C/1Og3cJVc/sFJDX9tmfmutblJwl24Ba5JlF13iUYlDnjBWGev/ju5o4KvTs8oQwuq+Frwluv6567ijtyS0VLBFrt+rWKnEqJWpQqv6rWa2dcJa07jlg/8tGaMufEOTVUpfMmbaamZtNW0rbDhqFmWeXIJSbkNJMVUeyAqK+K3oCN1K0wvLGccJxOXBkr93vtG8irk9t7i0AbGTrYGq86dHpwenQao2r8626MEn0b7l1/5EFkcvJkQHUgddnmPBZ9RpV2rbaWBnSmHsWpdRu0a3ONsmFxbWBIP0NepK4ZfXwdDyzZvLkcigxk7FPcNKvQ27D7iCtJix1q5PrPSqF6rjq+quLLNZuBzA3s3B233wWwbDHToylEFKT3pZJWDQZDdm1meAZ5Bl5mLrXs5Y+E+jRVw6Eyw0HfYOyQKe97/w1uhk4tjdJ9dN1MrRtc+wzHjJdsbfwlO/qL0guJ6zVbDSq7KoVqmwqcyrIa6Vtd++iMuq2oDubP/LFrkpIjv2RnhQD1dfXAxfuGB+YDZgCF40XOJYbVJ7SktCtzfNLcMhGRiTCuT+YvrC9L7wzO+i6vLlauSF4Abe7tk41WbP1MguxPbA5bqhuVq2HbIGsfCuEK+OsFGwI7M6tgK8WMST0JXhIe+NAs4TdSUYM8g+UUmZTr1SDFiMWTRctVyKWxxbI1n4VrZS2EvpQ1Y6fjDCKIkcFA8jBJf75vXi8HvrN+cY45XfbODp3Uja69Zn1EzREsvvx53DR76DuiG4FbVHs2uxa7EusfGxh7S8t6O8JsImynvV1OHK788AXw7yHDIreThLQvRHrU1oUOFTS1U/VrNWMlZVU9VSIVDGTLRGfkLAPA4xiyl+ICQVBw9KCtQDuPwJ+BD56fFY8arthOdw4Nbcetrp1KvPist1xzvD475PvMe4lbUksxWxmq9Irm6vnbFhtaK5dL8HyrDTnuLS9R4IeRj1JWsxnzuCQo9JSE7oULpRR1KyUjdTM1OhUNxOLkuGR5hEYkCCOegxTynOI2EbLBRNDEMDAP/b/Kj6GvRU8KzrNegJ40HeI9oa1G3MfsjpxTHCI72nuc+2M7N1sJGvRa+HrSuuurCNtBi6hMCKyy/ZHOnv+/YLzhwmK/81F0AcSEdMyk6MUIxSQ1NYUkNS80+gTIZJ8kZ+Qi8+BzeWL+slwh9fGxkV3Q0SDZ8G8gHq/nH55PV38FjrDeRl263Umc5UyaXEPL4muRK09LBprkqshaxdq7ysVa9OshK5A8GryjPb+OjS+zULhxm5KP01uD9RR61Kkk7FUaBTHVVFUy9Rnk1bSu9GoEHAO1w07SoIJxofrhx6FTYQSAujBPv/g/0F94ryuelW47DYGc9byZfC/bp0tb+vG60qqlqp4KmmqQ2rZ66zs/O9DMeg1nTqzfx5EWEjWjKsPhFHZ04gU8xU11QfVftSsFC8TDFGBUCHN5YwRCtkJG0gzBvJFiEUFRByC5cHvAIr93vtgeXW2n/TdMnmwFC3TLAZreuq2Kc4p82lVKeQqt6woLlNxdLU1uUs+rMQlSXxNI4/3kicTnZSyFSkVP1S0lBbTmBKBEWWPSA3rjHWLEUokyE8GpkUvgtTCe79RvNh6VnettLjxZu6TbLSqPykCqHsnqOetZ/somKpe7N/wRTTnO3qB0QeBzHhQsxLTVJaVmZYblgnVsZSDlCzSCFEYjuqOEYw2ygZJHggaRkqFloQwgu6/tPzdOL20jrDxba7qhGiiJw+mq2ZRpusnYSjOKw3u+rQ0OzJCP8gnDVER5dQ21bGWUBaZFnGVQxTRk+GSPZCpTxDOQg2Sy/IK4YiVxotE9IEmvWp4NrMxbvorX+jmpyNlzmWD5cYma2dO6aStPzL/eV+BXEhdzalR8xRh1fhV1hXD1e6Uj9NBkfeQRJA+jucOsA2ojCDLHAlvBcGCUj08eB/yrq3taoon6KYVJWylCqV45jwoJyueMD23cb87x1fMSNDcU6vU45Wo1atUkxP/Ed/RIU++jpFOxM6ujbkMvUtOCYiFxoBE+cDz3C4dqi4nXCXUZQ4kxiWTJyzpki3S9DP62oHCSKgOJBF302WT4NPu06KTDNJiEQoQf5AZj7GPNk6WjLgKssflA1W9qzam8FQrtCga5k0lNmRc5Lflrifn664w+DgTwGVH0I2BUUWTQlRIVBkUDhNEEpSR6JEG0a6RBREfkQhPokzYyPzDGrwtdHauX+nzJuolR+TMpMnls+eF6zEwBjeXfxjFiwstjt0RxxLQk3WS8lJ0khuShJL8Uy+S1hLo0h1Q4I4GSSPCVzrm8qwsjCi0JdDkmaQ0JFclkefRq+txn/koALOHpgwVT5KRcRI9kjgR0RGu0fFSEZMLE0+Tg1O7ko0Q+411CANBGjjHsVTr1Gg65h+lZ6VfJh5nxetkMGy2/D40xDVJgQ0RzxYQYlBS0UERfhGjUl7TFJQQFPGU+BQaUfhOkckJAj55LDE0q0XoDuZdZamlUaZpqDmrQvBr9qJ9KgJEh3/LUA2ITpJPj1CLUOaR6FN01GpU+JUXFUSUoxJdjwaJBQEWuLsxHSx+KMnnECZcJknnR+k16+9wD3Xie5OAtMTziCdKHwv1TJFOBc7HUHcR6dNrFBqVK1UaVLoSu09jCf9CaPn98pmsx+m854DnD2cWZ/3pWSxM8IM11Dr/P1KDeYYBB6bJRApqy4gNUE9LERHTFdR9VbwVtxV1k9tRdgzmhqA+a7Zjb8lr8Skv6Bnn5SgC6VSrG+3PcdA2PjrcvvHBskPKhi6IDUn9C3/NUY/BkfXTrRUGlflVgpV7k6qRCowFxW39bLXpsDfsOSncaPsoSykJaiArvG3OsVU1eDkffRlAY4JUA9eFbkbhiGdK+E2+kCzSKFPIFXCVzZXXlVZT5RD6jHqGLH6Lt5Sxhi4e67iqRSo1ah+rO+yB7sAx2zUwuG87Nj3dv9DB0IMxxOkGyYkIC6CN9xAWUnCThxT3VOlUiBOK0cGOpQmpwtF8fjWscQquD6y1a36rJ2uM7KQtgq+u8dl0ZDbwedW8Xv2Uf0/BKYLOxQcHzUqMjKMPHFGMk21UQ1UyVNSUZNMEkZpOdgkdgrc73TXK8ctvam18rASsKCx7bSWuSXAyMbRzj3W394s5vbsxvCh9ej6vAPKC0cY0COdL4E5mEPQSipQ51EAU/FQrE2+Rbk6bSvjFvn7Eucp0oDE/rovtpmza7JCs562wLnwvrfEP8u10jLZi95D40jn0+wi9Af8fQauETkdtipvNkVBVUlmT0tT3lQ6VX1U7lEMTD1D6jOFH6oGWu0Q2FzH/7vgtNqxE7Dkr6KyP7X8uRLAkMX/zO/QhNiN3kDih+Up6Sru9vNb/rgK/hc8JLYwoz6dRs9MzlHKVNZVPFVNU9RO70f4PkEyOR/GBkDvhNvgy0LDEbzLtoKyKbPVtFm3QruQvxrDl8aSy6rRe9Kl03jX7tpj3wfj9unM8nj8agwMGwoncjN2QAhJPE+dUrpVXlY0VoNU2VDMSSFAVjLpIAILvPYm4h7TFcjnvoO6NLhhtoW3PLcgupW7jbzUv7bDJsjRznLR9tUB2YDeCuRP6TPvCPZW/b4HdxQhJ9QzkUECTPVS5lbSWsBcC12bWn1Yt1SMTPxAfzPNH34JVfQY4gfUPMZgv9i6tLd8tna2Abh4uZe73r5GwYLEH8YoyMnJbMrAyonKu8tZzVLRM9ol48fwtgHgE3YlSTbjRDRPLFUEWn5dtl78X/9faVwQWnRS8UtdPiUq+xS5/sfnYNriy13D8bruuL222LWdtU23Z7nGt2C5ibrIuq6+478Qv1G+tb0Wv5fEvcczzBTSsN5Y7rX/aRULK7k4s0aNUola0l41YgZjIGOIYbxeI1rVUydJnjsKKskW7gB18Gjfd9P/xZC9QLrztou4lLjyttS5brpzvCS+vrv1vF+8w7v9u6S8Wrx6vRvFb86N15vkefhVDe0fATOGRL1NKVfZXdVic2ViZqpl+2R4YdJeG1k9ThhACy9WG2UI+fSl6GLZJs0MyAXDl77Kvvm+pb+vvWC8nbsduAe2RLb1s+qybbIhsq+zG7jUvszJtNTJ5Yv51A9pJ2w4UEn+Vb5dAWNSZvtmc2WlY9th4FumUnlGTjigJUEToQZ38angNNKzysbJusIbwGS+WLmCu5O6qLviu2e0lLS9sIevPrD/rfqv7rI0uDTDX9JJ5yYAShvGM05H/lQ0YD1oa2qQbGRso2s0aR9lxV2OUzlCQzOnH+0MKvbv54Lbq9dRzo/NxspvyYHHTMXgwsi/rrl8tq2u56v9qiuqNqujrEGyt7xAyYPfQPZpEuYrrD+QUnxfJ2YTa15ti27Ha+ZnN2MRWVdIBjhFJUoSg/3o73fiXNmP0gHR+Mtey0TFIL10t7uyxqv3pwqlRqLBnjmhaKePrQ64ycoS4O/3FhVsM+lJm1hdZFJp6WsHbIlrXWmRYdlVhkj4Ms4hKw1g/x/uwN4X2LTSW81JyvvHtsLVuzK2xq9Oq4GmHKNwoH+fJKN7qlS0f8W42fjzFBX2M0JLsFrfYkloy2mUadBnk2IhW15OSj2hLW0dzhBlBXv8O/Ly61rneOFd1trMUsMyucKtwqSXntKcp5tQnsmjK66jvcvXO/t5G8k2f0zgWghjOWbQZlhlKWHvWidR6UTfNG0k4BZDDh0CBPl+8ZvnZ9wD0UfHarlyrJejO50imv6YjpoUn4am4rUzzO/uvROzL4pGe1ZcX9hil2OzY0teFVdfTmtBhDTIJ34eHhe/DewG3f3C9BrptNhnyvS5+qxgpZagsJ7Inv+jBK06u8bU3PREFwY0EkdCVRRcE2CFXw5cL1XhSzdBEzYSK8AefBMHDAYESf6A8pDkVdSawd6zQqmzoYidppsvnIefTqjsuQ/UJ/X+Fxw0cEgjU1RasltUWYlUr06xR3A+JzaBL60krh66F6wP3APS9Bji7szhuw2wBqY/oZCfWKFZpdavWML93RIBGiX6OxhLc1L5VX9X31REUB9I/j5KNvMvgyjrI64bThP2Bur5Eeat0YC9x7BJqLGijaDToUylqq5xv8TaPvcAFFEsUD1sRu1KVUxOSqNEVkDfOzg4pjWEMXEsXSPsGiULZPl8477MkrkUrs2nGKWxpIeoI6/3uu7SLPJ3D5Ep/DlnQ7xHvkqOSUtEjT+NPEg40DSFMrovcSjjIvIWEATc6ifSnL+rssSqe6f8ptWoHq+/vEfSZvDwDZ4nPDZgPxpFikbQROlBmD3QOV83CzjFNvUzby9uJu4X4AOU7DXTNL++saGrqaeip9CqgLGcvd/T8+4KCtoeli8ROdg8Qz8qP5A9ITzzOxg8yTxOPPw6WTVjLCgaDAGf5QHOabv9r0mqLqhZqCys+rQPxIjYOvWEEL8jfjChNxU83Tw5PGs7sDrxOic8jj0IPiw7tjS/KS8XoPy14KjImbekrt6p9qhyqpSvjLj6yOPg/vp0EQ4jFS0DM202zDelNx84zTj2Oso8Nj7XPkU72DYaKvgUV/tD35PIJ7mor8KrLaroq0mxq7qlyfHdFvaZClUaoyTwKysv0jH+NPY3PzooP8lCLkUtRqJFckDCNOsgCwnx6SrPR7zrsKiqM6lZqnyurbQZwdbRKOgt/UQPqhnCIaEmHizFLf4xXDcoPLVB70Z8SUxKsUekQm82DCIQCDDovc0AvLKxA6zRqVKra64qtUrAQM9G4S3y8wKbEOYYniDpJoAtJDIVOPE+skSKSMFLikyLSlpEWjrZJikNfu7D0lC+WrGqqsunhqfYqk2wyrkIyCnY8evn+8gJtBO7GLUeASPVKBIwdjcWP49G60yCUa9SUFFsTUpFOzVYH9gBt+FxyKa3oK5gqaandqlVrGaxwbsOyCjY4+fr9u3/1wgiEekZkSG0KuExbjqJQ75LLVE8VfNWZVVmUfJJtjs5JScIVOjJzV+69a9yqcmmvabwqEuuXrYOwJ3NNdjI4sfssvSx++4CEAjJEOAZFSZ3NBZA4ErhUWNWZFlzWeZWfVFURT0zHxtq/eLgcMl/uCmvPqlIp+CnK6q9r4q1dr/3yL7RYNwi5YztzfK9+C8D2gkyFjUkhjG6PLFH4FEMWTpcQl5xXY5Zx1E/SHg2nx2z/Z/iUsqRuiyvyKh7pQ+mmqhFrJKxG7mcw9bNMdgc4brmiOlN8+v30QCJBzoT6SB3Loc7KknyUb9aOV6JYEhhl18dW2VRTkICLQ4S9/hr3fPFx7V/q62m9qP8o5SkmabUqwOzBboYwPvDCcpqz+PWjt7J5N3tY/d3BXIbgysVOt1ITFQQXJRiZWYIaNJnlmb5Yr9b8E9JPfUjrQk67ubT6MC2s2aqIaVCpFOkP6UnqXStL7Jbtpu7Y8HMxQXLz8+f0kzZ/uAd6bf2fQUaGGgkmzGNQIxOe1ntYZ1lJWYiZhxllmJKWxBP4zuvJGUJHfCV1RfCtbOyqxKnnKRUo8qkkKZKq86w8bOOuLa5k7xNvwHBG8QayHPNdNUr3n3qqvuWDO8f9DN4QiRROF3kY5ppSmzzbA1sWGkQZfZbp04lPAgkFAZ+7aHXMMhYvAuyKqy6q9OqT63Jroyxg7P+tmi96r+FwnjDKMQeyRTLgc5D1L3cn+lq8xUFHBcyKIg7F0u7Vt1ei2XBaTBsSW19bSxrx2a2Xt1TTkJgMekYKf9Y57rSXcMauBOymaxHqruqh6wfrOOsBLHftPu1OriXuaO7u7xmvEG+1bvDwJfIlda14mzwLwVrHDo2u0vpWXViaWjsbARv/G5wbh5qFmXqWmNMUjsxJWYOGvdC4rnShMROvBy287IVsjOxh7BDskyyhrPqtKW4/Lv6ueS1+LaAtd24i7zZwrHJCdIE4cH7dw+oJRs5qEq8VUNfWGXaaZBqVWw7a3Jo3GMvXLpO3z8oKboV9/qz63jbe89+yF3EDMB+vpu82r5avZK7uLj+tuG0s7PysVGxy6+jr0KvhrNquc/GhNBs5qn9DhRZK0hEA1FYWg5gw2VvZ3VnMGZwYvFcJFZhTiBBoi59GVwFRfdE6KDa69GCygXIesXjxwzHKMTSw0DErcKCv8S5MLbAsu2yELSrt3G7LMTA0xzpkQLOGqYymES6USJby19VY/hkSmRPY2de4FhCTwVFezi2Jc8Sogb/74/kNNsb16zQHM1Hy73JssPJxSfDrL/bvU+8drf4tUu2sLmhvA7HHtOt4Sb0UxA1JaA6Y0nXVEdaWV/1YUxinmDxXYtWKU0AQBYxLBxfBsD1rOm23y/ZfdWf0PrOx807y9zEO7+lun22drOasMeueK8XsV+3b7+7zZ7hCPkAFTst9D6VTP1Uf1rqW5BbNln/VIxM10ONOBgnchRkAzv5Qe5258jhBNrz1AzWfNIZ0PXJt8WTwJu7RLnvtvazdrezvO7EYtSc588CixszNONF4VDDVmdajVqGWXdWuFGxSAQ/Py5JIIASMAcx/vX2wu5M53zgFdx5073K6sGjujC0GrDRryWwdbKNt67BDdW+66oI5SVaOA5FxE7vU/lV41SJUpFMHUWrO08x+iInFuMJi/0x9ovwkuda4A/Ysc/ixqu9N7gss1CwI68fsZi0LL21yg7i3PynGzI0gkSGTLFSNFQmUxxPZUidP041tygSHjURFQcpAd/8zPei7vHjuNd+ypbBQrnnsreuS63QrduxjLpQygzhsP40Gp0zm0JgS6dPA1LRUIdMvEUfPjgx0yeJHZMTaQe7/0X2Le8z6G7cxs1sw1W5CLQCr1iun68zs068EsvZ4Q4AixxmNvBC3klYTeNOvE2BSTBCGjl+LTkmMh4HFioO1QYm/PrxtuOi1U7HNrtts5+tuasLrVqy4LwXz7boZwuMKFM9/Uh7TuhOyUzWRxNCjDgwMLco4iCgG4MW6BA/CYj/uO8s3tPLFr9etUKwkK6Jr9SyD777z+XusA4wKoE9v0dsTTVQrE4oSgxDajxyNHIs4yeOIEIZGRGoBZf1e+IO0PC/JbTWrcep+am/rXi2RMb54GIBTCDCNiRFpEqUTWpMqUgtQnU72jJiLpUqPygiIjYaNQ79/GjoKNXVv5uxs6mdptWmxanCsnbEsN0jAQki7TdHRWdLgk1lTJxGDENUO3c1LjDiK9ooRSLxFnQHb+25103DD7R9qRCkBKP1pBys5rrN0uXyshQ6LyY/z0jXSyVMWUpMRq9BSD8SPRA9+ziMMxwqXBpCBL7n2MvXti2p0KJUoOeg3qWjsMDE3+FhBqol+jlqRiZMC02SSsNJ/0bKQ0FBTD8DPDY4IjBdJMINNPBx0Wy55qo/o/6e/57RomirPL6b2Y77rhgIMP89wUUlSQNJYEe/RtpEQEXJRQVGFkP4OSYrGBVm9pbVdLqTp3ydi5kFmUqdTaT6ssbMm+0LD7soCDYQQbVCB0UJRkpGBEUDRi5H2EjoRe1BBjZlH8EAid+hwbGsvKBlm+mYUJvTomGw9MZI5EcEEB+1LrY6NT9OQNZBokK3QzdHSUmiTV5N9knRQdsvBRjn9kTSXLdEpGCbrZe5l4Cch6XwtejND+usCTUe1C5wOE48yD0TPqJBPEVYSU9QR1KcU2dPGkdVNkca6fec0zu0UqO3mVaWLJaxmrqkVbS3y47oWgVaG24pbjIoOWM89D1HQpNIjkzyUORVP1dJU65JITvnIeD9BNe5uGKkVpmulC2Uf5eDneGoqLxs1mnwMgpTHBQq6y+DNfY62z9RRfNMsFIEWatbCV30WelRnUK7J8AFn99dvjmoY5t+lqmVJJg5nx2pjLrfzyro5f5REfccEyanKysyozkCQhFLPFRFWslf22J+Y1teZFNpP/Qh9PpZ1HC3l6MMmFeUYpN6lsicoaemtr/IIN7O8voADQ+OGKQfgiUZL405lkLnTJxXhF0eYfph+19XV1JHKy+bDTDl9cGJqlmdmJYalGOVKJmkoKGsfL1u0SXkNvXLBW4PtBgUICgpiTOOPD1I31MHW5phUmXeZuxkyl6VVPE/ex8M+oDWCLp7p2acEJiTlouYX51rpvqwor/Yz6zfqu0N+iYEnAy5E9EdcCjiNMdCvE+uWGdgeGQGZ/hmyWMmW/VLHjFXD6jq08o5tKukDZ11mfCYsZxxoqiqcraiw/vP/dxy6FXxtPj0/yEJ9xOUHt0tRDmuRlxUZFy9YoBlgGV0YwBeU1PKQM8jqgLN3qvD76+ApXafyp5qoLeitKcDsUC7mMda0KDZX+Ah56DvPfZF/ecGEhJiId4xoz/jSyJVTlx7YC9iwWFwXWFVxkfDMrAXh/rw3VfDLrOFqjClaaMepBCnjqoQsKa4gMKnyinVx9045WbtwPQE/VIGJg+YG0IpXTfQREhPwVYwXIJfSWARX25ZH1HaQrQtZBUs91rdVsl/ueKvIqpsqKqoQ6rfrhq0S7v6wYXKQdGc2Gjeo+M654TtQPUcAgUQACG2LZY8sUh2UqRZkF1rX/Ne/lwvWMRPRUPPLlMTJPRZ3mvKAbwotUmv0axqreaw8bVSubi/Bsh0zRLU9tdc3kzfw+Ql6lry7fjkBIQTCiXmNeRERE8rV2hahV2iXfxcWllNUoVEUDG+F90Aj+Rszg6+g7Rcr6itSq5jr0WxqLXivDPDu8g4zLnRKtTL1gjbcd815e/taPjjBmEXlyfON3pE+054VXVYYloQWoFXkVL9SdI8QCnvEpn8A+Xm0n3Hk8DhvBa5orhUuOe6e75Kw+XF4sm5y2zSWNLT09vXi97E5dbw1/2AD34eGTLyQjZMklNxV4dZeVl9V89TzkwaQSkvnhmW/wzqadj6y9TCCb7vuI+6prplvp/AbMKwxETGmMckzCzKtsz/zkXTkNxN5xb3ngilGaotPDobRk5Pt1NgViFXTVXuUZ1LbkIqMscapQLg69/WtMsAxFK+GroVuvW5TrxXvdnBQsVPxyTJcsvGzKjQftLb1oLek+li+Y8MCyFSMzxCLUpiUjBVpFVUVFBQ/kqZPoYtiRknAYjqDtp1zu/I3sLdwsTC98RPxSzKj8gmyvnIdsh/xr/G+cpE0AvYruZT9T0KCSBfNSlDoUuYUUxU+lWLVI9RREsRQHov+hv4BF3tPNww0jbJ98UNxU3G9sZcx8/Itchqx4zGD8VsxevFMsuA1u3hr/GMCF4iQTj9RnxRgVaKWB1auli1VO5MJUHsLbkWLQDv7HbekNb2z+3Ke8Z0xUbFZ8aXxITA7L2xvPS+lcKIyKLSzd+d82gMLSZbOTFGAFAPVqZY9VcLVatOzkKaNc8iAg0M9mnkxNa+zbLLKsv4ybDHYcWBw0i+LL1qu6u69rxXwUXOfN7l9RMWPjBFRKJSFll4XB5co1lVVFdJpDhoJHcJEPIk42DZzNLCzlbLrcnIx4fDWMAZu4+3ybXitkm7VMJj0YXo7wUZJmc9/E4fVwRbe1wYW2ZUA0r2ODglfQut+MDq5uKR2UvW49DQy4LDBrwwtT2xIq1jrEixK7pxyovmhgq7LT5FeVMaXOJdOF5QWqVR0ELhLMIWKgPj8WXke9xb12TTz896zEvD67sxs3awc675rnC0y8A51nr4Xx3RPFJPFlokX3BgCl+MVpRLTTmNJE0P//2E7zDoOd+f2RjPn8cgvXq0lazUp2+mW6lxslXFK+KwC4Yx0UiOWAhfxmH5YM1abk+bPhMo+xSjA7H5Ou555sHdxNHBw463DK95qaKjIKTTptO06s8l9nQk/UHiVVliumVoZgJgHVSRRWctnRwDC3L41u1g5M3aD83Tv1W0hqiNoJ6dyp3aokWv9cvC89wiJUUvWMphs2SbY8NdWFNhQjwuhBvmDvsBrvuv7+/mF9WRwzaxOqWXnhWdoZ6vp924ANgTBtQylUy4WvZg92CSW+ZQ9UTpMVkgIhd2DIr/gfc866DZmcW+s5WlZZwlmaKbY6OZtYDWkgX5Mt5OjV4aY8BidV1OVtxLxTnhK4QgXhPwCpD+vOzG05+6aqiRmqqUIZNilUCeSLMk2a0J0DTcTNtY+F1BXIFVuEgeOCUqVR/eGrES+AfM+sXke81KtYujkpjrlA2XTJ5ArwLTlv67LStNOlzvYWRgKFxAVKZI5j5KMzkq9yNUGD8HJvGC1Fy5S6TumCCTp5JUmBCnb8HE7XscBkAYUxZcX106WTlTEErrQOk3bzBVKhghWRIH+9fbw7zrpWyZFJTTki+Xp6NXvFjkpRVZOpVOGlfQWXtY31JgTaRGZEL6PFo2LTC2IcMIOeswyEeus5uolFWTfJbkocO43NnyBeosFUiMVSBZtFhQU/hOWUycSiVHYkSpOSUu9xWY9qrPzrGfnliV3ZEClcOclq8fzif5MyJqPOZMdVLKUVpPQkwFSlZILEbYRaVA5TdYJEgHxN9ev0GpmJzGlueWHZ1Zq57EpeghD/Ar3z4VSaVL40lGSVVHQUmrSrBKw0cLQtIz/Bve9/bUurQ3oqeZxJbUmEmhaLJqzjvwlxG8Ksc67kNpRrJGqUUNRwdKO08hURBO0EboNu8bOPcJ0BuzrqE9mpOYQZsKpOOzds1W7w4Omye4NKY7Tj35PQ1AlkOjRnFLRE0iTQFI3jwGJpwDB9wBvHOoCJ8Fm4Gc1qFkrWHBwd0g/IYVhCc9M5A4ZjzgPupC+kcZTpVRbVK1UKdJ3Dg1Huj40NLTtqWopaFDn6GgpadDtTjKbuciAiwVPSJ6K5sxmjWKOP09g0MAShBPYlFjUJ5L6T6IKMEGhOH7wn+vHKZColGi1KZTsLG/ftRM6vj+tRBjHKoksSkBLww1VTxkRC5LB1CHUsxPcEjZOXggd/1M2gXBpa5fpmejWKT7p9WwLr9X0hbkyfnqBoYSzBmYIZEnhS1YNT4/DUeTTThQIlBTTOpCDTAlFBbyvdAiuyKvAqm2ptanCq2ntnXEtdU/5ir2XgXoDroYJx+jJa0tqjcCQxVLzE8JU9BS3E/ASJo5aCIGAXjffMYttqKtTKrpqbOsc7IVvMfJ/Ncn59v0ev7EBswP0RbqHtsn0DSnP0tHdE7eUQpSJVCCSfA8qCbMCJLqm88Zvt2yAK6grJyu3LIeuuDCqc2v2Djll+/e+Oj9PAXBDp8ZiSXJMvc+XUhoThpSZ1I6UFdLk0BzLo8TEfLJ1kzC3bflscSuEa80smu3Dr8oyBPUOt2q5mvxyffH/ecFPRFaHQosrzh+QhFKw0/AUsFTFVEfTCBBSjAjGEf8/N+cylK9gbe/sxm0F7bYu0jBTMnMz4HWYdye4pvmluwS8B38xwpaHcMt5TrNRrpOQVPRVZpUy1GMSwI/1yzOEJDzldlPxwm9n7bTs0K0s7YNvHvBnMmy0L7X2d104Bbl6en48IL5PQSyE9QixTA0QWVKtVDjUzhV7lTnUHNJHj2IJ7MOcvOJ2dvHNbxitgi0MrWkt4y76L/ixQPMnNDB01fXa9qm3gLly+8t/FwNRh+wMplAakuVUVhVKVdKV5xUTE5+Ql4x5hZx+y3jt86FwS64i7RAs66zEbdDvKXA/8UvygfPW9F+1SvaseCu6MP1vgUoFwwsLj/hSxBVCFqKXBNdwFtHWLdPeD/aK1wSCvj44f/O9cICvE63yrdLuD65fLwovW6/2sDLwLvCO8WYyUrQYdii5oD77RB5KGo8k0vLU7ZZQlwsXb9ZCFUxS0I+Gyf+DRHx29njyLy/m7lKt8G2gLldvNu/r8E+w1zEjcLWwB7CrsToy1LUrOOy+O8ROSsjQ+JQWFnYXlNiWWPvYONcIlO/Q68tTxYe+u3fP8xCxD682bnTuai8Er2IvQbAkb8SvZ69Pb/ywMrCK8qK1rvp/QZcId077EwdWP1f8WNZZX9krGCIWONHNTVHHR4FIusJ16rJJr+nvMy9m759vhy8R77tvCO6c7iUunq4ib2bxw/a8O8YDEApVUP4UcherGR1Z+dmXmPpXmhTLkQFMUMXufzt53LWmMyUxLvCyb/Fvui89LpEts2yx69drsau77A5tt/DsdhV+X8eMT0QUdVetmf1a7FsWmtGZu9d4lEOP6AmEgmD8njiRdj9zvXFu79/vWW7L7d/scmqRqWIowKnMa2MudbIHOi0ChAwvEu8XddkHmnDaMVnjWGlVspFUDCxFbUEqvJV5cTZg9O8zknM9Mbxwai4/rGrrD6qmqgOq0SymsDz1ob54CA2QiRXnGSxanpsp2qOZhheZkyXNXkZpv4P7f/i6dqO2DLPz8jKwgi/aLZVrO2lbqJXoNakGbE3wsfflwerMGlKllzSZXNqv2pQZ1pdYE+yPaQpWhZnBQ359+yz5nniG9krzz/BdbQ1qtiixZ+en4ilL7KaxrDqIxRmOXZSFmEWaFlpkWdBYH5V20KELn0ZsAbu+BnsN+Ug3IHSpcRhuNasj6GmmjGYpZcYnyqrHcW96gIWSzyJVTZgK2aTZupgC1rGSlg3zSO0EdgJuv6U9L/snd6K0PK+27DDpOWb6JdpmByeo6wPxx7vYRv3PupWkGJyZu5lTWDkVmlJsjkGK14bChK4BGv3AOv51nbDLbHpomWZ05PxkpuWIp+8slXXlQJPKQpGKVgbYD5gm1rLUhdENDcJL6AlHhlwEZMFe/yI5/DTk73HqQyg+Zw4nSilRbPX0Afy8Bv7PgJTM1ttXCNZrFFRR5hAvTdfLYolmBsuEYMDAe0H0hG6hKexnLaWxZaBm/Kk6Lvt4t8MTTFNSDRVy1c3VZhSSEk3P2c4uzIWL5ckURmECaTszNG0upGnoZ2ymH6Zgp5Uq9rEr+tCFAM1cEdjUetTe1N6UNhKvkTMQIM88jnEMbgg7gjB5yzJQbKlojScNZrMnUWofLwW3r0HmiigPv1KR0/oTMlIK0OfP0E8KT6AO+MzTCKgDQTwI9AbtrGmiZ6KnFmfG6kIuszZqv46IKk3WEGZRYNEjEOMQfxAREDeP7M7gjdOK2wWvfMB0tK1aagbocyfJ6Ibq7q9xdvM/sgcCTAoO24/tEIZQjJDdUP3QsRCYkJUO+MrjQ2+6TvG2rGmp9OikKHEpjKyycdC6AcJfSDKLQ02bTp7OXs6vjx3QDdDI0W/Qn86HiihCrLkrcUysEWnLKRjpiytkrrR0unx0wziIcoskDF3NLs2/zprP+JDSUnTSQpHYztIIykCetqPvLOtmqZVpTeoc7BiwLTXD/SaDMMdfCcVLFUvYDT5OVlBskYaSiFJQkOANcMZTPUz0965iK1iqAeo86s6tejGq92E9U0I3xW8Hy0nzCocMzI6AEM1SZBMNky5RP4zGhpj9OHRP7qVruWquqqrr0q6JcrO3unzOgRCDv4TixywJeouoDlAQ9RJmUwbTLVFlzU+GZfzvdBuuq6vmKz5rPmxX7vnyaTbDvEi/0UKuxFiHP8l/zCzPNtGPUzZT7RPS0q4PFIkOADH2lrBiLMgrmysr6/rtsTBwc+n4B3wSv2BCNkTlxxGJrAzDEGjSetOaFAGTi9FADOjFSTwCs/xu+SxGa4+reywd7mDxrTUhuTF7zT4LwHnDN0YcyUNM55BLkvlUBhTGVGXSd45ox5W+jfXDsF3tEOwpK+yshW6bsRY0FHcquR47EfzFfzoCPQYhyn+OcVG504XU5lSO04bRNUvzhE67vXQQ7+atkGzgLRQuFW+7sYR0JLXN97A41fsUvUYA1EVpSk+PLhJm1FbVf9UnFGiSBg3JBqu9yTYbcQOuby0VrRPt1i8D8MVylHQFdc13THjmuxB+KAK4R/VMo5DXE0RU2lVR1RRT41DUC0sDynuh9Ikwsm5NbZUtm24pb3Cw33Jq85K047X3t3r5APxHwQiGRAvpEEKTVBTq1XbVQRT6EoiO1IiRQMe5nzRO8RmvbS6droUvnDCnsY4yrjMrc5+0qHW4t4O653+AhSdK3A/Ek3UU8tXaVjyVbxObkLBLBEQsvBu2K7HVb9xu8O69rs9v8jB3cQRx+jIp8ljy1rQEdrS6Gj/cRfsL8JCHU9mVpBZqlkZV+dPoERAL3EUNvhb4CfQ4sbswWbAf8FVw4nE8MTExLbDvMKGwkTF5Mlm1UXmsv56GJUya0XLUS1XHFqGWWdXNVCuRPcwihlV/GnoatfxzWHHrMXAxVXGtMWzxQvDdsCnvkm+tcCxxH/O3Nsd8toNLSnpPxJOHFZPWvJbNVuNV+VP10PMMDwYJwAM7QvcytFqzHLJGcfwxavEC8FCvnK7u7i9t9+4n7zqxDbSheeeACQcMDbARupRNFjEWp9a/VeOUm1IzDkKJ5sSYfxs64Hgv9c20SXP28whym3EGcIFvla6D7kMujO7ib/EyCLZKe8TCu0lNj3ETDlVyFriXIxc4FmNU69JAzykKP4VPwIW8ADlWNvd2KzVf9G5zB/JMcTxwPy6DLhRtZy0Mrbiu6rFTdUq7I8J0yWmO+RLrVPfWKJa61iVVXpPBEWLNssidhKDAcfzbuon4j/aBdbk0jLNX8f0v4e5wLTAsVyxcLFYtK+6/MUd1ynxnBJ0LK8/Mk3WU+lXq1ioV89T50uaQIQzMyG9E0QDGPkr8D7qXOOQ4LnbwNW1yifFB7wjtpGxrK9RroWvNbViwX3R8Oq2Cn8oRz1jSn9S+FVxVolVzlHTS2RAljI1JpIVqgeC+n7yEe285PzerNf/zTfH3MC1u/yzga9hr2Kw+rTAvVnMT98s+W4ZPDNDQmxMyVF3U3xSF1CoS3RELDrILFcdiA7qBBX+l/f77pbmG97j1U3OJsdZv/u3trNssbKwtLMPuj3FA9V669YJoiMSORVFFksgTf5MoEtjSU1BhTi/LbUkIRumEVcKDga3+hX0ZOsV4JHVqsp2wgi7mLTzsjG06LbxvKXIE9qU8mESLCv4OnJDbEmbS4pKWEiERDE9djShK2Qj+xY+DjAKnwSN/FbyCOu03+jRscluw5C837q5u6W+KMRB0MvkRf3tFfUq2DfHPlJC9kNNQ8xAYzv8NIAuPifgIh0efhh7EX4KQwLp9PLl89cxynzCS7yQuaC4pbqLwNvMleCv+T4S0Sc9NdM9aUH/QIU+pji9NGMuFSd1IbkaTxZKEmMKZAV/9sXnI9nXzBjEOL5fu+C7J7wnwWXKMdz08uALmh8OLJwzhzcdOUE4cjMAMAYrBCZaJfEfrht3Eu8KwwGM8JbgJNIRxmPAfb3ovHe+ycLty/XbTPXjDmsjNjA8NzI5zDhgNm4yDi0BKBEktCBBHhEa9BOxC7D/pfAJ4jDTI8nDwqjAKsEDxPnJgtTW5Mn8IxMMJNAtrTDNMakxljAWL/gr9yodKgAnMSacIREXWgg79dHhs9JqyJfEp8JDw5XHLM8O3ZLzwwh1HLcm5iwTMCwv/i2mKwMp8CeaJ/AmRCQ+H0kYOwrM+VvmYtfLzFPHVMYPx7fJ4dEx3o/wagRVFpYgziRPJZcmgiUdJvklICaYJp8mvyPLHRkQe//c6ZfZt8/KydTHTMhby5nTdOG/9JsHaRbCHnMjlSWrJgUmnSSXJQ8nVybSJfIiWhs9Dkf63eWN12/PS8wby7HMHdIj3GHs6v9RDyYaER7yITkiVyP/JC8nICqmKxQrYyl0I0wYEAMo7bvb49DHzHvL/swF0Y/ZGOn++csJjxS5GPUcRB70H1AiYCQlJ7InGCdFJM4b4Qz1+O3kvdaszzHNa82K0MzWXeG07wX+CgpGETEXQhqgHGEfiSNmJgYpgClkJ4ggcRNM/S7ontj30UvOc8570IzVu94+7u77EAnKEMIVMBjCGyYfRiPwJS4poimaJvkgphLk/L/orNrb0+TQnNCJ0yHZNOT28tf+oAfyDMMQ0RQIGHYduyLBJn8peCpZJuIdoA/x+mbn79lT1BfSwdLk14PfPOgy8078fQPVBoALZhERF7UeCiZbKtQrriolJt8aqwYl8cbfatYA09TSidX12kDjtu3998r/HgXuCLoPRxaTHgcmsiqWLCIsTyglHzcNBPdT49DYntMg0zvVktmE4Ybqw/K3+hgAugZsDIkTrhzQJOgpaiyALJApdiHbEIL6WecH23vVj9T/1QzaiuCP6MnwEPbe++UBXgiXEJsaWSLrKGQsZy3DKgYkBRa7AJjr+N7n19fVPtaS2cDepOVd66XvivRX+hEBqgw4Fw4hCig0LEQtdivCJCEXOQHB7MXe+deo1UbWl9nS3obkdOnK7s3zqfhM/0AJShRnIGIo6SzYLWMsKCcJHKEJEfVJ5Mrb3NfR18LZEd6Y4xfoGus57nfxLfjfAvIPKR2SJggsNy7QLUAroCOEFM3/V+yC4J/b+tkY2x7dUeET5TDoIOv/7fbyw/vJCAQYTyMqKrgtXS7kLFwndBsrCG7zP+Sv3GvZndn92t7dC+FP5CfmCOhW61jxYvrBCPQXyCNDKhEttC1OK7olxxk6Bwzz9eUH33zcTNyg3UffYeEn4yjlKudn6r3wEPtcCsoZ9SQNK9Et6y2gK9gldxr+CPv2XOmK4aLdttze3ZXfkeFj4oHiAeTT5o/suvbZBS8VdSETKcgswC20LOoozCA0Ep4AbvGg53Ti9d8h3xvgIOBu4F7gTOBy4djjqOra9oAGHhhKI0Yp9ys+LHIqdSWyG88MuPtL7yLoNeRN4iDiqOLo4iziGuH53yXhu+Ow6sz2vAZcFrAiLSnzK/4rmCrNJlgfNBNMBav3Re6i6YznQOcP5y3mquWq5BzjZ+Jy49XnaO/i/CINVhomI8MnzSkMKjMoIiTUG7AP5AGg9ibvEOtq6Kfm6uR549DhXeC83pfe3N+D43nsgfqKCokY7CHpJicpOCm/J/MiURvqEEkFLvsL8/jtreuL6onqAeli5qPjO+FY4FvghuIp5+Hvof1RDeoZjCHaJVwnMidaJekhmxstE4sJMwD592vzA/DS7Rfrqeim5djiROEM4M7fCOKp5rHuOfvgCTEX7h4zIwUl3SSEI7AgRxs7FKcLYQPh/GD4dvU68x3xKO5n6j7my+KG4N/fGeAM4p3mFfDs/NIL0BfMHhYilCO3IyUiXh+nGgUUZAxKBa3/Afsm99j0DPFY7pvqz+bF44HhduCD4E7iZeYG7WH4DwaVEZEZLx46IFkgMh89HZwZvhSpD/wJlwRZAD39uvoF+Bz00O8/62vnruQw44/i0OIx5DLoWu8t+q0GVhEbGBMc+R2AHqId2xt5GHEUkg9DCgkGtwK0/3D9T/pu96nyCO4s6vfm0eS7437jwuRw5/Xs0fURARQMGxTzGEgbEBwGHL8aVhgJFV4RfA3UCfUG+QMYAUf+LftE91vyk+0i6qrnQObI5VPmJOii68PxbvvaBMkNxBPcFkAYiRj9F4YWJxRcEbUNzwmVBo4ENQLM/9v8LvkU9Xjxk+2L6mXoa+dW5zvoIOrJ7XbzYfy8BW8NhBLyFOkV3BXZFBIT7xCbDrEL+ggHB+sEdwNeAsP/3vvh95Tzf++J7Mjqs+lS6QHqzuuJ7hb0MvzTBG0LUxAtEz0UexT1E6wSoxBGDk0M+wn6B7MG1AT7AmEANP0R+T71XPE+7kfsc+t66zfswe3N8Ir1Zf0rBYcLUA8gEfAR7hFFER8QPg6VDNgKIwk5CG0HHAbJA+8ACf3k+Ab0+/Dz7mDt7ewd7RLu3u8589n5GQAUBicLJw7UD9wPvw/cDoUNkwzZCpoJ+QcEB+cGOgXnAqX/TfuB98bzWPGv79ru3u6N7+/wzvMH+Bf+pAQcCYsLGw2aDYMNugzzCwwLgAknCdAI0AfpBnsFzAOhAKf8xPgG9X3y//Bk8IbwAPF/8vj0U/nP/ucDTQihCqoLVwxIDBMMZgvRCmcK2Qm9Cf0IjQc5BRQCUf67+Qf2nfMW8nXxavHe8dby4/T2+PL9xgJvBtoISQrFCtMKjAqfCYcJbAlXCQoJbgiRB9EFqQI5/9b6Kve49IHz6fLi8oXz6/RJ9wD7tP8dBBcHYQgtCTgJKgm9CKsIoAhMCFAIkgjyB9AGTwRkAYD8uPhd9rT0NvQr9Gv0pfVv9yP7JP8HA8IFEAcACHoIWghkCBEIIQg/CFsICQgIB8AFIANT/5j7bfhr9qL1UvWP9S72kfcv+sf9vwCtA2UFXgbHBvAGIgcnBxMHnwfDB3MH5QaiBSoDrf8G/F75bvei9mj2dvY095X4rvqw/VUAuAKAA1kEGQUSBY4FBAY4BpUGywa+BsUFGARWAaH9i/qZ+Lv3aPeL9zT4cPm8+5j+KwEEAx8E8wQ1BVkFcQXTBSMGjgbpBrwG1wU4BLUBWv4x+4P5xfiM+Lz4Y/mw+nj8xf6fAEcC6QIxA4oDDwSXBC4FuAUZBuMFbgVZBDwCJ/9P/I36svlo+Zn5I/oa+4z8Xv7s/yEB8wGTAuMCewMdBMcEJwVvBV8FxgSjA58ByP6S/Cr7nfqD+r76Wvtn/MP9Nv9JABkBpAEhApkCQgPjA1gEkwSeBGcEzANTAj0ACf59/Mn7ovvH+zT83Pz5/fL+wv9QAN8ARAGvATECxQInA3ADewNBA64ChgG//yn+LP3G/Lf83fw1/b79c/42/7D/JQBsAMcATQHhAU4CkgKsAqgCbwL8ARMBwv+g/gv+zP3U/f/9R/6h/gD/Xf+u//f/WwCWAPQAUQGQAaQBnQGAAT0BtgAJAFH/+f7c/uH+9/4f/0v/fP+u/9b/9f8NADEAWgB7AI0AjQCCAHEAVwAzAAsA7//n/+3/+f8=`},{name:`shoulder_left`,sampleRate:12e3,frames:7440,wav:`UklGRkQ6AABXQVZFZm10IBAAAAABAAEA4C4AAMBdAAACABAAZGF0YSA6AADg/wsAOQByAMIAMQGzATsCFP0K/Rf9Kv1O/Ur9I/3q/J/8Q/yn+xL7sPpU+gr6APor+r36efvD/HP+OADiAcoDMQVRBi0HqAfsB9sHQAfkBkAGBgYoBiwGMgdkCE4KkwzYDu4Qve4E8PfxdfPy9GL1svUY9Sr09/Ig8q/w4O8o73rvRvDo8d30kPhI/FIBbwUhCd8Lmw4JEGAQ8g+PD84NYQyeC8AKKQoWCycNaxBjFJkYnhzHHxEj3OKt5f/nbOlW6fPoOej25hvl+OOX4nPhKOKT47vluOkX70r1c/tRAjkK6Q83FO4XBhrkGY4ZHxj3FS0TuhAfEO4P3xB5FXsa8CCvJ+MtOTMWN9fYYN3S31jhouGo4Bjfddu21z7W19RB1N/V49fA2q3fV+cu89f7WwVhD8wVvxqWHwgifyIUHwQc9xh6Ff0SoBFNFMIXVBxCJVcvRTYcPrREjErWx+XL388j0nXS6tGczjzNtsi+xV7EpcTQxVvIMc4d2CDhHPHC/84L+hWqH0Il3CcRKNAmTSMzHsgbvBm3FJcUHhu9ImErQjTCQGhKDlOkWrBgaWQPwJLDmsaPxlfEE8LsvFy5Y7fvsYOxkrO3ts685sT/0oHi7fFJBKsSLBzHJeErfi7eLWMoaCJuHq0aMhYjEYYSIBbiHZ4ptjZuQklO0Fh6Ya1m/Glbvi7CI8LhwAa+hLj+sVevva3urb6t6rICuVDATczz3BzrEvuxB1oYuB9mJRQp5SZyJU0hMRyJGKMQ9g5TDWAOYBaxHmkrhjdTQu5P5VfUYLxme2ppbHO2d7hmuGK1BLXqr96rn6lyqJ+oM6pFrmW1Jrr4xMXX2uqN/SMN+RtXJvspTC3bLZAmJB/EF9gRnAp9CVYIaQ4SGY8kbjNkQqFPo1rqYOdnNWxybRxv9LHFruera6jypWGi6J9un0Gfe6GXpeiqH7PHvj7MQdu+7M38tApXEiUVpxlOG68dqRyCF1gSlgpJDLkNZA6RESsW6R9uKrw2bUSyTgpbAGJ+aM5sPG9Ice2zfrR4tFiuOKsupnSjraCjoQukwKWZq7q1ZsHB0PfehPBE/h8OPB0IIhAkdiR9JIgk0B9gGLwTEg9zDcwLOw4QEZQXbyAyMNs7+Ee7UE1btmPJZzNrqW5ZbhqusKvhq16pIKU2pvKk76Q5pSCmt6vesQS6P8bNzofcSee7+OQFZQ0wEwUbORkIGYMXTxS4Ed0N8QrWCEMIsQ3pFEEbbiPGKo85vkalUWZZbF+yZKZnb2vLbAxsTmsZqYqmyaRAol6if5+lnoqgnaN7qiuxgrrCw8bP7OG78bj8uAb7DL4MTw+2EtUP+QtnCgoF/AC0+9P8cfy2ANECdQZPDL4daCmiNtNCtUtDUPhYmV+FYgxieGJHYgNi56THpS2kEKNOoh6iRaRNpk6p3KqurVi1+739yjbYV+Dj7K75MwnIEmcUoBIgFPoUxxLJDUAHFf+69832sfvr+vT8NAFkDswb4SmXOeND9ksoVMVZwl/kYI5ijGEqYcVf8aYSo+ShyJ9rnrKdzp57oOyjIKoUtMK6ksdz1IXgie5P+80CFQkqDosRmhOkEHkNrwiAAUj9Xvgp9B/zLfOr+2//4AoWFwslGDUgQTRLmFH1Uxta11uhXV5eZV06XV1cZVs1reCqjKpXqM+pXqmzqFSsrLFttx2+yMpg2CHeG+qw+ngCfgfKD6QUfw9SC48M0wfz/4P9jfk59X/zjPY9/gwBHAuZF7gg6CmvM+I6vUFpSbhPk1JSUxlU/lTJVJFU71PfUWqpMKhTqBqnJqdJqAirpa05sMi2hr35wt7L69c64ArqcvMi/Dv/8AGGBMwEXv4M/7z4oPY38y/x0PKv9r/5vACaBTsRKRq2JAkvbjdqP/FHQE0eUFBSIFNNUyBUuFOtUDdNr6xYrMCrbatkqxCsCK+1s8i48b1OxdnNgdki6DfyIfi1/j8Fzwc/CeMHKAKJ/Kb36fIt8Eftme9c8aP0ifqNAGoLMxZqIRAqJDL4OgNCwUdcTJhNtk6sTjlQSE8NTRFLUkmyq/SpralQqYupkKrhrKqwaLWwvLrIutL/3RzqA/Mb+WD/qQJlBBcCeAHc/RX4I/Qc83vvwu347ib10/f7AI8LvhhvIu4tQzhRPwtD30hMTGdOKlCcUMdPBE7jTEJLx0iHRTGrOaqXqxCtCK/FsWO22ryyxU/QU9rs4fLnwfAy9bj44frA+Ir1jvGx8Grv4+m16STpGunf7KLy/fpbAlQLzxgRItkq7DMDOx9AmEU+SJ5Jn0hLSL5GaEMnQqY/CzzNpjqnwqjHquOtebJDt1a/U8fqzFLVx90U5vvte/TX+Mj3pPes9gf13+9i7Svo6+TT5Bvn9+wG9IX7HQmTEzUj/zBQOr9BOEazSj1N202UTkBNgkqhRxREfkLxP9k+s6QdpVinvKqRrq62n74xx0/QvtzK5unsdPHA9074gPaO9LHzyu6J6cjnVeal5fHmUOyP9RICLQ3wGqQm+jE3PCJDOEjkS1dNZk45T3ROuEvUSONGhURVQv1BZ6LMo8OmbKwgsn259sPIzgzageTo7S/zHPcC+PD35fJP73bqxOQv3ovds90l4nHnVPG6/YILIRxNLHU2G0CbRv5L802mTqtOEEx5SVpH6UMpQ49BLEPunQaf1KG1pq6s1bWhvtDJKdbR4J3sf/LC9Ff1p/GF7qLqv+Qv4LfdX99Y46rqKfS4/iAMaxx8K3M4pEKYSXFNOFBsUtZRVU8pTI9IO0REQaNBD0QjmR6cip8Ups2tBLi5wofNpNdq4ezm3+zP7HHo0uTF4Cndh9g41+vXRt3X4xvwZf4vDywhgTDuO95F9Ev2T7RSelPmUXtNrklaRtdCwUFuRM9GpJbRmUSfk6f3sZC+7spt1QzfUOdb6lvpHeUp37XYqtJwzo3NXc5L1XzfCuyR++oN5yDQMYg/HEnhT4BSsVS/VKFT4k8DTNxHeUXnQ6ZHo0w8lxGc+aNor4u7KclL1gHggeaB6D/n8eEC3OjWH9LZ0PLRHtX229rm4vcZCOgahi3NOw5GgEwdUVlUqVJvUBVMKEaPQT8+zD/rQd5If5OPmZ2i260eu97J1tXo37LhJOCn3ILXK9FkzWLJUMhNywrU7OH78rYEwhiZKr46dUflTqZTz1TmU3xRP0zCRfpBFkDrPutBl0kCkmOYyKKEr2m81snG2A7g0OBb3ULYJ9P+zrLKX8i7xyzNlNiY6G/8ew0OHjcvnDu8RcpK6Uu8SthHdkQUQAQ8hjvuPOFBsUk+VHGZ8KNlsFK/+8oA1W7brN4o3XPXfs+tyNXDEcMBxirNpdd55vb62hEZJws3ZEJASZpLskypSuxGTUDnO2k4+jkMP0pJ9FLJmX6iba9nvGTJ8dID2cfXitPgzVrIwMCTvGW8ccB8yCDYE+2kBB0Y3CybPVBGx0kSS2tJe0VEPCU3gzOAMes2K0AATIVW7Z5Lq0S6Ocal0dzW8tYX1MzPGsv2xUrCb8OEx3TQv+GN8RoIHhv6LRk7RESVSeRKdUYmQ748YjWOMi80qjbXPeVJmVUeoo+scrs7x6LOFNVR1m3SB84dypTEeMLLwmbIedM74nv4xA4wIVI0tj85RL1G5kV6Qfo5aTPNL7QrUSxBNtdByE5EWgqryLeZwvPOydVe1N3PFMiRwZG9i7r5u0K/wsl42+jvcAZXGe4nfjLxOH85jjaxMd8tgigFJwUrUTGxOY1HkVJvXK2yDr/nyFvNjs93zkTJqMQjwMK94rxCwHXKjdWe5tz7zg9pIqww5TmrPgw+2TuAN6gxhC4tLMMsGDAQO79GE1QHsOG6YMYYz5DTitX1z6/KLMUIvxi9P7/zxBbQeN1+8RUGwxaIJ3QwWTTfMhwuqiluJrgghB76Hfsn7TI1P95KslbwtavBt8hrzJ3MwMm8xy3E8r7iv3O/w8Pj0aPeJfIABMQVQSO2Jzkuui8VK2woOCHNHrkeOSPyLWM4a0SGT45WcL4BxefL2M6Wzg/KOMeNxLrBQr4CwmfHiNEJ4NjzMATTEXEfeyh7LPsvXyzkJQwcbhrYGQkd6ykpN6o+yknPU47AHseNzwPVbs8syB7ESsEzv+u+6MIiyObRC+Ev80sHgBc3IWonHSmVKB4h/RxvGZ0WmBQFG3IkADFnPeRJvVE2V9TFUMpGyiLIXMWLwHG/7bwxv77CM80A2FzqEPqvDDcYCCT1JoMmYiKUHlEUEg+LDX4QRxhWJMsywz/lSvBSE1jfxoPJ7sc9xWfC0b3ju1S9QL4Qw1DMvdlO61j8nQwyGZwh4ihEKSElOyBVHDcXRxalGMQeWChsNotCC01OU8BXy8jey63JVciDxPnArLu1vE7AEsjJ0SPiP/abBgsXwiavKggsRyhaJVMdgxY6FaoSwBJNG38nyjQlQgBNPVKBVQDDQscax7XE1sESvfK70byJwGTHFtHw3o3um/7DDtIYgCAFIt4heyDfGSkUNBLXEN4V9hv0J4kz/kAkS91RJFYBWenDbMMewsW+0bnutzq5uLwTwW3LqtfW5fL4LAnJFTkdXCGsIdsZ8BTLD1cIXQjdC6kTGh0ALb45O0cyURNXallNWwfAyL1quUq2BrPWsrCzvri/venI+9fc6IT9mRBzGKYh7iLtI8MeVxjSEvcMngnCDO8TqyDiMDhBS03iVMZaBl6eX3nCor0LuKy0ibF0sZyyYbbSvsPIHtp97N76TwzvFeQcbyCvHAgbzxT5D20JiQXMCnwUIyCtLk8/qUtNU+5apV8dYYe75Lc1tQKxeq01rd2sUa+MtJ+/JMwi233oo/ZyBN8PPBNYE+4RzQ0YCoIKcg2NEb8XHCNsMsg9/klkVFJZdF4cYIJhnrXjsuGxcq+trC2vBbBqth+/q8p22Lvkn/k+BvkMOxVFFbkRPw0hCCgEZPxTAD0JmBOCIM0sST0uS/NTy1zbYF1iQWIUrjmtUamCpmOnpKbnqoKwtbicxCjVrudD+mYImBIaFpwZlRguFB4L4QKi/kb+swLECOkT6yDkMGdB+U9bWKldI2HhYvJiEKnIpgKk06EMoeKhDqZlrKO108P21QnkMfIIA8gLOA8KD5IJIwLz9tD2sfen9F38dAifGLgqwz1HTNdW5F2nYxdnCWb0ZKmjOKBFnwKdzZtunfGfTaehsa+/UtEF4mTzIQGSCfwQiRJOEP4MLgQu/aH6R/3sAPkJBBYDJV00o0TKVOFcsWI4aJpolmjSZjSiYZ2amdCZRJy8nYCjj6w7tqPFwNtC7d37EwMiChsLJQtLCWMB9fo/9hP0lvkSAXAQDR3jLOI+Xk1PWNtgOGXgZwdo5mf7ZpqeYZsqmo2ZuJofnBCiY6qCtN7GmtdR50n1t//uCe4IAgZWBIT7u/UW8Bnuu/AH934FnRS7JuQ3UkexVFBd42FoZeRluWTPYShfUpfBlRGWN5eymWWet6bss2HC2dL15sP3fQPkDFwQxRHLCKUAyPdn8X3pzez/8ef8uAqCIYE0OkXOUWlddGLqZdVn12ZkYyRgI1xvldWTKpMklBKXIpx0pEOw373yzMrZiOu79dz5+/oa92zzG+505x3lX+Dg443offZHBkIW9SlKPFJJO1V4W9ZgxWGLYgdhxl59W+FX+pV2lqqXWZoHngCnb7BqvbXNzN0g7ID1Mv17AIn5nvQt7sXqNed/4xnl+etQ9/QHwxhcKm87AUnUU8db7V5+YFRgcF+XXJRXJ1TJltqWUJivmxuhM6nVtIXClc8+353t8/YJ+7z+L/oc9KruaOkw5JbhIeTK6jLwdADbEQ0k7TVWRMlN8lSkWOZar1ltWBVVI1E+TiRNjZgBmhqd0aElqCi0zcAW0BLemO0G+Vb/dwHnAVz7FPYq7+Ho/+S55Ojn9fBH/DUJ4xqUK446r0UxTN9RCVVnVXFVPlKCT4BMe0r+nMGdpZ8mpMOplrKQvc7JDtZR4KTpiPJS9Aj1ke/A5izhwd7y3ijfh+BD6RL1Bwf5GkcrVzj3QUZLUlDcUEJRPFCDTWxKekYVRaxDGZ+Gol+mI6uUszW+c8qE1pviruqA7wHvYu8L7GvnKONN3wHf3+A85QHvBv2lDFIdwC79OtRDQUmITIFOS02iSt5GpUG8PoQ8/j2woQal8agjsBu6TsZw08LgtOsw8MPywvJx7mrpauLX3ZraEdoq4WvozfThBPgU9iXcMdU83UTdSOxJVEo7SJpFLUJGPzc9ijwUPfylzqlTr2e4u8JIztrXUuIp6rvrQelo5efdVNlm1onV3NV32SzijOzf+ooN2x1sK2Y2ED1EQT1DJkSVQto9cDkONnI03TX7Oeemfqszsli7esfa1HjhFuo+8EfxbO1u6EXjL9zs1+rVrtpg4FXsxvzrDdweyywkORpAQkPBRD5ESkKAP6c7pTnrN0k76j59q2KvlbXMvf/IQdUp3/jkOOqP7GHs+Onw5XDiHt4L3vnhq+jh87n/4A4CHowp1TSfPEdA0kDLPgE9wTnGNZU0FjQCNmU6capBsK+3z8AQzKjVi9+k4xjnwOZY4xbfAdqf1WLWm9lD4hjtUPsQClIZJSesMnk4YTqiOuU3WDQkMAsuwCvgKw0veTc0qs+wrbhWw/XNddpJ4/XnpeWw36fbKNdD0ljQs9B31rjgyfH7AcYRkCPhLwI4kT7SPyJARTpNN8kzgy/tMIIymjbLP2mtrrYkwmnOC9ow4KznXefx4Z3agtUHzxLOn86U1NHeFu9fBKoTJyUpMo04uDwpPto6TzW2L9IrdyihKoUwbznjQp2seLUkvl/KgtKS02fTy9FZzZjJScaMxKDEFsrU1ubnQvl4EQchNzAqOnY/8EAnPME2CjNoLDQrHSxDM1Q9nUeGrl64jMEIyz3V0tno1zjSGs72yKTG3cd0zW3Uf+KL8rQFaheWJzExOzboNs80OS5gKoMlfSUGKJUxRT+iS19WgLjqxyDV6ttd4GLeXNaCz0nM+8nSyAnNddf+5mn6UxAhJDYvwjlzPFw8jDgeME8riic3KOksvjWFRFNR3q3PuEXGec9n06nTOc9XxivCIr+Iwf/DLss72R3tHAXEHDMrvjSEPag7tDwtOC001ytiLJYxIDt1RftS2Fw4syO+m8kfy37JWsU0vnu5fbg3u96/E8oy2w7zLwv4Hi0tATJYMrIwGSo4JY8exh3YIegpIjczSKJWGGIStt7A5cWPxwPGp8JDvXS5uLl8wb/J1NkS72UIxRwDL2k1dDrfMjEuwSi1I30hISbVLn1BXU/dXJdmG7ksxSvLycuxxtTBCrviueC7zsMOzpThf/WeCu0csSwCNdQ2cS9zKGQeIhzVIispGTTlQyNTkV+4aPe0mLmMupy52bWYsh+wNLLyuenHCd+y73cGSRufJ5ktXi2/J6EgshjYGqMdfyfRNatIP1lFZJZrAbVotquzo65eqo6n96UnqLWudby71OPuNgoyH7QsvDHpM3Ix8CYVHoYagxx6Jzk5Kk6BXNpmI26ntwi8w7zatluv1KgdqsCvorp3ycbijv3ZEyAlMjMqMqwrASX0IQUb1BlLJMozUUVGVx1jB2wvcFW37rebs0iubaepo0SoQa5vvpLS8e67BcUVHCMjJvkheBgtDTMEagFoCX0dKDTbS7VbUmhJbhW1bbbTtkyw9Kmsp+Or8rNwwSHX9/KTBsgXxSERI4sgYBgKEngJ0winFNMkdTy2UjJgtWlcbXuzsLCQrIqpjaWyoxiozbEdxuXaefeZDjMduybLKCYhtRioDaQKbg2WGZAwNUSHUhJg8GbXamStvaq8pi2gCZ/1oiirHrluzsLlzADfE20fICD5HJ8PyQUw/rsDvAx1HXIzBkggVdJgo2e/aF+nE6L7npycm571pJqug7uu1GnxDgkRGFQgORtAFFcLnwfnBf0OPiDfMn1FBFZeYWdnv2n9aAyrQqZ3o/GjYqiNsCnB8tRU70kEYxFgGHsYyQ8uBxIBov9jB2IV/Cd+PbJNlFmYXrRgbWBYo12gJ54GnUufH6fMtePJQeDK9T0I6g5FEVEKLQE++Rr42wLRFOAmZj07TidcgGG4Y5Bj7V8CqMmknaSrphGscbeRyirdavQmAkAJsAUCABX6GPap9lz9oAjEHuIzBUdvU0NZxVxkXAFZ/KXtobuhCaUUq2+2WMcj2wjwugAhCx0LyQS5/uD4Mvev/dULmh70MsZDyk/RVOdWOlWSUkBPR6LboTel6qrDtuDHaN7A8GH9PgKZAbL3RvC46kbsQPEz/SQS5yi2OihH9k9VU7tRqU4mTF2kcaONppGtNbfIxh/bzO4l+3sABAB6+fvwB+vI6PXqYPgiDLshMjaERTJOMFKUUnpQTEwwR32lOKflqvWyiMCx0/PmwPToAAEBGf009ITtOunP6nv02QTSGBAvTD75SD5OQU4uTMJJ0Ea2prSmWqpksJ66d8wP3czqi/R4+RL3eO1258HkcONR63n8ixCQJcc3jEM9SrBMUUzgSOdC1D57plqoA64ttj7DZNKB5Mbxm/SS8jXsGOT430Tf1OUC8iEEVRpLLYQ7LkR5SV1JrkVFQQA9uTsoqN+rMbNBvhLMWt4L66bylfX08Pjq6+Ii3tTf9ubv9SUKzB+oMAs84UKhQ3pCHD3NNw40jDQWpoKqUrK9vpLNCt4o6tDwx++m60nls+Af3NXf4ukZ+dcKfx3nLsk5HT9rQlpA5DwaOY04EjqnqH6s2LQowGLOkdqw5cfoXuhR5FDgI9lM1hvbn+Zh9d4K8yBdMRk7XEFBRDZCQT4zOkQ2uzUyOxCrdbJTvXnL8tnQ46jqQOuN5RveEtnL1oDY4+B98vsH3hvJLUI7BUKrRO9Diz/SOJIz4jFTNi8+lqywtZbDAtRx4kPrx+0x5y/hIdp3163WWNuc5jf3Cw1nIn0wzzplPa48ejlyNLQvQy0mMI43k0ALrdW3dMXw0yLfV+Lz4KLdMdbQ0AXO6M921qfjbfcbDx0jnzEHOqE9Yz0NO7I2mzPwMFcxCTt7RVWthriNxLnQ3thf3dDdq9dJ0mbOosvUzp7WJudM+ooP5yLvMPs5RT5mPgo7MzZSMZQvQzK0OQZDEqk9skG/XcqV1cPZ6tvm2ETU6cxpzA/OpNb+44z3ZwzGIPgxLjzgQIpAFT1uN1AxtC4WL/gyTT52Sjupm7R9wdDN1dNJ1bLTy8vgxaHETcTIx9XSH+N/+GMPpSRXNLs78D11PO81RC+4K2Qpny3oNEdCEk/Tqey34MUBzm7U09O+z53Jr8P+wf/AYcVi0H/iOfg+DoohvDGVOWU9kzzROAYz7C9FLnQy8Dm4RitSg13ptdTB08s40qbQRMx6xfy/ab0JvLLBkMvS2wPzhgxnIV4xKjq2PiI9bTf2L50qkCb6JyQwXjwtSYxWgmKltZm/dMh3y1/JjsPbvde5Aretu/bDANBB42H7iRHEJWAyezlnOLE01S8pKZAlDyf/K4I3w0awVvRic7CavJvExMdnx5HC6rp6tFexabPTuU3Eq9Tk62UCxBkjKlw0GzhGNZ8uUSgXI2oheSTULbs6UkmoWIJkMmx1uYy/EcPAwAu7ZreuseOw8LMTuzPLKtz08QQJRxs4LEA0mjZbMrwrbyKjIAQiVycVMPE9p03xWwNmym2Kt9G8Fr6kupO0Wa4rq3CpO63XstS+5tAL55/9PxVVI/Ir8C6nL8kpXyLPITIiaiYGModA1E0aWtVlpW0Vtuq7Q7yntxi0E7CpqxCqdKzQsSnAcNPJ628DoBeIKcYyYjNNLxQnUh4VGXIWNxpaIjIz40U4VTtigGtBcRK7mrzVu0+z0K6Oq/aqO6tJsDq5s8ho37D3Bg0cHG0kryjpJgQjbhzkEooUoRiQIv8yN0RkVCxgTGqfby5xNLZvtWiuZafQoqqiKqMsqVS0o8Lt1lbyWQwXHMEj5CfxIXwcVBXvDYQIQArREVIjPTVZSo5YFWIyaa1sx7BEsVGu1qmRph2nxakOsCS7dc214hP5Xw3qG7ghGyXQIm4dJxXeDVMJ4g1UGB8n3jjcRwlXzmGEaQdrTGyfsSSxSKzJqLqm6aXQpwm1t8CT0ffkefvACoIWNByOG48Ucw/UDP8IWAkJFBEiVjOaRO1Qt1ySYopleGYDrLqnd6OAoc6jZaSNqRO1bMW43MD0aQyKGhEfCiQkIdYXUgx4A3gETQhlFl0nhjXlRkpW2mCxZYRmxmVDrmGquqjIpjmkG6UEq7G0i8Yk2aHuKf45CxoT1RJ7CvcEfvqu+T75GQRMD9kfIjOaRz9Tm1vXYK1iV2C/rUWpZ6dvpsmmYqsQsem86s1N3qfzcgSMCTMP1A2pB7sBtf8xA30I9xIeJVg3bEXaUlxZml4fYElhl1/8sQGsfaoqqiKvrbWJwbDOVuO78qUDNA5PENwORAYJ/qr5ufmI/iUHMRR8JZI2+kbZUOpVRVccWFxXL60ZrKepgql2rVu3IsKM02XlBPeQATUOCBEhDJMCz/tT9lT1HftOCf8XPCkqOldEyUsRUE5TuVIDUASpiqeRp6KrFbLcvAXIfdcO6ZL2wQN3COsEKv/j9k/yJvTg+Q0FpRC8IJkwaUFsS3FS61M6U+xR/k5mrRWusK7BsZG3ncZC1b/lGPcQAkQHiwX+Aq39C/O38Wv04vx4CiYbny4qPTpGXUt1TXROYkz+SeiraatorLGys7u0ygnbQO55/+kGjQfMBjP+8Pba79Lrle9M+EgKzR/2MGM/CEkcTYdOhExwShRF76luqsCsFrAludDE09SN4ffsjfWB9Jns/OZh4JbhwOQc7Z8BARNYKSc8QEd5TZxOZE5hSxRF20AgqWuqXK7utFDAZc6w4YfywPpu+pz2O+/v6dnoE+ww8zkAHBPrKBM4V0IxRl1HskY9Q+Q+oD2ip7+poK//uW7Iati36Lv0CfmN9I/uauah4lPflOQ38IwD8BlPMMs9pUbcSQdMvUisRd5Cb0GVqmmu4Lb2xH/U1Of99Bj8Xv1C+Y/1oe1z52fq4PJRBNoYHSsZO15Dwkh0Sb9FfkLvOxI5Q6WUqBWv37hyxkjZYOTU7hbvYOzl4kfbtdp239roU/w1EO4nBDZKQWBHjUZhQzk/jDlTOPM7b6i5rtO5Tsk218LhceoR6pTkT94Z28XZr9/q7rIFzhufL987CELvRdZF/UBTPTQ4HDlCo0KqArXGwvTRP+BW6gjuQ+vi4oLawdVG2lbnUvxsEoInzDabQAtFs0Q8QPE58jUeNjs8xKMarvu6Kswg3Lnm0umb5nzfetlp1ZTZAuXv+SYToShLOZZFU0kLSptHxEKJPbs9eEMRpPitAbzly/7a8+To58zip9oh1b3Q0tSZ4tj1Ug1IJZU26kG6RlVDCj7+Ni8xJjYLO+meSamjtu/IGtci4Dbh8tkc1uXNIct+01flOf9yF+kuLT1aReRHEEa9QXs6zjdPPe9HNKMisB3BUc+j1iDZr9MHzR7H+sU4ydHU1uxACX8j5DjXQWJFd0IgPLg02zH1NoJC6lEdqjS7vsog1xLdmdmD0KfI58GQxabQr+ZJAyEeBzToPydFNkTrO0AymC+BMg8/4U7Wo+azBsXY0aDYvNRbz3vFH8BrxAvOheNGAGocYzTxPyFF80QnPNo12DROOGBDIlHloWqy0cLe0LrV+s++yKbBB8A7xPDPEeY8AfYbvjK4PlhCAT45NtIw7C0eNQ9DVlM6onSwLL9xyQ/MHMa7vlO6Qbp7wPPTbu9wDtUmEDiCPxY9yjYaL7YpoSgGMcxCOVetpGe17sAgxhbGrr/+uLO1BbgZwQHTFvJbEd0nnzeFPfU3cy9BJ0QmBis5OvVLeV7zqe+78sYbyrfFrr1xtxS2B7xxy9LjRQJsHk8z7Tw8PBE00yqrIr8jBC6OPl9Sq2LGrE29NMQAwpu7ebTesoC1X8DQ1ITwRxBLKbs20DzxONAw9SryJEwp0zZiTNRfAGxVt9e/UMDPvRK3x7Kisra31scB4SUCehwaL6E5VTcGMAQpASQHJ08xa0VLWf1mJXBvu37B2LvetQWwLq5Gstm9U9QC86gOoifQMwQ3ITC8JlogrB8OKIo5IU44YORrPbdqviy++LfjsSKss61wtFzFOeHQ/WsXZyayK/YqZSNXGzMWfht0K9VBmFYHZbFsiri/uUa1nK3IqMum1Kt8tyjOrOrwBmQd0Sq8KdcjQxvuFB8WdSAENgJLNluAZ1Vtn7nAubi0rK7bqj+uobnSyybmpQEeGJAlvyiZJXEbghAbDxwUuSb5PLlROWCKaEa47rkXtS6wGqlHqGSt1LmbzifnvgRiGEshWiPmHUYUaQ93EV8cjC5YQnRTJ2DpZue1T7RCrsypNKcVqQKybsE82IH0QgvCHN8hsx3VGCwQUwwUD70bqS9QRhJXt2EqZZe0DrNerkeqJqqEqz20qsV53nf8GBCKHNofzhlDEyYNrgxTFnsmPDvZTZtad2KiZfK6SbVDsJ2rTqswsNa71NDW5pf+SQ9CF5QXWA/UBLL8EPn0A6EWoC9CRHdS81phXqSzxLEyrXWqdqqvr5G6T86r5yP/5RC9GkAa0hG3CEoD3wS9DVcdvTC3RNtRFlpnXX9dMrElrMGpqaqTrqq4cMkB32XzkgSVDpIQcwnIAEv9i/wLBbcXGjCiQvROcFfaW7hbvbYIs0qw6q5qtGO/bc4h4S733wY+D6EPRQoV/k/4qPbr+2oLxiCsNZtHO1ESVgNWhFR/ry+suquUrQWzir4j0fHnL/v/CXQNPAhXACz4N/eo+GsDxRX0KaE8xEgMUK5SKlHlTxKt0qr5qpGuWblvyODb0PEqA4cLKhCMCkcDkftg+1ECiBGiITMzkEILTSZSv1PnUC5Nr6yBq0qugrKnul/IVNoC75AASAWLAvf6j/MD72Tv3PRNAlkUViv7PF1IdkwzTi1MfUnnq/mpXKqmrgi2E8W51lnqF/rIBNcGbwJz+Crx5PG59doEaBZgKa08OUcaTzZRL09aTUxJ3K3KrkCwHrjtwR3V/uiY+R4C9QJE++f0Ku606qHsp/WHBkob8C8XQHxJQE3PTbVJzUWrqreqyqtSshi7Usrd28jwV/14A+YEc/4c8x7u1utj9TEDQBZXKUk3hERkStNMMEwMSQJGCqqmqnCtmLFcvAXLGdlz6ArvQvOK8Yjt7upM6G/oBPdCCN8ezzTdQRVKwU4VUDlQfEt8SAVGiatkr7G1HcGd0PrfrPOk+Zf8TPga8NHpI+XJ6WryKP8CFZEnkThgRO9JQ0vER29FB0DtOlilJacrrF+2TsME1ivl9/Gt+AP4u/Cm6srmQOe766L4FAscIKMuBT4TRRlJAUp7SMxF/EKFQgqnQa1Jta3BXNY55THuUvK88YPqD98i3UvcP9sh6an+YhUTKz88a0dATKNN0k2cSUVEsEIwon+lvq2fuaLHjteP5JPut/C38tznb9xU2ObWCt0J6iL/hhe0KeI7dEUWSiJL1UZiQ/s8uzo5QJSh/qjRs2nCotHR4MTq5PG86Dzh1NlL1fzYzORd9usOgCI2OXRGOkyDTZdML0lmREI+Zj2BQQmfRaketsrHqtkG4kLorei45m3fvdfH1u3V19197jUJjyADNG1Bt0ZzSG1JS0XwQXM6SDrDPouaEKOarwO9D85p23bkAemt5Z/gLtnj0vLXN+Dh8mkKZR4GLkA8QUYrTBtLa0WiPr87fT1mQrRLTKAMrHO5j87J3cTnReZB5UndV9XZzYPQztRZ46P1URAKKSY7c0UrTJNLV0gtQys8qjo/PPdD2pf/oFGtyr3ZzJXYptwG2AnRy8p0xSjGGc362hvxBgq3Ja05dEVkSxhKK0WlPoQ4ljSqNbE8UUhHmP+joLMOwRrKvM+X0MnKHsa4wDnFZslz0t7pSAD1GVc1ZUSoS5xKuUYLRF87OzhhOpk9BUrZWCWjVbHIwsrQ6NW00/nOD8Vrv9m9l8GwypPcxPh3FbguaT8HS+xLXEpbRSg7zjLfLVsz3D6bTSqaDaoeu83K9dPa123SJsyFxM7BMcLpxFrWie7nBzEiojUaQhxGUUFsPZM15i09KNIulDrYSvxZr6IyszvB9Mor01vNGsdWwNu80b0cxAzUU+u3Bacg9zPSQCRGI0YlQXA6NDXXMGk2Y0L0T81fiKgFu8rHzNI714nRtsqMwOa7Xb0dv87MY+Ej+04UhyuQO59CGkLqP4A49TF2MU42bj2WTJpZ/aBwr/K8JMSixQHDsbwwtwi26bV5u2TKW+KR/XEXay5DOok8TT1zOBownSVyI2YqYjbkRfBW4GSjrTe7UMV/yYbCZ7x8tx+2c7ZqwqXSx+fJ/QcawSovNjg50TfHL3smdiSHKH8wCkCMUB9fUGliuhrFDcolyOnCd7xRuNq2Qbs2xevYrvAzCuofEivaMJgwmCtrI1sddRpjIR0v4EGOUrFgf7Etvr7DocTsvYa2V7HusCW0i7zdynDidvwKF6cloC1BLMon+x4uGyMb7B2OKn4+VlG5XxBof758w47BaL8UuNKy0rBRspm7ostX4nv9TxXaJE8s0i0CK9EjGR1IHW8i9C/xQoZSZV4/ZuHA3cWIxdvBgLojtR215bn2xMDUD+66BQ4adSYmKzwqzyJ2Gw4WeheiITgxMEbBVBpfoGVOwtrFv8EVu0+3/bHJtC+9A8055Dj+mxe1KBAsjS7AKKAgdxlhF4kbPyYXOCFKxlX2XfhhDr95vOu4H7V2sRKyqLk4xu7ZV/C/CskbISSRJgAj/hi8EJYNgxJVHLEs+D8mTpxXPlxLvS6857fNs3OyL7OTuLTDTNZo7FoCwhL7G9IbABaCDWcI/wfmDtQeJDKeQmFP21agWv7Alb4VuwC3TrTLtkq/u83n4Qn3nQviFvwbGhsKE0MJAwYaC1YX2Sa/OJdF508/VZJXmsDVu7u3tLaat+u+xcjJ21LxSAVJFGoZ2RWVDxMGTgPQA2MM8htwLlE/cUp9ULpSUr4IvGe4/7U6tyy8vMWL11juggNeEE8V7BS1DfMFZwOqAxkMYBswLuk/AkooT2xR/cB1vYm5HrewuBe9IcgR2g3t+v92C4cQiQ7VB+oADf0p/0IMwh7VMphAA0nyTBJOkL95vFC5s7i3u4LCQdEq5QX6qQm4ELIQcgvSAqz/NgAxBsMWnyi4OqJF20rnTOlLrb12uvC33Lg3vQfH3dZe6ZH6GwSHB0YDZP2w9sr2x/3ZDIsglzObP7lGVEk2SWVGp7irtiK3fLuMxC3U5eeu+GsD1QYqBZv+V/fe+B3/5gxQIGAyQz93RQ5I1ke3RHC5dLhcurm/V8oA26ruWP5cBaYEIgAB+dj0nfXf/UcP4CPbNZxAdUVORnFDwz/Lte60xbZ9vEvIEtpF7iH/oAYCB/j+lflG+Ub8pAidGCYtgTsoQ9NGSEZgQxRADrdxuKm7F8Vy1uHo2fnlA9QEBv6k9fHw8e+z9CsGYxtgLb86UkFsQuw/8juItNa0lbfovnLLX9y47R/6AP7m+YPxweyU60rz9QPsGdIsfTroQDdEuEEAPh05E7Nttd28RMgF2o7rF/kI/TP91Pag7/nqvPFkAGsV5yklODQ/3EFPQKU9WDqAszm1dLvJx9vXzOis9k/7uPdz8U3ta+wD89MAIRYvKDU1ND0/P5I7nDXGMJavqbLEufPEK9QP5b/xCfew87rraeLg41DwlwI6FyQr3zeaPeQ9LTx7N14yMK/Asq66cseZ2OzoR/Hh8YXr9eX44V3jj/F/B2ge8jAEOwFBDUBfPEE4GTRBsMS0ML4GzX7dYuu97yfssuUM3zXdCeNX8msFmx0CMPs4mjvcOD00WTBjLySw1rU/wT3UQOVN8L3yH+185/fiDOVG8KEAsxkkLco39DyGO0425TFhLW8wBbBOtzDFydbA5gbsNOjB4kfeQ9wy44ryVAm0H5gwBjtUPPk3nzPZMFwx8TY3tRjB4tDT4Y7r0ecf4bfaqNm+2mnmKftmEsQoxjc5PKw7zzRzLyIuBDEWPIi6SsmG2MzihOU/4ubZV9Uf1vLcRertAqsb1i5dN2w5XzXYLAMpTi5uNqq24sPo0n7f1uSZ5Zvd4Ncw1iDZzuQK+0wWdSrYNCw5/jPiLhMpWiizLtk6KbubyB7W9N6Q4Pja9NR40bvTrNwY7+8IqCAKMFE4PTYbM2gsmCqeLSA3d0MVxBvSAt0W3o/amtNV0Y7QUdmd6TT9CBUIKBcwgzOLLpsoByGRIV8reznhumPHhtEi2AHWh9LOzCrM5c523LfwTAjmHJMpeSwDLGEmSyOgImYoETQeQMzECdGi14jXf9IHzFHKsM2m1XzoqAKAGYYqDi/SLzYrdyLyIfshUyo0OG1DbMvc1SPZ19gS0tLNOc1v0ILbCe9cBcsX1SIhJvEijxtDFp0Wxx3BLJM8tUYN0G/XfNq41prRqc0czTbT3d+l9ssNdhsHIpIjxR9BG50XaBsCJbsyaz/eRhDScNZ61HLQxsrFyWPLLtQD55H7/hB6Hu0g8R+iGC8Rsw9mFFoitzAaOutBMM5F0VTS6c0hygnKzc453C3uZwAgEVIX2RlJF4AQfQuYDWgVnCPfMSs+DkTU1R7XrNJnz5jOws6C1fTeTPAmBZkSzhpOGagPOg5UCqILvBbmIhgxpTqSQIrW9NMV0EvNN8t8zS3VPOEF8uUFlBOLGboZ7hK9DE0IqwtzFDkhQDCdOc8+1NUr1WrRdM5XzYvP69Vm4cHy0QSYEJsWchYIErELWAY/Cc8PmR4ILxQ4wzuJPVTVbdN/0MTQZtI11nTg4vEIA1oP2xUjFk8R7Af8ApMGYA/2G80pUTJRN1Y5NtVG03TOUs2wzqDSyts+6Jb69QkIEZETLA9KB8wCsANzCoISCR+NKwYzRjY5Nx/VNdE70EbQB9NN2X/livTBAUUKQQ0zCzgHoAL8ACUCdglFF3IjPyzvMCIzZDMs00LRws/hz0TUOd4K6nf3SQJsCO0JQAjZAzz/Vv6BAjMNpxuYJuYsujA/Mnkx69Rp05DSqtP417rgGe0D+v0CHgYJBigCcP0X+nT8egP1DC4aGyXJK8IuGy94LvXV+NNt1WzYkt0X5kDzgP86B70IOwh2AVT89vwO//oGoxKbHaAlSCr5LEUtbyvE1VbU19P81TnbOOTn7tz4OwHnA4P/LPw8+Z/4afpIAWwM1RYCIWgnGCpOKscoz9a+1WXWZNkd3h/mAPDp+ZUB1wNXA9H+t/pR+Q79nQJHDc4XxCDyJbEoOykJKPYlfNce18nYxNwS4/3rAPXl/PL/MgAY/bD40/Vc92X9eAe6EcYawCA+JJwlQSWiI20hCdbc1ozZVd6f5sPwevkh/lP/Qv1D+aL31vd9+lsBSAsGF7IeiyOLJakkESPRIFbXo9f42BjcVuFt6VPzpfiH+yz74fY081jx9PPz+s0DYxDLGcIfBiN8I/0i6SAJHoLXp9f52ebdmOSB7fL1A/vP/F76FPjw9Hb0w/a7/SQHahGAGewe8iDBIMYf6R0DHPzXBtmH27DgSehZ8dz3CPu6+zn5PvbD84zzjvc+/vEH5xEyGU8d2R5yHjMcLhrUGCbYutnp3JHi2OmA8+j57frQ+sr3TvSK8bnxePa1/X8IehMoGhQeHh/ZHtsdERyuGlIbwNoA3ofjkevS8nj44PpU+Zb2PvOm8UfynfYU/2sIiBHlF14b9Bw1HGIarBfxFRQXpNmG3Mfhnuij74/1gff+9k70gPGh8J7xUfbW/qAHPBGrFwwbFhw/G6oZFxeBFYMXqNpi3XTiQOnW74jzt/XS9B7xwe3i7VzvI/TN/LkGBRAQF8Ea6BsFG4IYBBb/FOYWfRo33fzhn+iV7j707vVy9JDxzu5U7nTvKvSB/HMFag7pFRQZ0BnpGOIW7hMVExUVVhkn3e7hGejI7SvywPSC84rwK+557Wfv7PQW/Z8GrQ+OFW8YcRk/GNQVlRPFEqQUVRit3Z7iZ+ia7kby5vJP8YXuT+xr6yDtvPE2+mMEuQ0FFSAYhhhBF5gUFxP/EfITNhjbHHrifui17rjywfLz8GHuWewC7OntjvJL+kUEbg0RE1QVIhX7E7cR6g9vEOUSaBcQHfDjBOor7/DxJPKg7y/tDuvY6mPt+fI1+6UEpwyQEakTLBQdEvMPtQ6hDjkSwBfyHKfkselp7mzwfe/j7Rjr9ul96ynv8PYKACoJMRBIFGAWOhWFEjgQDQ56D2wT2hjJHbPmKOyn79Dwlu8C7brqgOrG7CzxCvgyAqsJqw/QEisT5hD2DT8MqgxoD6QUwRnSHZfo1uzI7kPuUezR6dnoHeqT7fzzr/ysBVsMhBC9EZgQ/A1qDG4LYQ1CEQEXkxvs5zTsx+5a7pvsierC6QDqx+x48ZL5PQK5CUoOARC9DiMMhglMCC0JPw0+E8wYhBwN60jty+0J7YDrQupK6hLsoPAd91wASgc4CwoNbwxpCuAHPQbMBzYMuRF4FyUbwuss7nfuRO0c62np6OnE6/fvl/U5/poFFQrtC5sLCAmaBjEG2AeJC68QShbbGdQbTu5D7nXsBevf6YnqaezH8CH3eP5MBY8I3gg2B6QExALpAmIFxQpsEKIVFhnh7YTv+e4X7aXrUetf7MnuWPNq+hABcQYQCdEI8AZwBI0ECQaRCd4OXxPAFr0YUO/I7zjuvezR67br3u0g8rz4NQBrBWwIuwhIB0IFhwPyAyMGaAqOD8cTWBZAF/HvDu+Y7bDs/ezE7iLy7fbv/Z8DwQZUBw0GzAMTAnsCKQWKCWUOKBJZFE0VFfAr7+ztQe1Y7bnu3/FX99v9MANTBj8G2QQpAwEC7wIwBYwJRw7yEewTmxSG8mHx1u8572Hv0vAt89z3R/2MAU4ESATIAjIBWQDoAcYEOAmwDYYQ8xGDEhryG/EB8O7v5PB+8jT2jvvj/1UD9gOVA/oBEAA9AEkBywTdCRsNlQ+AEG0QCvJN8QjxdvGo8o31j/kh/s4BKAPoAmkBp/+U/lX/gQKvBm4KNg1qDqIOYQ5P8vfxDvIO8yv1RPie/EMAmQFZAUQAs/7F/bT+3wFlBQEJnwvZDDANvQwC85Xyf/KD82z1fPiC/Pr/EwK8AWMA5f7P/e3+0AElBScIbAqZC/gL2wuL9PTz//Pk9Pj2kfkD/aD/mAADAEf/1P24/dz+tQFWBXgIHQrTCsIKXgpA9QT1VvVJ9jH4/PoZ/ggA6QA3ABP/Of5U/p3/ZQK8BeIHIAleCQwJjQjG9bj1R/Zj97j5A/yY/ub/m//l/g7+2v1w/ngAjwP+BZsHRAhMCOMHQgfu9hz33/dW+WL7aP2a/vL+P/5c/QP9SP3m/pgBXwQzBhQHNwftBngG9vcm+MD4D/oO/CD+gP+2/13/lv7w/eH9+f4oAZgDfAVjBosGXgbdBaYFJ/md+Xf6w/tX/Xv+2/6T/uH9mf2W/eH+AQEAA4IEPwVtBSUFqgRoBMj5Fvrf+if8uv3//kH/Dv9g/ib+Xf6P/xkBkAKsA1AEXARHBPkDygPP+jf7G/xO/Yv+TP9T/93+WP4W/lP+9v56AO0B+wJRA1ADDQO8AqgCpfsI/NX80P3e/pb/iP8g/5P+Yv7B/ov/wQDAAVICjQJ0Aj4CAwICAoP84/yN/Tv+1f4b/wb/0f6H/pz+Ev/n/9IAkQHcAdoBnQFXAS0BQwFm/b39L/6+/h3/Tv9G/x7/Cv81/7j/YAD0AFYBVgFCAR8B9wDlAAEBcf66/gP/Y/99/3D/V/9F/1D/ev/T/zQAaACHAIYAcABUAD8ASAAt/0v/ef+b/67/sf+x/7D/t//C/9f/5//x//D/6f8=`},{name:`shoulder_right`,sampleRate:12e3,frames:8160,wav:`UklGRuQ/AABXQVZFZm10IBAAAAABAAEA4C4AAMBdAAACABAAZGF0YcA/AAAQAGkApwCgACAARv9s/tL9XP0A/a79s/5q/77/tf////wA9AJiBV8HggcYB58FfwI//qf6Z/g397f2jfaz+WL8Af4b/iL+5/5WAQgGHQvtDmUPsQ4yDAAHof+r+BL0CvIs8cbwIPJg9jX5a/qi+uf6Jf1fAiAKORKEF1wYqxYKE9ALOgEJ9xrwH+zd6hjqAure7NPx2vRn9uX1ivbe+WcAhwmDFOocVyI0IaUeDxk2D0wCIvff7WLoh+UK5YrkYOOp5n3s5e7Y8HXwkfC38vL3AwDrC4AYjCLAKbws3yoCJhYegxKHBMf2eu3i5d/h+98k4Azfid3N2vrgYOX35xjo+OfS56PqVe+I98kCiRBRHtUqqzPuORA+7TwzOZ8zmCoKHn4Q8wBZ83Lq9OLr3qLcSduP2uzXqtcy1lzQNdDi0w/X7djV2XHaPtoj2z3gVuPp6ArwoPnVBtsUXSKBLeA2fUCwR2tNsVHpU21SK0/DS+5G4D5hNdEp+x3mESEEJfuf8LHltd9M29zV4NKX0GXQ4c2IzNLLQ8z0y3DMTMrBx/7CYcO+xyLLPMu1yezJkcjsxXjFGccExuTGocdeyPzHq8v00QXXHdwf5CDti/i9BBcQwhsRJn4vcTnwQdZKglFLV8Zb7F/WYxNnuGnYatxplGcNZbtiul2kV6hRMUrORJg+zTWYLsomTxyEE8EIzAFJ+Mbx+euH5YzdGtvJ1pLV3NPa0dzOMc2Dy3bOhs4x0CDOhM4A0PrQsc9+y/fEW8DJvBi5MrO8sj+0rLZJuvm6CryFvdq83r2Bv8i+vb7/vanCIsXixErKnM2L0SDbW+T+8Az+zRDIIMwtuDmBRstPMljYXdBh02MLaP9qImzmZx1j5FwIU1JJ+D5LMDQhJBNKAxr0Puhg5WzeK9oF1RTTYNXp1rrWzdYy1CXPGsh7wLq2Sa5xsfCz7bCxsTG02rRWub7CoM1Q2PnoJP6xEAMkDTphTL1WZ19DZr1pcm0LZuRaWkqVNMMdkwj++NfxMOoq4xHiMOTc4A7f/tlpywK46Kjcq+ernKzXriixxbquzc7lhwGlHy87ulAiYSRrgG4HbIhc7UTQLBwSivg/6U/iaeE85ILk49wJz0W9KalZqBWn4KmAro66l9Oy84wb6T5wVpZjrWgjaxteKULvIXAC9uw54b/h/ON34/rWxcUGr72lbqgqqwmzYcfe4ncNfzi7VOtgymcwa6ZdAj7HIE4Mvf67++P8ifgs6MTPfLRlpoqnEqszt07TV/czIaxBH1RRXa5eGlL7MiIZWAmDBdD/ZPsW8W3ZHLaHnt2diqEhrB7IJfT7IW1DolQbW0Fbj0ntLU0YsArWBYACp/zS6JDK3KpAm+qb2qTXvHvkfRXvO2lQRVvGXbNZND38H30OoQfwBCb9SOhCyTKvWpgKmZmgmLKf00IDTSv9RWxOf1T1VQlD6iqeGwQXQhk1FpcJ+ewSyjqsaJ9qpV+0U81M9PccaTnRSHhPE1CbUEE8JyocIRAebBq3FNwEfulCxmirvJnOnDSmdLxl3gMF6yQfOihCREUXR9ZHxzWWIrsZNxwnHIsZaQ7M83zTbbhYpOacn6COrIq/IN0HA5Qg1DItPktB6UPlQ5hGZTb2KnMnyyYYJzYgkBJj/8vkY8tGsVWjAZ2ooXusKb6I1gX2IA4lJBsyxjdXOv45Ejv6PO89cDSsLo8qJyqdKLMneyBSF7gHVO721XLB2rAjp+ugkqTJq5W11cbk3mL20A5AIG4raTQmNZE2LjfENzw3KDmxPWs0Oi3ILFArvyzxK8YsCCn+I8Me+BJ3AMbufNt1y929qbKXqzylUKL8pTWs+bNjvYrIg9q56UT9sA6KG3EjlyaLK+wsRS1uLncuADDFLf8uTzMUN9w9bD3EOLE0qjD4MGkxKjG2MG8uty39K6YqRieZIroZsRF9CIP8PPAD5KPVZ8lqwOq5FrM7ri2rhqjPpTimGalirAivybTdurfCU8o31trgT+xZ+VADUg2XFCobViF/Jigs9y7LLnYy4i9BMLYwCjGmLpMuEi/0MPIvrzQcNrE3ejpMPHM3OzQeM+Q0NTPzMmk1TDbpNKQ03TMNM/IvvSz4KaohExryEfIL9gI2+EjrHOHW16PS4MqNwby5ILUzsRCu4qsTqtWobqu3r6u1bbrHv+rI+9OW3sToC/Ml+3wC0Qt3FQwaJxwGGz4ZKxspHB8cmhkmGtMawR16IiMqbjJROfg3bTmXOEE79zknPKY7qTeeNMsvUSduHSUNZP1N6gjbu83SwI62w68uq1SouKasqwyzSr0qzEbdQ+0j+YkIAxN+Gfwc3R/BH8ogSSN3KEUtHDWXP9dDTkKGQQhAET6JOpYzESowGmUElexV1F/CL7Z+rk2pO6aCrpG8R88i5jj6wwl8E8oZ5xzjG9Id2SEzLAE7tUabRNtDU0JuPik1sSStClvsUdAkvPWuIKi0pOqs8MBd28j2ygqzFDcYXhjOGD8g+SuXPcVF6kRwQyY+wC45F6r0jtQxvPOu2qd8pVG1m9Ab7vgCyAz+DswO2BKCH1o3DE9pT3xNsUScNHYS0OnNx3qyeqijpWGyZc4c6xsAOghCCRoKixZQLBtKclMDUlpKxTRODmTijMH4rx2pU6gZw+zgzvo9BGsGLwehEz0uZk6aWIJXrku5MU4GtNm2vKiuIquxtrLXyfT2/QT84fudCREoOUrVW59X00l6LSH9N8+TtLeq3ajUtKrV8u699sT3t//MF+I8sVxzY9FdB0k0II3tk8ZRtGSuE64OyUzlYvB+8SHyyQHOIc9JUWShYDVQ/i3c+TjPibenrrCuHLQE0EjiSebF6DPyEg7iNw1a1mXlXJVE7hhT5H3ACLLor+ywRbvh0h3eD+Je5vD1LhieQHxfe2oJYRhJ+RvS6U3GHrUZsUaydbA1xh/SHdTb0/ffh/zzKblPdWgkaodcMkBRFbzmx8uEvVO7rbi0swXFbc/I0FjSmtfU6y4PSzxlXLttGmpPWYM6TQ7F5B7M/r+au4e6YrcTuLzDi8icypHNPtgu79US8jlEV+hpMG65YyJNxilFA4Pi9s2nxZPDKsTWv2u3gbxQxKPFlcRKyA7Ur+lAB6EsrE0NYjxunnDnZt5TujcIGCP4/N+M0oXMsck1x+rFcb42tOm7XL96wKm/EMX/zBnd/vIFDxEuuEmvWzNo5m6Pa1RfIk/5NUUZBADy7KrdzdRz02nPEc5ozynL/sBmtZq1F7nIuc67vbqvvdLDLc/93MzzvA0LJcQ7xE4bXOBlQWtWb6doeV7kT1g+YSrdFcQBAPEt5TPdt9We1RjVQNQL0WDP7cwOxVq7lrFytPi0/7bDtli3PbjQu+DAlcgz09bi9vI1CmkcyzEqQ5ZQ2FvWY7xopmwWbvNpR2FtVotJcDhtJ78WzAXF9qrsIuQt3kzc0NuB2vnZWNog2wPZrtMNzi/EXrq9sFKyQbRatHy0LrWwtUq4frtOwHnFXM993dbq0/7iExgoxjnURzBU2VxPYtlnE2p3a6dm3F7JU0NJ7Dj8J28W6gWx+kXwmOnG5wnh1N/D4R3gGeFX3RDas9Brxx2+5bXVtoS4kbeqt2G3vLsRwLDJx9b65oD5vRHLJe84mUYYU6BZwl4hYnVk91zUUnFFKzTuIFAPDgBz9tbv6+9J7q/tZ/Dk7aToCd8E0Ma98ba+tl235LZMuN++A8la2xfyBRDTKuY7zEpCVblaf13VWF1LjjUGIFIPMgD390Tz4PH771DrQeQ61HvBu7EYrTitJK5Psjy+LM+t68QOyywuQtBPoFd6WltYtEZ6LmMYjgcr/aL61fs3+X3z0ObE0D64ha9CsQm287561qfzRBUUNeVGa1GCVcZVO0IDJtcKEv92+Fj6lQD/9zjlI85ttQK0drf0wNvUAvHyF4I4wEm1UDFUzkyrM9UXCwraBLQC+P1f9XbcI76crLevJbXsxF7lAA5FMn9Fzk5RUKRGCyygFCAHbwdCAy/4BOKbxFGvcK7AtXXJi+kEFHw2KkdWTEZKazvUIxEXzhW+FJYMhPf+2Ey5aK+ws9vBWeEYDw4yWUIURTpGWDohJe0XAhVPEuoE9+d6xw6upKqns9rKYPMnGd4zED9aP9FAVCzKHU0aNBx3GOgC+d/KvqeuLbbTyIPvoheUNb1AKURBQ7c7mSU9IcweExyeCqLoqcKHrB+rpLc90Bn5SB7kMU04SjpRPxkxMigAJvwjnxqS/rPX+LjJp5Csqbws378HHiJQMIE4gzlNPqQxPCqJKA8mcR7VBo3bAbq9qW2r9Lpl18D9jBuaLOw0xDbkOfw7+yz/KBQoeiPiFvv7adZMuRGqwqeHs6nLyexMDqkh+SxBL7IvIDWjOZ0yPDMYMD4uXB88B9Tnfsazs8WoV6ypu0bUN/bkD8cgYCixJ2gn9Sk4NUE22DGTMvQwsCvvHegGned6yae0LKpppI6qdLdGzI7pIga3GH8gFx8qHEkd+iW+M986xDYHNSk2rTb8MXEnNhH59NPZ3cEGtEqqUKWzqKqzBMXo2ufzHQg8EwMbPh7yHdEd9SC2JHcsUjmJP5U/p0DpQbhANT/GOiMwZB4MC9jw+tbWwru0Qakko+agRqWWrb27es7x4dTzxAj1F9kefyBEIqkhmR+oIhYn7CmSLk83yUFOTcVN4U0iTQ5MaEy3S3RGWUA8OCYptR22DgT8auYw1grJW7sHsaSqU6THod+fNKDgo92qjrabwkTTeuR776L7UwImD8EWWB3pHkUeWRwFGtkafh9tH0Uhth8eJ38vDTeBP4BH20ouS3FKw0twSfBH00oMSuhIskUGQTs8nDNJLQQjJBJLAoTwj+TG0jzIqb/ptGasq6Vto5qh+p2unGObTZ8ipritT7miww/OeNm05izxPfwMBDILxA8wFHsV+xW3GHsY6BQrF9oaHBx+H4slkS66M7Q/80tvT1BPLU0jTY5MpU3gS9pHIUXBPbo0DS7pHsMO4/ar6KnWVsUqtqasEqRAn3OcMpyZmQmdFKcytnXDxdfk6Ub1UfstASoH6gmCDfoOkApOCqwQZhmGJi0x/z70TKhVzFaXVyVXFVaWURdLK0ANLpcVd/vi3svCJ7Pqpi2f4Zr6mGCalKlavvDY1eoU+uAHQQ3SEB4RJhDqE6ob9SjuPL1O5V/RXh5fW1tyVNlHSjQyFnP1+dPnuWmnx5/tmvua8abwvQPbH/JxAMUHNgdHA4AEUwtMIH83olKvYR5ioF2tV5VE3CYa/2HWAbl0pjCfQJ7Pn3e45dC45qryYPbB+Gb6tQX4HpY/f131Y0VhIVcSQogfl/JIyqKx4KQIodShergZ15brTvVV9kf1ZwHfF1Y45lneZ+VkSFnmP9oVp+Rzv2CrJ6XBo82t7MxP5Ajuw+4P8OT/ORvDQ1Fiu2f5Xs1JECPS7+PFWq/LpzCn4avvyfHfzOV06DLw3wcsLeNULm6AaaBZijj/Ba/VJrtEsQawNK0QyWXhLOn66kT0fQpQMmNXk22AaAFXVjK7+37KvbVhrh6vRK2WyNHYAt3+3Yruyg6jPPRfG25kZKJLOxzr53THA7olt8CzMLjQzEHTftWN3K7wPxrCRmdjjmn8Wjw8ugc43fbGbb8ZwLK+wsMV0fPR69ad4KH98ChoUJBmn2moWkQ53gkc4SfNTsYXxtfCJ73bx+3LEcw61R3qOhKYPUJajmjgXtZFgRrI7krV68thyq3Jx8MvvTzExceByBHS1OgtENI53FQgZPti3U/MMHkHoOdf1sDP39LdzoDF1Lx3wZDCnsNAy+3iJQKFLZRLn10PZg9gLUzBK44Geeze23fX49ao1XjNR8LavxHB9cDGwkTMxd11+pEeYT31UeNdLmNZWYRFhyuHDhj3mOkA5e7ipt/Z3I/SAsNCuzG+V7wYvSDCPc9v4zICfiFxO6FMplgcXnlfX1NbQJkmVA02+Qbq4uD33sfgiuFZ4T7a884cwAC6hrpyuuO6RL76wxHRKuI8+gUWxC2rP9hLB1TTV6pZu1OPRnE2ySLED4kAtvOZ7NjpkOdY6ETnz+Z340fb+M8Lw7y4wLq/uRi5HLpTvCTBO8vX2AXpZvrfDrAjNDPDQCVJJE5bUSBTRlWuT7tE3jhvJ8gYFAqgAVr4M/N371butuzM7rTuo+0w6P7iedjVzm3DjblftVu2/bXctme4sLyowS7JX9Lj3vnutQNLFTAlKjJZPR9F0UrLTlJQHFFoUf1NbUVdOX0tMyBiFFcKhwEb/+H98/nV+Vv5VPn4+NL27PGG6G/dGNNZx9+7J7VYtaS0GrUFtzm7MsB0x5HS9OLJ8dkFBhpwKoI4EUK1SLRMIU59UGRQskvPP8ww9iOAF+IN+AVw/nv8Nvvw+3j5+vQE8dDnjN1u0erDXLe1srq0NLXrt2G9BsYq1Ozme//JFg0pPzrRQoJJgkwjTe5NdEeJN4wngxdoDaMEtQGOAroBmP8w+V/twttdyZ66/649r/6w2bSPvgDOOun1Bj8g3jErQG5Hf0yKTYxNezy6KiMbyRSSEVEQ6wqTBsL7Q+hUzzm7sKvDrEexU7tzzGbo1AsvKr86hkUCSVpKd0qiN5gm+Rj/E0ASLhDxA0HwedQmuyepqqqGsPy/ANueAGkj3jU4P4pBpkEkOlQiGRIPDZoJEQq5/iDoIcjLr66lwqvEu63bYgRnKE86+UKEQ7BFVTerIykbZxnMF2sKou0zx6aqrqMlrOnBH+qXFLkv4DnlPlRAUjZoIkYd1RthFb0C2d3It3qiz6PItIrVzAZZKI04JD3XPho/JS84KpkpFB+6BqXaP7Ldnj+nncCb8LEe3DXAN/o4wz7kNCgrPCmWIOsCF9bHsLCcOKFkuVfnRxUvK2UzVDS+PgA8wDYOOHMspwxj2mewbp42rCXNn/8rIQIylDJTNKNFhTxVOLs0hh828nG+76EXm5iw+NvjC6IkhiqRKRs2zkN8Qk9BtDOEDo3WR64lncag1sLC8IUW3CQ8JAQtqEHDRxFKmkKPK8z9ecQApAGYe6Mgyan12w/SFjsWISL9PxZJXUeMPvcgeO/gvbigP5eYoNbE0vALDQgT+xSsH4w7NFSjVD5RnkDXGvTj6rhdpK6cRLRr3qwCIBX2Gkgeciv4RFpeXFwQV4BFriGo5xS7g6T9nGKhesKT5/ECBQkSCl8Q/B8yPnVculv0VwFJdClj+cHH+6zJobWdvqz00A/zQwHdB9IMrBKRJZBAiluKY3VfcFbYPsUUvuirvoSsQKKIn+ejub/r2/bwn/S49Uv03wFzF5Q04lVlZlxmaWAaU2I3OhKR5lDHo7P3qYGk0KPLsULLhtqU5dbl1+Wq5Vjx3QG1G60+Y1sOal5pjmTZWWhAHSHH/afUDroZq8yjn58RnoueGqusvbLPCN1T5VjnEejc7az22wvsIvc/y1QlZKdtQmvbZRNe+03sNEcR0/St3FTH4Lnvshyuha2prSatk7KlwTzM59h/3/ngl9/B4V7mafDQ+wwPECB5N2ZONF+xa0hx0m/cbIJmtl3PUOY6kCU6CT3yvdv4zOvA0rgWst2y+rCTsfKwSbp3wjzOoNez3GbaOtwy3/3bqdyk58zsg/5BEdIqpEI1UTdfUmkub7Vu+2hKZENZTky7Ny4l2QpS91viQdExw3O8gLLEsuOvirE5sKuuyK41ugjFCs3V0HvQWc80z9TPjtNV2trl8vNVA2sf6jZtSK9WZGDiZ8lm9mIRXGROxzufJIoO8vmE5WPWN8anv8a7QLy/vJK7armdtqW0Yb9jxA7H68cEyK3K5NAX2cTlWvjLEe4r6kH1VC9gGWeNaWtinlbhQj8sGhQi+3DliNfZyxPKB8kjzUfKo8SyvBXES8y90PLPYdWg1PHc5uu2ArAdVTdETiNcsGOdZGNbFUrPMBAU6fkX3kPTrM+MzyjPd83SxiC9X8JJx0fIlMiO0bPhPPtGHBs68lDZXg1lFl5lS0EuOQsv7IXd3dQh0zvVadF/x0+++8GvworCQMgN1PbvFBqTPMxSCl+4YFNRgDcMF0T4luP33Rjb29lCzsq7bLo2utW9FcS61nX8MiOgRspYqV8fVgc8HBv1/z/tguwb617kJdONvYe+Yb+zxarbof4WKhBI7lbQXPxMzC7SCwH1Ke0/6c7mINQfu/a0QrX6uQLNdfYMJaNEEVXCWL5FsSLpBqv2WPNx84TpBdB5u6a7scFr1MD8oyhRRzdT+Vb2RfMkrwnO/hn+lvzX6r/MgrVDty3A8ddiA6cuzkUIUGFR/TU2FQsDLP+X+nzu5da4tuSvnLMRxXDnRxhVPN9KLk5WQwwjIRALBk4INP/d5yTGZbD0szvB3d9mDRcz6EXHS3NLqi+sFpQLdwkGCBL1QNO1s7+tVLXpym712CC3Oa5D7EPxPfUghBAAC/EIKv9H5r/D2Kx6rJy4sNJ4/LQkuToXQgBD8z5xJUwYyhNpFPwLiO91zYWyCKvcssrF7OqfF2YwYD+gQAhAxzekJjAfAR+HGd4MCO4LzECzjqg6sI7BKeLkCQwosTaiOq460T6rMgopwSWLJgwiHBWL+MnVxbnlqamsJ7nX0NHyuRLUKBYyZzKENOA2IzRNKdMlRyU3IaUWYQPQ5trI47I8pzypx7TSycnoiglzHnssTzHhMj0zkDVyOnQwECwkLGgsuSdxG54EyenUzdy4+atopY6rYLiezWTpHQTHGS8mASwMLsgsJS3+MDk7XTejM9o0MTUCMjMv2SWNFaf+tOV2zHm4vK1Np3um3a6evBTOkOQ+++gORRrWIZol7yRoI8olxSeoLjA4BjpyNyo3xDhKOJs29TGMKnEdQQs19uXe0MrEu9+vYakXpKuhWaiYsTO/5M+i49L1vAMUERsbjyA1Il0hdx8bH3kgJyZZLV81Fj8iRNVDoEVDReJE1kNgQSA8YzQdKU8beAdb9gLjdtFYwdW0zqz2p+CjRqETn6Olsa2quFjHF9b/4nTx6f6GCDwRchWlGVIbARxEHsUd1SC/JgErbDPDOpNFRkzjSqFLHUvCSQZKMkmORd4/uDQrKcEZ+wZ483Le8MvxvaWxrqq2pV2hf58gnpikLa4guxnMktt26rP5lQGLCawPyBP6FVEWDBjxFzcc+iSwLVw5uUVyUg9TzVINVLdS3k+mSxREVDgiJ00SB/vT4E7KirlzrDikzJ4jnNWam6IpslnEStgN7tn9HglMDv0QKhOCEV0TlBhRH2UtGUFWUqBWmFUgVExR3ku7QYouoRL49UDaO8EYsW+m/p80nY6hRLQry+rf2vQ9AgYKNwtOCx4M7BJYIGM3+U0EXCpbUVgpUu1E8StVCYbkT8UbsXGjop5DnL+js70w2/rwnv3AAYECGwP2DBkfOjpKVhhiYl+5WMBKJS8zBNnXrbdpptSf457ypNHBw95m9Ef+EwCuAK8LvSMqRF5gCWefYNVRaTQKClDbiLlCqMWiDKJ7tKDTh+lt7+Dw//RrCKUroVB6a1RopFyEQskSsN7ZuiyqH6a2po+979pR6wvrs+7J/U8gH0zMaG5qVF6LP6cNCNnBuj6uUqscq/jKZd4W30vgIOxfDf89U2NxbEFfcj9fCnDXq71ftTe1RbZx0Rbbftqg4DD6FCc0VodvgmY0SVgXOOBdwUS4pLhNs8PIe9UH1hzclPVnJyVXN2+hZTBG9gsD2Om+bbneudCuBMQFyn7NKN1fBCA6imGdb2ZhrTg1AFPUjcPywfi8grXSwHW+XcHf2LkNw0PxZLNp5VBqHQvqbM5GxhHFv72bvFDC/MVU1ZH6SzOjXMFtJGoKShMZVexB2TPUP9Awwsq+1r3qv5fMpPIAK7lXa2rQZQtHzhaY8A/h4OAt3KXKzrqevrvAdM2s7wYl5E60ZFxsclWyK2j+tOaw3wTd1NgAxNq0RrjHutbKW+9lJfRNX2CrZu5Way6zAmbpi+LE4+Hfe868s5awQrPNuuHQCPmTJndKgl0JZIRYojgtGH39bfR08B/x5uC2x8+swq3krZe4D89n+i0jm0UEVy5eF10hRQskOwmm97XzMfVE87PlpMwUr5yqZqtgtFLFxOEHBTgp20PYU4tZ71uMRkAsaRVFCisHAAvZCiECzO7h12684q1IrnSx7Lh5yiDorQzVKRRBk06cVOVWVVOgQHApeRbcDMMFnQCc/lL5PfD+3ffIlrNOqQ+qMq97uk3J0dou+sEZKjITQ95MqFHKT09QK0sfOFMmDRsOEsYPrw3rD50NnQbS+SLmic8NvAqtPazSrl+1Br33yunekfpjFPgr9DolQmtIr0t5TJ9NcUosPNUqhBvOE6wMBwukC1oJawZaAC/23uqV2Y7GBbaRqnGo8qxEsla6Y8Yb267y+gi8IOoyWjugQvZGU0ZhRCVDOUaeOywrOh8dE80KKwlmCHkHDwQGAbH8QvEa40vVt8hPu9+vRq5RsGW2nL7+ysHcYvIoCDweVi+LOQ0/GETDRSNGEkXpRsY6RCx0JfMgFh3sHDUaSRdOEXsF3fjY49rNcLzwsE+peKs/sMC3ksTM2FTzhQofIGAw9jiQPbBB0kFQQpBDejlTLQ4ihB89IOwd5B1dEo8Fiu9z2NHF8LPQqLGqubAPvAnOveaFAdIUnSPSLI4tkS44Mi01TTJDJzklIiclI/khzxUGA1DrZc0GubCrSKeMr767AtRL8ckNYySDMHozijBXL181zS+8KNglYyUuH8EUMwGV5YLHFrSxp3mpTLS4yrLrFAsSII8qQiyrLkkxijqIM8sx+TEKLFIckP4K2ee6wapvp1m0v8788XEQAyEIKPssAzDKPFA5gDgdOQ407SIbAevTZbZDqYqvlstW80sUhSYWKEUq1S88PtE9DjhbMxomFQEP1bez+KY1qJS+Q+aXDNodWyD0IvQt1EGlP6k+XjR5Fm3oqMFHrV2lLbsP4rIHUB5ZJIgllzE7R/RFKUTiNisUZOHzu1qrO6y/zHH0EQxNF0AWtB+cOlxEwURuOMIaZuejuoGqWagsxxHynwgzD4EPEB70PgVLykiqOtAR1du/tv2p96wW0fT13wWECt4SZyt/T/tR6UsbM5IBH8o1rGemdbmC4cP+EgS4Bg8Xlj3xV+hTFkMkGNzavLdxq+mpTs9H9BYAmPtwCDEoe1C8WSFQMDIZ+QzH+bChqou1xdjb7132qvf/CkY1XFsEWlJKbyEj5jy7+61Hrcq8reGT8m72m/6AF0VF9GJNYOdPDiVS6Pq7Bq/NrNiyxdKR5JLlzundA0QyUlgcZPZY0DjcAgjSNblMsdqxk8A810req97j51sGdzhpXFFmqFrZPL4Gv9juvbG1ULJQshLMm9fY2Bre3+/uFm1FBmRnaTtcPj7cDGLf4sMjugy6FLnExOHR9NQF2VXi2v6FKXRQnWZ7al5cJD4QDSvieceNvaC7ibvBtpPHDNCz0tDU+97Z+LAg50e+YFBuPmjeU4wzxAfl4ibMHcNzw2XBk7vHuSPCKsfsx6HLrNgY8akXnjxwWJppRG9dZFlPQi4OCLLnKNWczTfLqckHyOS/mbmSvzrBBMHmwpzK9dp58vcYbTpeVrBmKW8XazhcV0bDJdUBYehd1VzKBMYHySrKJ8exwRSy+7fquju8+rz3vlLFjdTO6jYMJyxdR4NbpGhub61wOGaIVs0+1yNcCFHvCt9S1T3QE85Uz/rNq8jqv+60pq/Vshu1FLjAtU64Wr+Jy9HdQfc4FNItakJZVEVhUmlgbsBp1VzeSUI1Wx7bDBz6feqe4Vbdj92e3qjdVtrO0TPHpbvprVuwWbHcsIOwkbKbtUK9bcki26HzSxBYLCxEI1VWYcJoPmxlbTVjRVRNQQMqlRIu//3uDepm5dvhauFE4FHfMttb0aDGHLXFrQmxq69FsSKx8rV+v5DNneHT9xwSlC8jRZJWr2CFZvhpAGhUXPpLtzTdHaYJ9frP8NbrDezH6MDl0+IW2GjJMbf/qtaqAas6rQyxBLgFyPje4v7uH6c8X085XLFilGV7YuBQVTgQHowE+vQh6qLouulg6K7jzNkwyMq1zKk6qyCtZ7K8vUjRcO1tEhIy9khXVhBePmGSWlxEvyiMDUL9yPdi+Kb39vIJ58DSZLphqhCpWqumslPED+LICJ8taUarVIhaB12tSi0uQRT3BHL9fv3H+rbzmNwqwKOnrafRq4C60tMW/Vwmf0VOVaha/FvZSrgszRQ5B3ED2P/B9J3g9MJ2pymmlqtTvCbg0Q6GNpxLMFPwVYlL8y0TFesICQj8BCb1R9j1tfikvqlTuObXkgeGMFtHjE8CUX5FlSdOFDEPQA7tBCvrxMURqAanZLGhyTT3RySPP3VJCUvsPyYiOxQbFE0QvP/x3vC3BKP9qZe7NuEMFGM2I0TJRXhGWCyzG5ca9BpdCpzkeb31pCWrK77w5U8VxTHvPLM/JEBaJ6sceh0SGWkDpdvptIylRLBSzH79ZiZBOfI7zTveNeIlzyCkHvkOae0kwwSqRKm9vP7kfhXGMlw6pThSPPouAihBJ68gYwdt2Zq1qKY6tBfV4gURKQw1XjXSNtc8RjCBLgUpSxbg8B3G+6zCpxe4QN14CtElaizZLS001TeoM5Azry1UFlTtfsXIrQSqN76t4psMaCQnK0croS7+Oz41UTTGMB4fT/sV0Ni0dap9swbPofZ4FLYhOyQoJz8w7TyYOmY71DUvJCT+8dNluSytP7ARyO/sSQ2hHVAj2iJRKAw1rj5JPz89fDM9HWT3d9C7to2sq6pEvdPa/PtvEQwZqBqzGkckrzaqPXs/Mj6kMxcfafzY2K+91rCAqnGtPcBz3I31YgZ/C94NwA22FxkskED9QbZCZz/1NZcg4wJj4KnDe7VlrSusuLdJzyLroADsDbISQhG/FGsWPyO+NS9JAkjJR4tFBj73LRwSj/Xq1zbC47Vjr4ms2bFBwnjad+1L+d0AcgXcBjkIbg9QHCEqVD2HTd1NN038SIhDLzYPIZgISOit0k7BubbUsQOuV62ztLPDpNhz50L0Svuk/G8ApwJXAgMJ0BCKIEIyS0NxUmZQQ07ITPJJgENdNPsc4ARk7f/Y0MYFvMqyTK5erYKt9LB1vdvNSuCT6/n1wvuj/9AAEgO2AwsHWgxPG5Ao0jkySP9VEFY0VSpSLk08RPE26iXIEb33hOE7zVLAIrdTryCu5qtUqSephrZXxs3TVeRE9DH5w/v0//3/fAATAqkIfxMyHS0vBUBvT4hbb1q6WcBWf07sRSI1ux74BJTnO9FnwMK0drC9qwus36xxrE25U8vY2nXpzPCr82jzCvPE9W/73gM2EDUghTTHS69Zh16xW59VUE1dQeEtBxNL83HaEclIv9y4r7KMr4mvx7QlyAba8uJ75Obn2emi6yzxUf2vElcruUhFXZFlPGNmXiFTGz8LHdP34tUfwYixP601qjeq366zxTjYhN8j4bnlXeTU6yr8LBTgNXNTe2b/Yf1YVEeRJzUFXuPgyYC82rdjus+6wMv+5hPtQ+8v7F7uJfoWFSs6s1gda1Fo/1t8Q9Ecn/Yl1enBbbpptj+2BsW32fbcpNk321fsXws4Odlbnm6oZWZU5Cup+0/Rl8CHufW3lrOpwdrPGNZy12rlKAK8LmlYFW7AaYBUfCdc9AjNa8ELwwbBmr5X0J/UitTf3fwBKzIhWZ9uK2VRRpEQy97TxI6+EL+3t0y9bMUtxN/QgvYpMfZcI3CsZd5BfgOI2VHGesFLvpivZLsovuLCJt2eEQFH3WfWZn1BPQbZ11XJnsq9xYmyErdjufHDl+SSI4lViGyHZbg5GgIU4djUoNPZw3CyZLdmugbSxASvPbVjD22zSRcNK9q3zI/OY8e6qrGpCa9bwDz1eDx3YU1tKViQJ6r4w+SA5sfZO7cSrf2up7zx6tIuD1kYaPBYNij3/AXtK+x44uHCEa1Yroy8JempKc1TAGWzW4wqLvvG6n7qEeH3wdOkF6jNtRbbJxvsTYBhxWbkQ8wUDfiO9+rwzdqysOGjKKg2v6H30TUsVZZfrFckKksFQvsy/FLsMszdo0qh8KhqxkEAJDjkUpRcqlVNLA0LeQTMBSL+kdvxsUOgk6U9v+nw8yX2SB9WQlgCQDYeLw5mC+EJnPkh04Kor52Ppz/BTvIBJxFHSVNKVKtM0StuEz8PDQ6JA/jmd72InyeexKhcxVT2qCgjRT9QUFJHUjA2OSD9GVcbRhdhAq/csLTknGaem6s5yHPxnh1zPTBK704wUNVFrTFuJ5UlGSRAGbgD7eDeuZ2i2p0Zp8W8zeA0DmwxmULHSKxKe0liR2A0yyvGJ6UnpCQrGQD9p9q7u3amZ58Rp/a11tF39kEZtC6LN7o59zqvP0tEMTSMKjcpxyntJlQcEg1V7UnPcbUEpD2cQ6IFryjFM+K6BhcdUy1/N3E5VzqAOqw/4DjQLN4qaypCKUwm+xyoDfT13NobwCqtMaP1oPmoVrZwynTmxgPzHBYuWzRsOOM1ljaOOYw+bzuDNU0zFTMEMnMwKiZdFxMCPOhizIi5r6vYo2Cjp6wSus7O5OYPAsISSCFOKKIsXSr0K/QtCTSlO8k1bjINM1czNTTLLRgiVBL0+xfhi8oIuWCt9KVFqFyyUcAe1hDwYgZZFosjRivPK2UsCS33MQY5xECtPu458jjQN+Uy9itQGssC0OSLyeK3ba0ep1en8bCjwULZ4vQ7CRwbASDNISoiNiMpJhgwEj0SPHg6Izt7Ossz5CXxEpb1Ztg6wUizwqxCq8O4gM2A5tf/NRMMHH8fjR2XHVYiaS8BQRM/jD3oPLg3yCmWEavxL9OeuzqwbKq7roG/lNnr9WYKsxLZFVAWKxztJjA6UkTGQ6RC7TqiK0IOvubLx361Y67aq+C87dnJ9sUHkRCJEV8VVBtkMLRGHkY5RJY9RSuGCZvehcHTsQKthLPHzfTqEwAXB8QHeA1/HH42uUnLSTxEPTSiEvDmv8TDtLSumbao0p3yIAFpBSIGlA9bKBpHzEtqSD44fhfm6I3GbLfdspm+S+Hw+ukElQR/CNEeCj/1T+NLajsDF6rjQsEDtMiw27sK3IHwMPaY90AIACoaTNlRFknILsj9YNLcvCy2ZrnV2RbyhvUR968HaSodTn1U7kjqJ8nw7ckButa37MGp3/PtpOzz9akT+T5IWexSwjwWDf3ansS5v0+/79xc7Trv1/LSDX86Mll2Vo5BwhBB3OjC8b5uvjvSD+EK5J3n4gTtNUBYbVdgPucK9deLwme/m76gzsDXR9nJ5OoHBD1pW2VaSj9SCK/a18iKxH/Cws311krWpOOmDuBBI15MXBM+rgaH2TLKQsnBw1fHmc2Oz3vdiwpVQNlcwF5QRHIS1+TL0ZfQtstkwzPNbs5S2Gf8OzImV59lZ1DVICXtDdMzz+rOMMBlv+/BSsdQ3UoPgEO0XTFkM02jHUnwrNr02HPUO8PevLG+asG013sEKTymXPtnxln1M04GvOzs5YvmSdq5vQi517mUwEDauQ3IPuhapGWrWfI2eg0Q8+fpa+Vr3XfGPLF7sGayU7+34b4WuUQMXHllilkAOb4QBPVa64LqLuYR1fe2DKvNqmKvIsIe5x8Zs0MlW2ZkcGTJSd0naAgL+eDyXvCS6FbW4LnbpuClJatltlrU5gGFLWVNn11bY3xlQ01FLisPN/0r+2v6K/WS6KzSTLZCoDyh0qM0rAi/LuJiDOIyIUwSWl1fymCDVOY5OyCODa0FtgOtACT+UPQf4VjHC6wEnkuc/Z76puG2ZNOj9qYX/jPbSEVUzVeBWVlV7zulJOYTGgpqAS8CzQPXABH2VOaszk62q6APmBeZ/Z1sp/+4LtCe74YRmi+jQcJPb1dLW95Z5lmRUM45BSf6GIgPxgrWCiMOCQyzA8b5F+jJ0eW9zKikmTiZy5sUovOuOMC219X0khPMKnY9NU3wVK9VZ1YIV11SXkjZM/kgcxXADp0O7A67DzQRGAqfAHDxZNfpwUysF5z+kf6TvpjMoLWtpcMO3W38Xhn1MaNFVE4+VG9WgVZUVhNYJ0ecNHIiZxtgGBgWrxZMFoMKRQAl74DZQsCpqD6a+I/4kYiZ46aHuFzScvHfEpYq3TsHSPVMbkpkSupM0k5FPH8uBCgIJfcj3CUeIlwSUf/U5gzNGbMTnxuSzpMOmn2pYMLL4lADBx1wM3NDi0rDTr1PJ1FZSuM8SzdkMzsvfioeIf0J/OtmypeuJZyLkxeaeqfRvlrkPQu6KjM6lD+tQDFBVUdMSRI+zDtqOKQ1gipmFYLyKso8qiaY+JFInJKwwNfV/Lwa2Sx3NMk0yDr0RVBBizsEPYg7RTAqFA/sZb8noveVKZwRsCrXsACEHqgt4TEVMow3qEHrO7I4DTXTJpMIi91Qt8Khc5j2pgTK/PVPFxYkqiYoJ3Ax/kJQQAo9CDa1Id37+sqGqxeeJ6TPwkbvDRVIJHsnISvYO7NN9khXRuU8HiMR7/u/5qcpnvW0Z+NAC78ZVBd+G0Qtuki/TDNL7jzuE3ndJbYZpjCmh8WT6rEGDg2fEDYfVz+8SZVLvj2SGzXhirdRp5ajYsC76pcAbglZC/ccUj4rTAJKszewD9PWqrTGqXSsidB49WIHWwsyFW4tak55UPBJ4DBU/cPMS7RVrDjA4+WS/l0BgwSeGKY3X1BvTR46sRAA2iy7a6+RraPOIO0W9Kf02/+6If5JzU+4RoEpy/L/yIq6y7ZGxjji++6T73v0aA6gNzNSiU64OS8Rc9/swwi8W7oj0tfnh+y48HD85SBuRMxTlUsmMl0DRtmOxBu/h79g1QTmh+wG7rv71hwYQIxTtEtDNU8MHt9zyXfDOcPayzjdI+EV4gzoswLRKn1JTlElRHEoGf3/2xPPTMzuyx3Q/ts83pnd2ub//rYmD0UQUqBIni+PC67lV9GOy8bJxMgR0XHb7N/R317ssAhVLXxHaVNZTp08fBuO+77ldNsA2J/WR85U1I/Z5dxg3g/rJQfeKQVDMVAtUeVEnipRCXvsy9oa1zHY69YC0YTSC9VC177XZuOF9scTuTDvQrNMHU3pPPkkQwUh7WDg5Np6233aBtbvzDXOH9CtzxHQE9if6mwK9ia5OxNIiE60TDtAAi1kE1P58equ4+Tj9OLS3ZvU+8ecxqnIJMlizR3S7uCX9iIToCv1O0FGsksjSos8FiowE8P+fvMx65LpLOkJ6zHqyeCa1XPLjMpHy9XLiM9Q2C7q1P5DFesqIDuVRUlL5U5dSoY8qir2F4gGivw/9z30YvRp9JXwHut935bQWsZoxuzGDcd6yifTpOEt9N0LPyQzNOg+A0abSb9KYkDDMVgfQA0O/u3yAe9K7abv8OwB6K/fRtKaxVq+lL3Lvsa/AsSJzTHa4vI6DGIjSzaXQJNH1EoXSyZDGTE5IDgOjAGK+0b4GPqw+qr4vvC94obQkMDturG7nrxWwhPLLty08mgQMCiTOCZDkEj5StxLF0BQLxkd4g5QB3UDBgUIApj7Ye+m3nvKG7nettq3jbvsxaLZCvR+ECMqEzsfREhI8ElgQiYvqR04EW4JBwbrBkcCzfTz3s7JN7detFa3LsBEz2npbws2KoU6REL/RGlGgDg5IWwPXQaQA0EAxvi46JfRr7uurkexc7jdx6zlRQtyKVg5BkEiQzBDdy48HYgVghUvFK8JqvM81Ky53bEFufvJ+emzEZ4wjD5dQVdBUzuaJVMZ2xX3EwcHde7uzQ20m60GtZbJbe0tFk8vzDdROXw8SylxHpocXxpfCdDpVMSCrmGuCrut204HGSdRNLk1Rjd0KK0eqx3FGNsG4t5WvCOrwLPNzZP6diHfMJ8ydDVAMSQm6iVZICsJU95Tu52qBLaI1vcDvSERLmUu3jWOLkwrTyqqGLPyqMYQr1yykc4F/N4dkidKJ0swRy/0LNEq6hTd6XPABa6+t+rZugaFH+kipCWeNb8wZy8fJLj/c8+ls7WtO8Ve8e0U/R4sIEQuPjkKOKswsROi3t66Ea8qwIXqdg6vGZAbrSfoPEo7jzVrGb7l4b+xssi+Q+f3ClgWyhacJNw+wz0mN7MbP+mWwZ20tr2P4ywEHg4zDywe1DvvPsg3kRs857zCGbb4upLeTv6qB88JnRnEOIZCPT0dJaj03cq2u9+4qtdR+p0GigfaEt0ubEVZQawwhAg42MLB+ryPyinr4PpF/ooDEBthPQtGjD2SI+H2KdJPxODB69QE77n50vmAAm8dFD5CRac8RSLM9QXSX8QNw/jNhOac8GDx4PYEDosxrEaSQbwvbQkk4a7LhcYGxvzTZuUU6Wfpv/M6DscvF0UdQKQvEA6C6BzTest5y9HO++EF6m3q0e6m/yYdFzkvRtY/yy48DvDsmNcA0G/PIs7Q2xjmOuig6oTy3Qk/KMU9xUWJPnssfQ9p8LHdlNcS14/VZNby4LrjF+Mp5/z1Tw3PKNE7n0M6POYt9BKL9j/iFNqB1ojVmNLy1EDap9ty2/XfL+ytA3seOzPqPQ8/hjZEJDgNdfcG6XLhR94c34Dc5NRZ3FneJ97Q35PnOvYdC3wgpzH7OzFAjznXLDkZ3wLY8ETo+ONA4RLfj925143XJdqz25ncvODT6pb6bg16IfwvaTgpPLU49S5VHr4KTPqP7THm6+Of5NfjUeEa223WT9is1zjZ9NvW4ALrWflmDgogJSyUNBQ4Uza1Lp0iSBNYBJL4tfKr7z3v4e0y6mbkYdyo1qTWm9Zb1wXaD+BT6yz7eA1yHpwq1TDqM7IzaCuoHswPcwHG9pTwse9n8GLxpu8O63PiytiD2ZzZLdlc2yvgy+qX+sIMmRwgKPktczEyMiIqzh2uDswBfPg/9Drz1fJH85Lw7+lI387XG9gv2EXaL9+/50v3YAhMGjclECzGLpEvHycGHGcOrQQZ/YX8jft7+v32i+4f4jLZ8di02WbcH+N08DwDHBb2IhYpwSuwLBQkkBaeCMj/svw9+y375Pdw7+HjIdlE2hvbk9+m6iT85A+KH8smgSmxKZAjzRTlCFgAMv+9/uj9N/bp6FDbn9c42QLfCOuw/XAROR+6JUkozideGoUNHAY+BcoGzwM9+ELnIdk82AHcUuU99/kM1RvuI3MlIyafGGMMhAbaBawDdPy/7sPe89Yp23Tknvb0DS8d7CM6JOMj/haLC3cJJwovBYz3NuTj1sXYneBd8iIJphliIB0hXSC0ExwMsQtPC4wCRe/Z3JjWZ9xb6vgBwRSjHHgdhx+pFWgOFA0CC2X/C+zf2nHWjdwJ7rwHLRjsHCQesR6tFacTvBH3CVf2huDA1VHar+hNAacUrhvTHPwenBcwFnoUdAzB+DjhAtah2SvoZAC0D70TnBSrGHMSIBH2DvQELO+k3DTVNtzg7dQGwxMbFlUWMhuaFh4WNxDyAOHm7NjJ1lricvmcCzARXhKlFRcYOxajEygI8O/93bHWN9vY7JEDERCgE00UIB2IHF0bGBddBHfrA9yz1nDiuvf2CPgOTQ6LFNceMR5LHRcUWf9Y5j7b2tmC6fX9iQhNCTULbxLJHcYcHBo5DZ/1i+Hg2fHYL+jy+8wGuQcjCRoRKCAnH9EcjBFD/IfmO9zc2KfgCvG3/+oDUAN7B+YVQh9fHz0bZQ6e9mXj9Nvo2fnjd/VWAKYDKAJ4B48VGyKsIbAdzBKu/aLoLt9b3JHenOyE+YD/gf3y/hAG0RX2IashkR1iEi39f+sk4eTdPd3l55f0LPxj/Yj+YQOTDSgcXCSGItAdQxFs/g7sN+GG3b/cVd+J6vDyiPeJ9i35vwCnDcUbhiX9I48g8RZRCHb01ueL4G/enN6r4l3slfOB96H4jfiN/9cI0BVLINkl6yQ0IGgYUQsF+i/uFeY/43/ikuLd56TuzPPL9Gv0ofRK+O3+OwptF7Qh3CQ3I9YexRcODS0BsvQR7NbmvuQg5HbkmOiX7znzjPXD9a32zPmU/6QHExEwGnQhfyX2I/kgQxvnEY4GX/mS8MPqJOhG5+jmm+Ya5U/pke/F8FryM/IE8131e/pXAl4L7BM5HQki4SQaIzQgMRrnETsHqPwR8jfswOgo55bmf+fK5pnlGegt7HXuv+7L7ibvtO4b8jL4EgB2CfwSDRtYIBQjyyEIH9MZrhEGCfj+g/bh8IntxOtC69jqB+v96Ufojux+7nPvJ/Aa8PXxWfXb+FoA6wjiEUoZTB6xIQ0iXB90G9kUJAuKAIX4h/J37rXtFO4s7tPt+OwI67vtVvBs8KzvO/AR8k72Xf1+Bl4QQBdYHGkfqh/RHNAXHhCIBl79MPbf8tvwr/FJ8Uzw/O3965ntwu5a7pnvqPAl9VD8fwZrEKAXEhwEHl4bxxWeDcUDhvqL9Rvzw/L+8nLyv/AL7R3ve+/E7v/vCvN3+mMEVA63FekZ0xv1F70QNAbE/ND2VvQs9AX0pfKj7o3sl+1H7vnvz/M0/MoHkhFjFyoaaBkrE8wJLAB0+hj4mvfK9+L0RO+Y7gjvRvDx83j8AQlQEjkXKBneFpAOiwQR/Uv5g/h5+PP0au7e7FftBe/c9GIA5wylE9wWuBa0D/oF0v62+y/7nvny9KztRe0N7vfx+vpdCGkR8BTaFVYP4QUh/8P8yfzC+rv0+O017nTwOvY2Ak4NwhL2E9QQswfVADP+1P58/Iv1Ge6u7g7xsPgCBYcOPhLgErUNGwWxAM7/jf5e+Q/x/e2J7yT1/v9vCpoP8xDTDc8FdwH5AJj/gPos8sLuR/Cm9eEAdwqDDv4OwQqdAzYBugDq/hz4bPA/7zXyMPrDBT4MJQ4hDuQHjQOSAr8Byfw59E7v0vBU9roAlQmfDPUM1grVBYgEhwSSAGT4bvEA8bb0ov39Bj8LrgsRDBYHeAQbBOAB2/qE86fwCPOj+aQDbgmbCrAKhQioBVMFKQQF/872uPFH8nT2cv9xBkUJZQkUCmYH4QZmBs0Djvx89SPyYPQb+i0CzAYZCBYI4AhkBmcGugUdAqP67fTg8ln1X/uOAkIGCgf1Bp4I2garBgIGkQIP/Ej22PNQ9RP6qwDYBAQGHQYhB7QHEwecBvwEKQAR+uX1aPSU9j/7xAALBKkE4gTrBSoH4Qa3BlYFegHO+433vvVl9vP5IP/5AkYEOgSDBFQGKwcMB78GqwSVAJD7Gvid9r72dPnM/VsBcgPiA/QD9wT/BjAHMwenBqMEtQAB/Pr4ofdk93/5z/zh/8ABHwIvArMCeQSbBokGSgacBccDUACG/N35kPgj+DP5/fvQ/t4AwgHlAQQCtwJcBEAGNAYeBnkFEARqATv+c/v6+Vr5KPld+oH8s/5KALQA+wDsAI0BtgKjBL0FtwVsBdAEbQMzAX3+Xfz++mf6N/qj+jb8Cf6A/zQAZABdAHsAGwFVAs4DCgUEBdwEbQSMAwwC7f/q/ZD8vvtp+077vPsb/Xf+e/8IABsAAQAFAHUAOgFWApcDPQQzBPcDhgOUAjYBiv8S/gT9mvxP/Ej8TfxW/VP+Hv+a/9L/wv/V/w0AoQBbAWcCQgNxA1gDIgOlAt0BwgCE/3r+yf13/Vf9Uf19/SP+1/5m/7j/3f/l/wQAPwC+AHABEwKEAnICQwL7AXkBygDr/y//qf5t/l7+Xv5n/sf+Mf92/5//qP+j/77/AABnAOAAQAF7AWMBQgEJAacAMwDB/3j/WP9R/1j/Xf+J/8H/3f/g/+H/6f/8/x4ARQBjAG4AYwBSADoAIAAMAAQABwA=`}],w=[`pointerdown`,`pointerup`,`touchend`,`click`,`keydown`],T={jab:{from:520,to:1500,duration:.09},straight:{from:420,to:1150,duration:.12},hook:{from:300,to:880,duration:.14},uppercut:{from:240,to:820,duration:.15}},E=class{settings;context=null;master=null;noiseBuffer=null;injuryBuffers=new Map;crowdGain=null;unlocked=!1;unlockPromise=null;crowdStarted=!1;destroyed=!1;timers=new Set;lastBreathTick=-300;lastHeartbeatTick=-300;clapperRound=0;unlockListener=()=>{let e=this.context;e!==null&&e.state!==`running`&&e.resume().catch(()=>void 0),this.unlock().catch(()=>void 0)};visibility=()=>{let e=this.context;if(e===null)return;let t=document.hidden?e.suspend():this.unlocked?e.resume():null;t!==null&&t.catch(()=>void 0)};constructor(e){this.settings=e;for(let e of w)window.addEventListener(e,this.unlockListener);document.addEventListener(`visibilitychange`,this.visibility)}unlock(){return this.destroyed||this.unlocked?Promise.resolve():(this.unlockPromise===null&&(this.unlockPromise=this.performUnlock().finally(()=>{this.unlockPromise=null})),this.unlockPromise)}async performUnlock(){let e=this.context;if(e===null){e=new AudioContext;let t=e.createGain();t.gain.value=Math.min(.8,Math.max(0,this.settings().volume)),t.connect(e.destination),this.context=e,this.master=t}if(e.state===`suspended`&&await e.resume(),this.destroyed||this.context!==e)return;let t=await this.decodeInjuryBuffers(e);if(!(this.destroyed||this.context!==e)){this.injuryBuffers=t,this.unlocked=!0;for(let e of w)window.removeEventListener(e,this.unlockListener);this.crowdStarted||(this.crowdStarted=!0,this.crowdBed())}}setVolume(){this.master!==null&&(this.master.gain.value=Math.min(.8,Math.max(0,this.settings().volume)))}event(e){if(this.unlocked)switch(e.kind){case`punch_start`:{let t=T[e.detail.split(`:`)[1]??`jab`]??T.jab;this.noise({duration:t.duration,frequency:t.from,sweepTo:t.to,gain:.1,type:`bandpass`,q:1.2});break}case`whiff`:this.noise({duration:.11,frequency:850,sweepTo:320,gain:.07,type:`bandpass`,q:1.1});break;case`hit`:case`counter_hit`:{let t=e.detail.endsWith(`body`),n=Math.min(1,e.amount/260),r=Math.min(.36,.14+n*.2)*(e.kind===`counter_hit`?1.25:1);this.impact(r,t,n),(e.kind===`counter_hit`||n>.7)&&this.crowdSwell(.35+n*.4);break}case`block`:this.noise({duration:.045,frequency:1200,sweepTo:600,gain:.14,type:`bandpass`,q:1.1}),this.noise({duration:.09,frequency:260,gain:.08}),this.tone({from:160,to:110,duration:.06,type:`triangle`,gain:.05});break;case`perfect_block`:this.noise({duration:.05,frequency:1800,sweepTo:900,gain:.16,type:`bandpass`,q:1.6}),this.tone({from:280,to:210,duration:.06,type:`triangle`,gain:.06});break;case`guard_break`:this.impact(.3,!1,.9),this.tone({from:230,to:78,duration:.2,type:`sawtooth`,gain:.08}),this.crowdSwell(.5);break;case`stun`:this.tone({from:96,to:74,duration:.22,type:`sine`,gain:.1}),this.crowdSwell(.3);break;case`knockdown`:this.impact(.34,!1,1),this.tone({from:60,to:28,duration:.42,type:`sine`,gain:.3}),this.noise({duration:.3,frequency:140,gain:.24}),this.crowdRoar(1);break;case`bell`:{let t=e.detail===`round_start`?3:1;for(let e=0;e10*r||this.clapperRound===t||(this.clapperRound=t,!this.unlocked))return;this.noise({duration:.05,frequency:2600,gain:.42,type:`bandpass`,q:1.4}),this.noise({duration:.09,frequency:700,gain:.18,type:`bandpass`,q:.8});let i=window.setTimeout(()=>{this.timers.delete(i),this.noise({duration:.05,frequency:2400,gain:.4,type:`bandpass`,q:1.4}),this.noise({duration:.09,frequency:660,gain:.16,type:`bandpass`,q:.8})},120);this.timers.add(i)}snapshot(e,t,n,r){if(!this.unlocked)return;let i=1-t/Math.max(1,n);i>.55&&e-this.lastBreathTick>=75&&(this.lastBreathTick=e,this.noise({duration:.18,frequency:420,gain:.045+i*.04})),(i>.72||r>500)&&e-this.lastHeartbeatTick>=24&&(this.lastHeartbeatTick=e,this.tone({from:52,duration:.09,type:`sine`,gain:.055}))}result(e){if(!this.unlocked)return;this.crowdSwell(1),this.tone({from:e.winner_id===null?280:520,duration:.45,type:`triangle`,gain:.18});let t=window.setTimeout(()=>{this.timers.delete(t),this.tone({from:650,duration:.5,type:`triangle`,gain:.14})},160);this.timers.add(t)}injury(e){let t=this.context,n=this.master,r=this.injuryBuffers.get(e),i=this.settings();if(!this.unlocked||t===null||n===null||r===void 0||i.blood!==`full`||i.reducedMotion)return;let a=t.createBufferSource(),o=t.createGain();a.buffer=r,o.gain.value=.16,a.connect(o).connect(n),a.start()}async decodeInjuryBuffers(e){let t=await Promise.all(C.map(async t=>{let n=atob(t.wav),r=new Uint8Array(n.length);for(let e=0;evoid 0)}},D=class extends Error{name=`ProtocolError`},O=2147483647,ee=new TextEncoder;function k(e){let t=0,n=()=>{for(;/\s/u.test(e[t]??``);)t+=1},r=()=>{let n=t;if(e[t++]!==`"`)throw new D(`invalid JSON`);for(;t{n();let a=e[t];if(a===`{`){t+=1,n();let a=new Set;if(e[t]===`}`){t+=1;return}for(;;){n();let o=r();if(a.has(o))throw new D(`duplicate field ${o}`);if(a.add(o),n(),e[t++]!==`:`)throw new D(`invalid JSON`);i(),n();let s=e[t++];if(s===`}`)return;if(s!==`,`)throw new D(`invalid JSON`)}}if(a===`[`){if(t+=1,n(),e[t]===`]`){t+=1;return}for(;;){i(),n();let r=e[t++];if(r===`]`)return;if(r!==`,`)throw new D(`invalid JSON`)}}if(a===`"`){r();return}let o=e.slice(t),s=/^(?:-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)/u.exec(o);if(!s)throw new D(`invalid JSON`);t+=s[0].length};if(i(),n(),t!==e.length)throw new D(`invalid JSON`)}function A(e){let t,n;if(typeof e==`string`?(t=ee.encode(e),n=e):(t=e instanceof Uint8Array?e:new Uint8Array(e),n=new TextDecoder(`utf-8`,{fatal:!0}).decode(t)),t.byteLength>65536)throw new D(`server frame too large`);try{return k(n),JSON.parse(n)}catch(e){throw e instanceof D?e:new D(`invalid JSON`)}}function te(e,t){if(typeof e!=`object`||!e||Array.isArray(e)||Object.getPrototypeOf(e)!==Object.prototype)throw new D(`${t} must be an object`);return e}function j(e,t,n=[]){let r=new Set([...t,...n]),i=Object.keys(e);if(t.some(t=>!Object.hasOwn(e,t))||i.some(e=>!r.has(e)))throw new D(`schema mismatch`)}function M(e,t,n=0,r=O){if(!Number.isSafeInteger(e)||er)throw new D(`${t} must be a bounded integer`);return e}function ne(e,t){if(typeof e!=`boolean`)throw new D(`${t} must be boolean`);return e}function N(e,t,n=255,r=1){if(typeof e!=`string`||e.lengthn||/[\u0000-\u001f\u007f]/u.test(e))throw new D(`${t} must be a bounded string`);return e}function P(e,t,n=255){return e===null?null:N(e,t,n)}function F(e,t,n){if(typeof e!=`string`||!t.includes(e))throw new D(`invalid ${n}`);return e}function re(e,t,n){if(!Array.isArray(e)||e.length>n)throw new D(`${t} must be a bounded array`);return e}var ie=[`left`,`right`],ae=[`jab`,`straight`,`hook`,`uppercut`],I=[`head`,`body`],L=[`normal`,`power`],oe=[`orthodox`,`southpaw`],se=[`none`,`guard_high`,`guard_low`,`slip_left`,`slip_right`,`weave`,`pull`],ce=[`none`,`guard_high`,`guard_low`],le=[`slip_left`,`slip_right`,`weave`,`pull`,`clinch`,`switch_stance`,`get_up_left`,`get_up_right`,`taunt`],ue=[`low_blow`,`headbutt`],de=[`countdown`,`fight`,`knockdown`,`foul_recovery`,`rest`,`complete`],fe=[`ko`,`flash_ko`,`tko`,`doctor_stoppage`,`disqualification`,`decision`,`draw`,`forfeit`];function pe(e){let t=te(e,`player`);return j(t,[`id`,`name`,`avatar`,`rating`,`connected`]),{id:N(t.id,`player.id`),name:N(t.name,`player.name`,80),avatar:P(t.avatar,`player.avatar`,128),rating:M(t.rating,`rating`),connected:ne(t.connected,`connected`)}}function me(e,t){let n=re(e,`players`,2).map(pe);if(n.length<1||t!==void 0&&n.length!==t||new Set(n.map(e=>e.id)).size!==n.length)throw new D(`players must be present and distinct`);return n}function he(e){let t=te(e,`trauma`);return j(t,[`head`,`body`,`left_eye`,`right_eye`,`left_cut`,`right_cut`,`swelling`,`bleeding`]),{head:M(t.head,`head`,0,1400),body:M(t.body,`body`,0,1200),left_eye:M(t.left_eye,`left_eye`,0,1e3),right_eye:M(t.right_eye,`right_eye`,0,1e3),left_cut:M(t.left_cut,`left_cut`,0,1e3),right_cut:M(t.right_cut,`right_cut`,0,1e3),swelling:M(t.swelling,`swelling`,0,1e3),bleeding:M(t.bleeding,`bleeding`,0,1e3)}}function ge(e){let t=te(e,`fighter`);j(t,`player_id.x.y.facing.facing_x.facing_y.velocity_x.velocity_y.stance.defense.stamina.maximum_stamina.conditioning.guard.poise.trauma.knockdowns.warnings.deductions.stunned_ticks.is_downed.action.action_hand.action_target.action_power.action_id.action_key.action_start_tick.action_startup_ticks.action_active_ticks.action_recovery_ticks.action_contact_tick.queued_actions.clinch_startup_ticks.clinch_ticks.is_foul_recovery_target.taunt_ticks.get_up_prompt.get_up_meter.get_up_required.get_up_count.get_up_window_start_tick.get_up_window_end_tick.last_input_sequence`.split(`.`));let n=t.action===null?null:F(t.action,ae,`action`),r=t.action_hand===null?null:F(t.action_hand,ie,`action hand`),i=t.action_target===null?null:F(t.action_target,I,`action target`),a=t.action_power===null?null:F(t.action_power,L,`action power`);if([n,r,i,a].some(e=>e===null)&&[n,r,i,a].some(e=>e!==null))throw new D(`partial punch action`);let o=t.action_id===null?null:N(t.action_id,`action_id`,32),s=t.action_key===null?null:N(t.action_key,`action_key`,64);if([o,s].some(e=>e===null)!==[o,s].every(e=>e===null))throw new D(`partial action instance`);if(o!==null&&n===null)throw new D(`action instance without class`);let c=t.get_up_prompt===null?null:F(t.get_up_prompt,[`get_up_left`,`get_up_right`],`get-up prompt`),l=M(t.maximum_stamina,`maximum_stamina`,330,1e3),u=M(t.stamina,`stamina`,0,l),d=M(t.get_up_required,`get_up_required`,0,169),f=M(t.facing,`facing`,-1,1);if(f===0)throw new D(`invalid facing`);let p=M(t.facing_x,`facing_x`,-1e3,1e3),m=M(t.facing_y,`facing_y`,-1e3,1e3);if(p===0&&m===0)throw new D(`invalid facing vector`);if(d!==0&&d<34)throw new D(`invalid get-up requirement`);return{player_id:N(t.player_id,`player_id`),x:M(t.x,`x`,-462,462),y:M(t.y,`y`,-462,462),facing:f,facing_x:p,facing_y:m,velocity_x:M(t.velocity_x,`velocity_x`,-7,7),velocity_y:M(t.velocity_y,`velocity_y`,-7,7),stance:F(t.stance,oe,`stance`),defense:F(t.defense,se,`defense`),stamina:u,maximum_stamina:l,conditioning:M(t.conditioning,`conditioning`,0,1e3),guard:M(t.guard,`guard`,0,700),poise:M(t.poise,`poise`,0,600),trauma:he(t.trauma),knockdowns:M(t.knockdowns,`knockdowns`,0,3),warnings:M(t.warnings,`warnings`,0,3),deductions:M(t.deductions,`deductions`,0,1),stunned_ticks:M(t.stunned_ticks,`stunned_ticks`,0,90),is_downed:ne(t.is_downed,`is_downed`),action:n,action_hand:r,action_target:i,action_power:a,action_id:o,action_key:s,action_start_tick:M(t.action_start_tick,`action_start_tick`,0),action_startup_ticks:M(t.action_startup_ticks,`action_startup_ticks`,0,40),action_active_ticks:M(t.action_active_ticks,`action_active_ticks`,0,10),action_recovery_ticks:M(t.action_recovery_ticks,`action_recovery_ticks`,0,40),action_contact_tick:t.action_contact_tick===null?null:M(t.action_contact_tick,`action_contact_tick`,0),queued_actions:M(t.queued_actions,`queued_actions`,0,1),clinch_startup_ticks:M(t.clinch_startup_ticks,`clinch_startup_ticks`,0,8),clinch_ticks:M(t.clinch_ticks,`clinch_ticks`,0,45),is_foul_recovery_target:ne(t.is_foul_recovery_target,`is_foul_recovery_target`),taunt_ticks:M(t.taunt_ticks,`taunt_ticks`,0,60),get_up_prompt:c,get_up_meter:M(t.get_up_meter,`get_up_meter`,0,256),get_up_required:d,get_up_count:M(t.get_up_count,`get_up_count`,0,10),get_up_window_start_tick:M(t.get_up_window_start_tick,`get_up_window_start_tick`),get_up_window_end_tick:M(t.get_up_window_end_tick,`get_up_window_end_tick`),last_input_sequence:M(t.last_input_sequence,`last_input_sequence`,-1)}}function _e(e){let t=te(e,`event`);return j(t,[`event_id`,`tick`,`kind`,`actor_id`,`target_id`,`amount`,`detail`,`blood`,`direction`,`action_id`]),{event_id:M(t.event_id,`event_id`),tick:M(t.tick,`tick`),kind:N(t.kind,`event kind`,32),actor_id:P(t.actor_id,`actor_id`),target_id:P(t.target_id,`target_id`),amount:M(t.amount,`amount`,-1e4,1e4),detail:N(t.detail,`detail`,96,0),blood:M(t.blood,`blood`,0,100),direction:M(t.direction,`direction`,-1,1),action_id:t.action_id===null?null:N(t.action_id,`action_id`,32)}}function ve(e){let t=te(e,`judge card`);j(t,[`judge`,`player_one`,`player_two`]);let n=e=>re(e,`round scores`,15).map(e=>M(e,`score`,6,10)),r=n(t.player_one),i=n(t.player_two);if(r.length!==i.length)throw new D(`scorecard round mismatch`);return{judge:N(t.judge,`judge`,80),player_one:r,player_two:i}}function ye(e){let t=re(e,`scorecards`,3).map(ve);if(t.length!==3)throw new D(`exactly three scorecards required`);return t}function R(e,t){if(t===`draw`!=(e===null))throw new D(`incoherent finish result`)}function be(e){let t=te(e,`result`);j(t,[`match_id`,`activity_instance_id`,`guild_id`,`player_one_id`,`player_two_id`,`winner_id`,`finish_method`,`round_number`,`tick`,`scorecards`,`player_one_knockdowns`,`player_two_knockdowns`,`player_one_damage`,`player_two_damage`]);let n=N(t.player_one_id,`player_one_id`),r=N(t.player_two_id,`player_two_id`);if(n===r)throw new D(`result players must be distinct`);let i=P(t.winner_id,`winner_id`);if(i!==null&&i!==n&&i!==r)throw new D(`invalid winner`);let a=F(t.finish_method,fe,`finish method`);return R(i,a),{match_id:N(t.match_id,`match_id`),activity_instance_id:N(t.activity_instance_id,`activity_instance_id`),guild_id:N(t.guild_id,`guild_id`),player_one_id:n,player_two_id:r,winner_id:i,finish_method:a,round_number:M(t.round_number,`round`,1,15),tick:M(t.tick,`tick`),scorecards:ye(t.scorecards),player_one_knockdowns:M(t.player_one_knockdowns,`knockdowns`,0,3),player_two_knockdowns:M(t.player_two_knockdowns,`knockdowns`,0,3),player_one_damage:M(t.player_one_damage,`damage`),player_two_damage:M(t.player_two_damage,`damage`)}}function xe(e){let t=te(e,`snapshot`);j(t,[`tick`,`phase`,`round_number`,`phase_ticks_remaining`,`fighters`,`events`,`result`,`checksum`]);let n=re(t.fighters,`fighters`,2).map(ge);if(n.length!==2||n[0]?.player_id===n[1]?.player_id)throw new D(`snapshot requires two distinct fighters`);let r=N(t.checksum,`checksum`,64);if(!/^[a-f0-9]{64}$/u.test(r))throw new D(`invalid checksum`);return{tick:M(t.tick,`tick`),phase:F(t.phase,de,`phase`),round_number:M(t.round_number,`round`,1,15),phase_ticks_remaining:M(t.phase_ticks_remaining,`phase ticks`),fighters:[n[0],n[1]],events:re(t.events,`events`,256).map(_e),result:t.result===null?null:be(t.result),checksum:r}}function Se(e){let t=te(A(e),`envelope`);if(t.version!==3)throw new D(`unsupported protocol version`);let n=N(t.type,`type`,32);if(n===`welcome`){let e=F(t.role,[`fighter`,`spectator`],`connection role`),r=N(t.player_id,`player_id`),i=t.reconnect_ticket===void 0?{}:{reconnect_ticket:N(t.reconnect_ticket,`reconnect_ticket`,4096)};if(e===`fighter`){j(t,[`version`,`type`,`role`,`player_id`,`seat`,`rating`,`players`,`server_tick`,`next_sequence`],[`reconnect_ticket`]);let a=me(t.players);if(!a.some(e=>e.id===r))throw new D(`welcome fighter is absent`);return{version:3,type:n,role:e,player_id:r,seat:M(t.seat,`seat`,1,2),rating:M(t.rating,`rating`),players:a,server_tick:M(t.server_tick,`server_tick`),next_sequence:M(t.next_sequence,`next_sequence`),...i}}j(t,[`version`,`type`,`role`,`player_id`,`players`,`server_tick`],[`reconnect_ticket`]);let a=me(t.players,2);if(a.some(e=>e.id===r))throw new D(`spectator cannot be a fighter`);return{version:3,type:n,role:e,player_id:r,players:[a[0],a[1]],server_tick:M(t.server_tick,`server_tick`),...i}}if(n===`ticket`)return j(t,[`version`,`type`,`reconnect_ticket`,`refresh_id`]),{version:3,type:n,reconnect_ticket:N(t.reconnect_ticket,`reconnect_ticket`,4096),refresh_id:N(t.refresh_id,`refresh_id`,128,16)};if(n===`waiting`){if(j(t,[`version`,`type`,`open_seats`]),t.open_seats!==1)throw new D(`invalid open seats`);return{version:3,type:n,open_seats:1}}if(n===`ready`){j(t,[`version`,`type`,`players`]);let e=me(t.players,2);return{version:3,type:n,players:[e[0],e[1]]}}if(n===`paused`)return j(t,[`version`,`type`,`player_id`,`grace_ms`]),{version:3,type:n,player_id:N(t.player_id,`player_id`),grace_ms:M(t.grace_ms,`grace_ms`,0,6e4)};if(n===`resumed`)return j(t,[`version`,`type`,`player_id`]),{version:3,type:n,player_id:N(t.player_id,`player_id`)};if(n===`snapshot`)return j(t,[`version`,`type`,`payload`]),{version:3,type:n,payload:xe(t.payload)};if(n===`error`)return j(t,[`version`,`type`,`code`]),{version:3,type:n,code:N(t.code,`error code`,80)};if(n===`final`)return z(t,n);throw new D(`unsupported server message type`)}function z(e,t){j(e,[`version`,`type`,`match_id`,`winner_id`,`method`,`round`,`scorecards`,`ratings`]);let n=te(e.ratings,`ratings`),r=Object.entries(n);if(r.length!==2)throw new D(`final requires two ratings`);let i=Object.create(null);for(let[e,t]of r){N(e,`rating player id`);let n=te(t,`rating`);j(n,[`before`,`after`]),i[e]={before:M(n.before,`before`),after:M(n.after,`after`)}}let a=P(e.winner_id,`winner_id`);if(a!==null&&!Object.hasOwn(i,a))throw new D(`winner absent from ratings`);let o=F(e.method,fe,`finish method`);return R(a,o),{version:3,type:t,match_id:N(e.match_id,`match_id`),winner_id:a,method:o,round:M(e.round,`round`,1,15),scorecards:ye(e.scorecards),ratings:i}}function Ce(e,t,n){return JSON.stringify({version:3,type:`input`,sequence:M(e,`sequence`),client_tick:M(t,`client tick`),move:{x:Math.max(-1e3,Math.min(1e3,Math.round(Number.isFinite(n.moveX)?n.moveX:0))),y:Math.max(-1e3,Math.min(1e3,Math.round(Number.isFinite(n.moveY)?n.moveY:0)))},defense:F(n.defense,ce,`held defense`),actions:n.actions.slice(0,4).map(e=>{let t=e.id===void 0?{}:{id:e.id.slice(0,32)};return e.kind===`punch`?{kind:`punch`,hand:F(e.hand,ie,`hand`),class:F(e.class,ae,`class`),target:F(e.target,I,`target`),power:F(e.power,L,`power`),...t}:e.kind===`foul`?{kind:`foul`,foul:F(e.foul,ue,`foul`),...t}:{kind:F(e.kind,le,`action`),...t}})})}function we(e){let t=te(e,`bootstrap`);if(j(t,[`client_id`,`state`,`protocol`,`simulation`]),t.protocol!==3)throw new D(`unsupported protocol version`);let n=te(t.simulation,`simulation`);return j(n,[`tick_rate`,`ring_half_width`,`ring_half_height`]),{client_id:N(t.client_id,`client_id`,36),state:N(t.state,`state`,128),protocol:3,simulation:{tick_rate:M(n.tick_rate,`tick rate`,1,120),ring_half_width:M(n.ring_half_width,`ring width`,1),ring_half_height:M(n.ring_half_height,`ring height`,1)}}}function Te(e){let t=te(e,`token response`);j(t,[`access_token`,`ticket`,`player`]);let n=te(t.player,`player`);return j(n,[`id`,`name`,`avatar`,`rating`]),{access_token:N(t.access_token,`access token`,4096),ticket:N(t.ticket,`ticket`,4096),player:{id:N(n.id,`id`),name:N(n.name,`name`,80),avatar:P(n.avatar,`avatar`,128),rating:M(n.rating,`rating`)}}}var Ee=class extends Error{code;reloadRequired;name=`ClientError`;constructor(e,t=!1){super(e),this.code=e,this.reloadRequired=t}};async function De(e,t){if(!e.ok)throw new Ee(e.status===429?`rate_limited`:t);if(!(e.headers.get(`content-type`)??``).toLowerCase().startsWith(`application/json`))throw new Ee(t);try{return A(await e.text())}catch{throw new Ee(t)}}function Oe(e=window.location.search){let t=new URLSearchParams(e).getAll(`instance_id`);if(t.length!==1||!/^[A-Za-z0-9_-]{1,255}$/u.test(t[0]??``))throw new Ee(`invalid_launch`);return t[0]}function ke(e){if(typeof e==`object`&&e&&`protocol`in e&&e.protocol!==3)throw new Ee(`client_outdated`,!0)}async function Ae(e,t){let n=new URL(`/api/hands/bootstrap`,window.location.origin);try{let r=await De(await fetch(n,{method:`POST`,credentials:`same-origin`,cache:`no-store`,headers:{"Content-Type":`application/json`},body:JSON.stringify({instance_id:e}),...t===void 0?{}:{signal:t}}),`bootstrap_failed`);return ke(r),we(r)}catch(e){throw e instanceof Ee?e:e instanceof D?new Ee(`invalid_bootstrap`):new Ee(`bootstrap_failed`)}}async function je(e,t,n){try{return Te(await De(await fetch(new URL(`/api/hands/token`,window.location.origin),{method:`POST`,credentials:`same-origin`,cache:`no-store`,headers:{"Content-Type":`application/json`},body:JSON.stringify({code:e,state:t}),...n===void 0?{}:{signal:n}}),`token_failed`))}catch(e){throw e instanceof Ee?e:e instanceof D?new Ee(`invalid_token_response`):new Ee(`token_failed`)}}function Me(e){return e instanceof Ee?e.code:e instanceof D?`protocol_error`:`unexpected_error`}var Ne=typeof globalThis<`u`?globalThis:typeof window<`u`?window:typeof global<`u`?global:typeof self<`u`?self:{};function Pe(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,`default`)?e.default:e}var Fe={exports:{}},Ie;function Le(){return Ie?Fe.exports:(Ie=1,(function(e){var t=Object.prototype.hasOwnProperty,n=`~`;function r(){}Object.create&&(r.prototype=Object.create(null),new r().__proto__||(n=!1));function i(e,t,n){this.fn=e,this.context=t,this.once=n||!1}function a(e,t,r,a,o){if(typeof r!=`function`)throw TypeError(`The listener must be a function`);var s=new i(r,a||e,o),c=n?n+t:t;return e._events[c]?e._events[c].fn?e._events[c]=[e._events[c],s]:e._events[c].push(s):(e._events[c]=s,e._eventsCount++),e}function o(e,t){--e._eventsCount===0?e._events=new r:delete e._events[t]}function s(){this._events=new r,this._eventsCount=0}s.prototype.eventNames=function(){var e=[],r,i;if(this._eventsCount===0)return e;for(i in r=this._events)t.call(r,i)&&e.push(n?i.slice(1):i);return Object.getOwnPropertySymbols?e.concat(Object.getOwnPropertySymbols(r)):e},s.prototype.listeners=function(e){var t=n?n+e:e,r=this._events[t];if(!r)return[];if(r.fn)return[r.fn];for(var i=0,a=r.length,o=Array(a);ie;function t(e){}e.assertIs=t;function n(e){throw Error()}e.assertNever=n,e.arrayToEnum=e=>{let t={};for(let n of e)t[n]=n;return t},e.getValidEnumValues=t=>{let n=e.objectKeys(t).filter(e=>typeof t[t[e]]!=`number`),r={};for(let e of n)r[e]=t[e];return e.objectValues(r)},e.objectValues=t=>e.objectKeys(t).map(function(e){return t[e]}),e.objectKeys=typeof Object.keys==`function`?e=>Object.keys(e):e=>{let t=[];for(let n in e)Object.prototype.hasOwnProperty.call(e,n)&&t.push(n);return t},e.find=(e,t)=>{for(let n of e)if(t(n))return n},e.isInteger=typeof Number.isInteger==`function`?e=>Number.isInteger(e):e=>typeof e==`number`&&isFinite(e)&&Math.floor(e)===e;function r(e,t=` | `){return e.map(e=>typeof e==`string`?`'${e}'`:e).join(t)}e.joinValues=r,e.jsonStringifyReplacer=(e,t)=>typeof t==`bigint`?t.toString():t})(ze||={});var Be;(function(e){e.mergeShapes=(e,t)=>({...e,...t})})(Be||={});var B=ze.arrayToEnum([`string`,`nan`,`number`,`integer`,`float`,`boolean`,`date`,`bigint`,`symbol`,`function`,`undefined`,`null`,`array`,`object`,`unknown`,`promise`,`void`,`never`,`map`,`set`]),Ve=e=>{switch(typeof e){case`undefined`:return B.undefined;case`string`:return B.string;case`number`:return isNaN(e)?B.nan:B.number;case`boolean`:return B.boolean;case`function`:return B.function;case`bigint`:return B.bigint;case`symbol`:return B.symbol;case`object`:return Array.isArray(e)?B.array:e===null?B.null:e.then&&typeof e.then==`function`&&e.catch&&typeof e.catch==`function`?B.promise:typeof Map<`u`&&e instanceof Map?B.map:typeof Set<`u`&&e instanceof Set?B.set:typeof Date<`u`&&e instanceof Date?B.date:B.object;default:return B.unknown}},V=ze.arrayToEnum([`invalid_type`,`invalid_literal`,`custom`,`invalid_union`,`invalid_union_discriminator`,`invalid_enum_value`,`unrecognized_keys`,`invalid_arguments`,`invalid_return_type`,`invalid_date`,`invalid_string`,`too_small`,`too_big`,`invalid_intersection_types`,`not_multiple_of`,`not_finite`]),He=e=>JSON.stringify(e,null,2).replace(/"([^"]+)":/g,`$1:`),Ue=class e extends Error{constructor(e){super(),this.issues=[],this.addIssue=e=>{this.issues=[...this.issues,e]},this.addIssues=(e=[])=>{this.issues=[...this.issues,...e]};let t=new.target.prototype;Object.setPrototypeOf?Object.setPrototypeOf(this,t):this.__proto__=t,this.name=`ZodError`,this.issues=e}get errors(){return this.issues}format(e){let t=e||function(e){return e.message},n={_errors:[]},r=e=>{for(let i of e.issues)if(i.code===`invalid_union`)i.unionErrors.map(r);else if(i.code===`invalid_return_type`)r(i.returnTypeError);else if(i.code===`invalid_arguments`)r(i.argumentsError);else if(i.path.length===0)n._errors.push(t(i));else{let e=n,r=0;for(;re.message){let t={},n=[];for(let r of this.issues)r.path.length>0?(t[r.path[0]]=t[r.path[0]]||[],t[r.path[0]].push(e(r))):n.push(e(r));return{formErrors:n,fieldErrors:t}}get formErrors(){return this.flatten()}};Ue.create=e=>new Ue(e);var We=(e,t)=>{let n;switch(e.code){case V.invalid_type:n=e.received===B.undefined?`Required`:`Expected ${e.expected}, received ${e.received}`;break;case V.invalid_literal:n=`Invalid literal value, expected ${JSON.stringify(e.expected,ze.jsonStringifyReplacer)}`;break;case V.unrecognized_keys:n=`Unrecognized key(s) in object: ${ze.joinValues(e.keys,`, `)}`;break;case V.invalid_union:n=`Invalid input`;break;case V.invalid_union_discriminator:n=`Invalid discriminator value. Expected ${ze.joinValues(e.options)}`;break;case V.invalid_enum_value:n=`Invalid enum value. Expected ${ze.joinValues(e.options)}, received '${e.received}'`;break;case V.invalid_arguments:n=`Invalid function arguments`;break;case V.invalid_return_type:n=`Invalid function return type`;break;case V.invalid_date:n=`Invalid date`;break;case V.invalid_string:typeof e.validation==`object`?`includes`in e.validation?(n=`Invalid input: must include "${e.validation.includes}"`,typeof e.validation.position==`number`&&(n=`${n} at one or more positions greater than or equal to ${e.validation.position}`)):`startsWith`in e.validation?n=`Invalid input: must start with "${e.validation.startsWith}"`:`endsWith`in e.validation?n=`Invalid input: must end with "${e.validation.endsWith}"`:ze.assertNever(e.validation):n=e.validation===`regex`?`Invalid`:`Invalid ${e.validation}`;break;case V.too_small:n=e.type===`array`?`Array must contain ${e.exact?`exactly`:e.inclusive?`at least`:`more than`} ${e.minimum} element(s)`:e.type===`string`?`String must contain ${e.exact?`exactly`:e.inclusive?`at least`:`over`} ${e.minimum} character(s)`:e.type===`number`?`Number must be ${e.exact?`exactly equal to `:e.inclusive?`greater than or equal to `:`greater than `}${e.minimum}`:e.type===`date`?`Date must be ${e.exact?`exactly equal to `:e.inclusive?`greater than or equal to `:`greater than `}${new Date(Number(e.minimum))}`:`Invalid input`;break;case V.too_big:n=e.type===`array`?`Array must contain ${e.exact?`exactly`:e.inclusive?`at most`:`less than`} ${e.maximum} element(s)`:e.type===`string`?`String must contain ${e.exact?`exactly`:e.inclusive?`at most`:`under`} ${e.maximum} character(s)`:e.type===`number`?`Number must be ${e.exact?`exactly`:e.inclusive?`less than or equal to`:`less than`} ${e.maximum}`:e.type===`bigint`?`BigInt must be ${e.exact?`exactly`:e.inclusive?`less than or equal to`:`less than`} ${e.maximum}`:e.type===`date`?`Date must be ${e.exact?`exactly`:e.inclusive?`smaller than or equal to`:`smaller than`} ${new Date(Number(e.maximum))}`:`Invalid input`;break;case V.custom:n=`Invalid input`;break;case V.invalid_intersection_types:n=`Intersection results could not be merged`;break;case V.not_multiple_of:n=`Number must be a multiple of ${e.multipleOf}`;break;case V.not_finite:n=`Number must be finite`;break;default:n=t.defaultError,ze.assertNever(e)}return{message:n}},Ge=We;function Ke(e){Ge=e}function qe(){return Ge}var Je=e=>{let{data:t,path:n,errorMaps:r,issueData:i}=e,a=[...n,...i.path||[]],o={...i,path:a};if(i.message!==void 0)return{...i,path:a,message:i.message};let s=``,c=r.filter(e=>!!e).slice().reverse();for(let e of c)s=e(o,{data:t,defaultError:s}).message;return{...i,path:a,message:s}},Ye=[];function H(e,t){let n=qe(),r=Je({issueData:t,data:e.data,path:e.path,errorMaps:[e.common.contextualErrorMap,e.schemaErrorMap,n,n===We?void 0:We].filter(e=>!!e)});e.common.issues.push(r)}var Xe=class e{constructor(){this.value=`valid`}dirty(){this.value===`valid`&&(this.value=`dirty`)}abort(){this.value!==`aborted`&&(this.value=`aborted`)}static mergeArray(e,t){let n=[];for(let r of t){if(r.status===`aborted`)return Ze;r.status===`dirty`&&e.dirty(),n.push(r.value)}return{status:e.value,value:n}}static async mergeObjectAsync(t,n){let r=[];for(let e of n){let t=await e.key,n=await e.value;r.push({key:t,value:n})}return e.mergeObjectSync(t,r)}static mergeObjectSync(e,t){let n={};for(let r of t){let{key:t,value:i}=r;if(t.status===`aborted`||i.status===`aborted`)return Ze;t.status===`dirty`&&e.dirty(),i.status===`dirty`&&e.dirty(),t.value!==`__proto__`&&(i.value!==void 0||r.alwaysSet)&&(n[t.value]=i.value)}return{status:e.value,value:n}}},Ze=Object.freeze({status:`aborted`}),Qe=e=>({status:`dirty`,value:e}),$e=e=>({status:`valid`,value:e}),et=e=>e.status===`aborted`,tt=e=>e.status===`dirty`,nt=e=>e.status===`valid`,rt=e=>typeof Promise<`u`&&e instanceof Promise;function it(e,t,n,r){if(typeof t==`function`?e!==t||!r:!t.has(e))throw TypeError(`Cannot read private member from an object whose class did not declare it`);return t.get(e)}function at(e,t,n,r,i){if(typeof t==`function`?e!==t||!i:!t.has(e))throw TypeError(`Cannot write private member to an object whose class did not declare it`);return t.set(e,n),n}var U;(function(e){e.errToObj=e=>typeof e==`string`?{message:e}:e||{},e.toString=e=>typeof e==`string`?e:e?.message})(U||={});var ot,st,ct=class{constructor(e,t,n,r){this._cachedPath=[],this.parent=e,this.data=t,this._path=n,this._key=r}get path(){return this._cachedPath.length||(this._key instanceof Array?this._cachedPath.push(...this._path,...this._key):this._cachedPath.push(...this._path,this._key)),this._cachedPath}},lt=(e,t)=>{if(nt(t))return{success:!0,data:t.value};if(!e.common.issues.length)throw Error(`Validation failed but no issues detected.`);return{success:!1,get error(){if(this._error)return this._error;let t=new Ue(e.common.issues);return this._error=t,this._error}}};function ut(e){if(!e)return{};let{errorMap:t,invalid_type_error:n,required_error:r,description:i}=e;if(t&&(n||r))throw Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`);return t?{errorMap:t,description:i}:{errorMap:(t,i)=>{let{message:a}=e;return t.code===`invalid_enum_value`?{message:a??i.defaultError}:i.data===void 0?{message:a??r??i.defaultError}:t.code===`invalid_type`?{message:a??n??i.defaultError}:{message:i.defaultError}},description:i}}var dt=class{constructor(e){this.spa=this.safeParseAsync,this._def=e,this.parse=this.parse.bind(this),this.safeParse=this.safeParse.bind(this),this.parseAsync=this.parseAsync.bind(this),this.safeParseAsync=this.safeParseAsync.bind(this),this.spa=this.spa.bind(this),this.refine=this.refine.bind(this),this.refinement=this.refinement.bind(this),this.superRefine=this.superRefine.bind(this),this.optional=this.optional.bind(this),this.nullable=this.nullable.bind(this),this.nullish=this.nullish.bind(this),this.array=this.array.bind(this),this.promise=this.promise.bind(this),this.or=this.or.bind(this),this.and=this.and.bind(this),this.transform=this.transform.bind(this),this.brand=this.brand.bind(this),this.default=this.default.bind(this),this.catch=this.catch.bind(this),this.describe=this.describe.bind(this),this.pipe=this.pipe.bind(this),this.readonly=this.readonly.bind(this),this.isNullable=this.isNullable.bind(this),this.isOptional=this.isOptional.bind(this)}get description(){return this._def.description}_getType(e){return Ve(e.data)}_getOrReturnCtx(e,t){return t||{common:e.parent.common,data:e.data,parsedType:Ve(e.data),schemaErrorMap:this._def.errorMap,path:e.path,parent:e.parent}}_processInputParams(e){return{status:new Xe,ctx:{common:e.parent.common,data:e.data,parsedType:Ve(e.data),schemaErrorMap:this._def.errorMap,path:e.path,parent:e.parent}}}_parseSync(e){let t=this._parse(e);if(rt(t))throw Error(`Synchronous parse encountered promise.`);return t}_parseAsync(e){let t=this._parse(e);return Promise.resolve(t)}parse(e,t){let n=this.safeParse(e,t);if(n.success)return n.data;throw n.error}safeParse(e,t){let n={common:{issues:[],async:t?.async??!1,contextualErrorMap:t?.errorMap},path:t?.path||[],schemaErrorMap:this._def.errorMap,parent:null,data:e,parsedType:Ve(e)};return lt(n,this._parseSync({data:e,path:n.path,parent:n}))}async parseAsync(e,t){let n=await this.safeParseAsync(e,t);if(n.success)return n.data;throw n.error}async safeParseAsync(e,t){let n={common:{issues:[],contextualErrorMap:t?.errorMap,async:!0},path:t?.path||[],schemaErrorMap:this._def.errorMap,parent:null,data:e,parsedType:Ve(e)},r=this._parse({data:e,path:n.path,parent:n});return lt(n,await(rt(r)?r:Promise.resolve(r)))}refine(e,t){let n=e=>typeof t==`string`||t===void 0?{message:t}:typeof t==`function`?t(e):t;return this._refinement((t,r)=>{let i=e(t),a=()=>r.addIssue({code:V.custom,...n(t)});return typeof Promise<`u`&&i instanceof Promise?i.then(e=>e?!0:(a(),!1)):i?!0:(a(),!1)})}refinement(e,t){return this._refinement((n,r)=>e(n)?!0:(r.addIssue(typeof t==`function`?t(n,r):t),!1))}_refinement(e){return new ln({schema:this,typeName:xn.ZodEffects,effect:{type:`refinement`,refinement:e}})}superRefine(e){return this._refinement(e)}optional(){return un.create(this,this._def)}nullable(){return dn.create(this,this._def)}nullish(){return this.nullable().optional()}array(){return Ut.create(this,this._def)}promise(){return cn.create(this,this._def)}or(e){return Kt.create([this,e],this._def)}and(e){return Xt.create(this,e,this._def)}transform(e){return new ln({...ut(this._def),schema:this,typeName:xn.ZodEffects,effect:{type:`transform`,transform:e}})}default(e){let t=typeof e==`function`?e:()=>e;return new fn({...ut(this._def),innerType:this,defaultValue:t,typeName:xn.ZodDefault})}brand(){return new gn({typeName:xn.ZodBranded,type:this,...ut(this._def)})}catch(e){let t=typeof e==`function`?e:()=>e;return new pn({...ut(this._def),innerType:this,catchValue:t,typeName:xn.ZodCatch})}describe(e){let t=this.constructor;return new t({...this._def,description:e})}pipe(e){return _n.create(this,e)}readonly(){return vn.create(this)}isOptional(){return this.safeParse(void 0).success}isNullable(){return this.safeParse(null).success}},ft=/^c[^\s-]{8,}$/i,pt=/^[0-9a-z]+$/,mt=/^[0-9A-HJKMNP-TV-Z]{26}$/,ht=/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i,gt=/^[a-z0-9_-]{21}$/i,_t=/^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/,vt=/^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i,yt=`^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`,bt,xt=/^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/,St=/^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/,Ct=/^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/,wt=`((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`,Tt=RegExp(`^${wt}$`);function Et(e){let t=`([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d`;return e.precision?t=`${t}\\.\\d{${e.precision}}`:e.precision??(t=`${t}(\\.\\d+)?`),t}function Dt(e){return RegExp(`^${Et(e)}$`)}function Ot(e){let t=`${wt}T${Et(e)}`,n=[];return n.push(e.local?`Z?`:`Z`),e.offset&&n.push(`([+-]\\d{2}:?\\d{2})`),t=`${t}(${n.join(`|`)})`,RegExp(`^${t}$`)}function kt(e,t){return!!((t===`v4`||!t)&&xt.test(e)||(t===`v6`||!t)&&St.test(e))}var At=class e extends dt{_parse(e){if(this._def.coerce&&(e.data=String(e.data)),this._getType(e)!==B.string){let t=this._getOrReturnCtx(e);return H(t,{code:V.invalid_type,expected:B.string,received:t.parsedType}),Ze}let t=new Xe,n;for(let r of this._def.checks)if(r.kind===`min`)e.data.lengthr.value&&(n=this._getOrReturnCtx(e,n),H(n,{code:V.too_big,maximum:r.value,type:`string`,inclusive:!0,exact:!1,message:r.message}),t.dirty());else if(r.kind===`length`){let i=e.data.length>r.value,a=e.data.lengthe.test(t),{validation:t,code:V.invalid_string,...U.errToObj(n)})}_addCheck(t){return new e({...this._def,checks:[...this._def.checks,t]})}email(e){return this._addCheck({kind:`email`,...U.errToObj(e)})}url(e){return this._addCheck({kind:`url`,...U.errToObj(e)})}emoji(e){return this._addCheck({kind:`emoji`,...U.errToObj(e)})}uuid(e){return this._addCheck({kind:`uuid`,...U.errToObj(e)})}nanoid(e){return this._addCheck({kind:`nanoid`,...U.errToObj(e)})}cuid(e){return this._addCheck({kind:`cuid`,...U.errToObj(e)})}cuid2(e){return this._addCheck({kind:`cuid2`,...U.errToObj(e)})}ulid(e){return this._addCheck({kind:`ulid`,...U.errToObj(e)})}base64(e){return this._addCheck({kind:`base64`,...U.errToObj(e)})}ip(e){return this._addCheck({kind:`ip`,...U.errToObj(e)})}datetime(e){return typeof e==`string`?this._addCheck({kind:`datetime`,precision:null,offset:!1,local:!1,message:e}):this._addCheck({kind:`datetime`,precision:e?.precision===void 0?null:e?.precision,offset:e?.offset??!1,local:e?.local??!1,...U.errToObj(e?.message)})}date(e){return this._addCheck({kind:`date`,message:e})}time(e){return typeof e==`string`?this._addCheck({kind:`time`,precision:null,message:e}):this._addCheck({kind:`time`,precision:e?.precision===void 0?null:e?.precision,...U.errToObj(e?.message)})}duration(e){return this._addCheck({kind:`duration`,...U.errToObj(e)})}regex(e,t){return this._addCheck({kind:`regex`,regex:e,...U.errToObj(t)})}includes(e,t){return this._addCheck({kind:`includes`,value:e,position:t?.position,...U.errToObj(t?.message)})}startsWith(e,t){return this._addCheck({kind:`startsWith`,value:e,...U.errToObj(t)})}endsWith(e,t){return this._addCheck({kind:`endsWith`,value:e,...U.errToObj(t)})}min(e,t){return this._addCheck({kind:`min`,value:e,...U.errToObj(t)})}max(e,t){return this._addCheck({kind:`max`,value:e,...U.errToObj(t)})}length(e,t){return this._addCheck({kind:`length`,value:e,...U.errToObj(t)})}nonempty(e){return this.min(1,U.errToObj(e))}trim(){return new e({...this._def,checks:[...this._def.checks,{kind:`trim`}]})}toLowerCase(){return new e({...this._def,checks:[...this._def.checks,{kind:`toLowerCase`}]})}toUpperCase(){return new e({...this._def,checks:[...this._def.checks,{kind:`toUpperCase`}]})}get isDatetime(){return!!this._def.checks.find(e=>e.kind===`datetime`)}get isDate(){return!!this._def.checks.find(e=>e.kind===`date`)}get isTime(){return!!this._def.checks.find(e=>e.kind===`time`)}get isDuration(){return!!this._def.checks.find(e=>e.kind===`duration`)}get isEmail(){return!!this._def.checks.find(e=>e.kind===`email`)}get isURL(){return!!this._def.checks.find(e=>e.kind===`url`)}get isEmoji(){return!!this._def.checks.find(e=>e.kind===`emoji`)}get isUUID(){return!!this._def.checks.find(e=>e.kind===`uuid`)}get isNANOID(){return!!this._def.checks.find(e=>e.kind===`nanoid`)}get isCUID(){return!!this._def.checks.find(e=>e.kind===`cuid`)}get isCUID2(){return!!this._def.checks.find(e=>e.kind===`cuid2`)}get isULID(){return!!this._def.checks.find(e=>e.kind===`ulid`)}get isIP(){return!!this._def.checks.find(e=>e.kind===`ip`)}get isBase64(){return!!this._def.checks.find(e=>e.kind===`base64`)}get minLength(){let e=null;for(let t of this._def.checks)t.kind===`min`&&(e===null||t.value>e)&&(e=t.value);return e}get maxLength(){let e=null;for(let t of this._def.checks)t.kind===`max`&&(e===null||t.valuenew At({checks:[],typeName:xn.ZodString,coerce:e?.coerce??!1,...ut(e)});function jt(e,t){let n=(e.toString().split(`.`)[1]||``).length,r=(t.toString().split(`.`)[1]||``).length,i=n>r?n:r;return parseInt(e.toFixed(i).replace(`.`,``))%parseInt(t.toFixed(i).replace(`.`,``))/10**i}var Mt=class e extends dt{constructor(){super(...arguments),this.min=this.gte,this.max=this.lte,this.step=this.multipleOf}_parse(e){if(this._def.coerce&&(e.data=Number(e.data)),this._getType(e)!==B.number){let t=this._getOrReturnCtx(e);return H(t,{code:V.invalid_type,expected:B.number,received:t.parsedType}),Ze}let t,n=new Xe;for(let r of this._def.checks)r.kind===`int`?ze.isInteger(e.data)||(t=this._getOrReturnCtx(e,t),H(t,{code:V.invalid_type,expected:`integer`,received:`float`,message:r.message}),n.dirty()):r.kind===`min`?(r.inclusive?e.datar.value:e.data>=r.value)&&(t=this._getOrReturnCtx(e,t),H(t,{code:V.too_big,maximum:r.value,type:`number`,inclusive:r.inclusive,exact:!1,message:r.message}),n.dirty()):r.kind===`multipleOf`?jt(e.data,r.value)!==0&&(t=this._getOrReturnCtx(e,t),H(t,{code:V.not_multiple_of,multipleOf:r.value,message:r.message}),n.dirty()):r.kind===`finite`?Number.isFinite(e.data)||(t=this._getOrReturnCtx(e,t),H(t,{code:V.not_finite,message:r.message}),n.dirty()):ze.assertNever(r);return{status:n.value,value:e.data}}gte(e,t){return this.setLimit(`min`,e,!0,U.toString(t))}gt(e,t){return this.setLimit(`min`,e,!1,U.toString(t))}lte(e,t){return this.setLimit(`max`,e,!0,U.toString(t))}lt(e,t){return this.setLimit(`max`,e,!1,U.toString(t))}setLimit(t,n,r,i){return new e({...this._def,checks:[...this._def.checks,{kind:t,value:n,inclusive:r,message:U.toString(i)}]})}_addCheck(t){return new e({...this._def,checks:[...this._def.checks,t]})}int(e){return this._addCheck({kind:`int`,message:U.toString(e)})}positive(e){return this._addCheck({kind:`min`,value:0,inclusive:!1,message:U.toString(e)})}negative(e){return this._addCheck({kind:`max`,value:0,inclusive:!1,message:U.toString(e)})}nonpositive(e){return this._addCheck({kind:`max`,value:0,inclusive:!0,message:U.toString(e)})}nonnegative(e){return this._addCheck({kind:`min`,value:0,inclusive:!0,message:U.toString(e)})}multipleOf(e,t){return this._addCheck({kind:`multipleOf`,value:e,message:U.toString(t)})}finite(e){return this._addCheck({kind:`finite`,message:U.toString(e)})}safe(e){return this._addCheck({kind:`min`,inclusive:!0,value:-(2**53-1),message:U.toString(e)})._addCheck({kind:`max`,inclusive:!0,value:2**53-1,message:U.toString(e)})}get minValue(){let e=null;for(let t of this._def.checks)t.kind===`min`&&(e===null||t.value>e)&&(e=t.value);return e}get maxValue(){let e=null;for(let t of this._def.checks)t.kind===`max`&&(e===null||t.valuee.kind===`int`||e.kind===`multipleOf`&&ze.isInteger(e.value))}get isFinite(){let e=null,t=null;for(let n of this._def.checks)if(n.kind===`finite`||n.kind===`int`||n.kind===`multipleOf`)return!0;else n.kind===`min`?(t===null||n.value>t)&&(t=n.value):n.kind===`max`&&(e===null||n.valuenew Mt({checks:[],typeName:xn.ZodNumber,coerce:e?.coerce||!1,...ut(e)});var Nt=class e extends dt{constructor(){super(...arguments),this.min=this.gte,this.max=this.lte}_parse(e){if(this._def.coerce&&(e.data=BigInt(e.data)),this._getType(e)!==B.bigint){let t=this._getOrReturnCtx(e);return H(t,{code:V.invalid_type,expected:B.bigint,received:t.parsedType}),Ze}let t,n=new Xe;for(let r of this._def.checks)r.kind===`min`?(r.inclusive?e.datar.value:e.data>=r.value)&&(t=this._getOrReturnCtx(e,t),H(t,{code:V.too_big,type:`bigint`,maximum:r.value,inclusive:r.inclusive,message:r.message}),n.dirty()):r.kind===`multipleOf`?e.data%r.value!==BigInt(0)&&(t=this._getOrReturnCtx(e,t),H(t,{code:V.not_multiple_of,multipleOf:r.value,message:r.message}),n.dirty()):ze.assertNever(r);return{status:n.value,value:e.data}}gte(e,t){return this.setLimit(`min`,e,!0,U.toString(t))}gt(e,t){return this.setLimit(`min`,e,!1,U.toString(t))}lte(e,t){return this.setLimit(`max`,e,!0,U.toString(t))}lt(e,t){return this.setLimit(`max`,e,!1,U.toString(t))}setLimit(t,n,r,i){return new e({...this._def,checks:[...this._def.checks,{kind:t,value:n,inclusive:r,message:U.toString(i)}]})}_addCheck(t){return new e({...this._def,checks:[...this._def.checks,t]})}positive(e){return this._addCheck({kind:`min`,value:BigInt(0),inclusive:!1,message:U.toString(e)})}negative(e){return this._addCheck({kind:`max`,value:BigInt(0),inclusive:!1,message:U.toString(e)})}nonpositive(e){return this._addCheck({kind:`max`,value:BigInt(0),inclusive:!0,message:U.toString(e)})}nonnegative(e){return this._addCheck({kind:`min`,value:BigInt(0),inclusive:!0,message:U.toString(e)})}multipleOf(e,t){return this._addCheck({kind:`multipleOf`,value:e,message:U.toString(t)})}get minValue(){let e=null;for(let t of this._def.checks)t.kind===`min`&&(e===null||t.value>e)&&(e=t.value);return e}get maxValue(){let e=null;for(let t of this._def.checks)t.kind===`max`&&(e===null||t.valuenew Nt({checks:[],typeName:xn.ZodBigInt,coerce:e?.coerce??!1,...ut(e)});var Pt=class extends dt{_parse(e){if(this._def.coerce&&(e.data=!!e.data),this._getType(e)!==B.boolean){let t=this._getOrReturnCtx(e);return H(t,{code:V.invalid_type,expected:B.boolean,received:t.parsedType}),Ze}return $e(e.data)}};Pt.create=e=>new Pt({typeName:xn.ZodBoolean,coerce:e?.coerce||!1,...ut(e)});var Ft=class e extends dt{_parse(e){if(this._def.coerce&&(e.data=new Date(e.data)),this._getType(e)!==B.date){let t=this._getOrReturnCtx(e);return H(t,{code:V.invalid_type,expected:B.date,received:t.parsedType}),Ze}if(isNaN(e.data.getTime()))return H(this._getOrReturnCtx(e),{code:V.invalid_date}),Ze;let t=new Xe,n;for(let r of this._def.checks)r.kind===`min`?e.data.getTime()r.value&&(n=this._getOrReturnCtx(e,n),H(n,{code:V.too_big,message:r.message,inclusive:!0,exact:!1,maximum:r.value,type:`date`}),t.dirty()):ze.assertNever(r);return{status:t.value,value:new Date(e.data.getTime())}}_addCheck(t){return new e({...this._def,checks:[...this._def.checks,t]})}min(e,t){return this._addCheck({kind:`min`,value:e.getTime(),message:U.toString(t)})}max(e,t){return this._addCheck({kind:`max`,value:e.getTime(),message:U.toString(t)})}get minDate(){let e=null;for(let t of this._def.checks)t.kind===`min`&&(e===null||t.value>e)&&(e=t.value);return e==null?null:new Date(e)}get maxDate(){let e=null;for(let t of this._def.checks)t.kind===`max`&&(e===null||t.valuenew Ft({checks:[],coerce:e?.coerce||!1,typeName:xn.ZodDate,...ut(e)});var It=class extends dt{_parse(e){if(this._getType(e)!==B.symbol){let t=this._getOrReturnCtx(e);return H(t,{code:V.invalid_type,expected:B.symbol,received:t.parsedType}),Ze}return $e(e.data)}};It.create=e=>new It({typeName:xn.ZodSymbol,...ut(e)});var Lt=class extends dt{_parse(e){if(this._getType(e)!==B.undefined){let t=this._getOrReturnCtx(e);return H(t,{code:V.invalid_type,expected:B.undefined,received:t.parsedType}),Ze}return $e(e.data)}};Lt.create=e=>new Lt({typeName:xn.ZodUndefined,...ut(e)});var Rt=class extends dt{_parse(e){if(this._getType(e)!==B.null){let t=this._getOrReturnCtx(e);return H(t,{code:V.invalid_type,expected:B.null,received:t.parsedType}),Ze}return $e(e.data)}};Rt.create=e=>new Rt({typeName:xn.ZodNull,...ut(e)});var zt=class extends dt{constructor(){super(...arguments),this._any=!0}_parse(e){return $e(e.data)}};zt.create=e=>new zt({typeName:xn.ZodAny,...ut(e)});var Bt=class extends dt{constructor(){super(...arguments),this._unknown=!0}_parse(e){return $e(e.data)}};Bt.create=e=>new Bt({typeName:xn.ZodUnknown,...ut(e)});var Vt=class extends dt{_parse(e){let t=this._getOrReturnCtx(e);return H(t,{code:V.invalid_type,expected:B.never,received:t.parsedType}),Ze}};Vt.create=e=>new Vt({typeName:xn.ZodNever,...ut(e)});var Ht=class extends dt{_parse(e){if(this._getType(e)!==B.undefined){let t=this._getOrReturnCtx(e);return H(t,{code:V.invalid_type,expected:B.void,received:t.parsedType}),Ze}return $e(e.data)}};Ht.create=e=>new Ht({typeName:xn.ZodVoid,...ut(e)});var Ut=class e extends dt{_parse(e){let{ctx:t,status:n}=this._processInputParams(e),r=this._def;if(t.parsedType!==B.array)return H(t,{code:V.invalid_type,expected:B.array,received:t.parsedType}),Ze;if(r.exactLength!==null){let e=t.data.length>r.exactLength.value,i=t.data.lengthr.maxLength.value&&(H(t,{code:V.too_big,maximum:r.maxLength.value,type:`array`,inclusive:!0,exact:!1,message:r.maxLength.message}),n.dirty()),t.common.async)return Promise.all([...t.data].map((e,n)=>r.type._parseAsync(new ct(t,e,t.path,n)))).then(e=>Xe.mergeArray(n,e));let i=[...t.data].map((e,n)=>r.type._parseSync(new ct(t,e,t.path,n)));return Xe.mergeArray(n,i)}get element(){return this._def.type}min(t,n){return new e({...this._def,minLength:{value:t,message:U.toString(n)}})}max(t,n){return new e({...this._def,maxLength:{value:t,message:U.toString(n)}})}length(t,n){return new e({...this._def,exactLength:{value:t,message:U.toString(n)}})}nonempty(e){return this.min(1,e)}};Ut.create=(e,t)=>new Ut({type:e,minLength:null,maxLength:null,exactLength:null,typeName:xn.ZodArray,...ut(t)});function Wt(e){if(e instanceof Gt){let t={};for(let n in e.shape){let r=e.shape[n];t[n]=un.create(Wt(r))}return new Gt({...e._def,shape:()=>t})}else if(e instanceof Ut)return new Ut({...e._def,type:Wt(e.element)});else if(e instanceof un)return un.create(Wt(e.unwrap()));else if(e instanceof dn)return dn.create(Wt(e.unwrap()));else if(e instanceof Zt)return Zt.create(e.items.map(e=>Wt(e)));else return e}var Gt=class e extends dt{constructor(){super(...arguments),this._cached=null,this.nonstrict=this.passthrough,this.augment=this.extend}_getCached(){if(this._cached!==null)return this._cached;let e=this._def.shape(),t=ze.objectKeys(e);return this._cached={shape:e,keys:t}}_parse(e){if(this._getType(e)!==B.object){let t=this._getOrReturnCtx(e);return H(t,{code:V.invalid_type,expected:B.object,received:t.parsedType}),Ze}let{status:t,ctx:n}=this._processInputParams(e),{shape:r,keys:i}=this._getCached(),a=[];if(!(this._def.catchall instanceof Vt&&this._def.unknownKeys===`strip`))for(let e in n.data)i.includes(e)||a.push(e);let o=[];for(let e of i){let t=r[e],i=n.data[e];o.push({key:{status:`valid`,value:e},value:t._parse(new ct(n,i,n.path,e)),alwaysSet:e in n.data})}if(this._def.catchall instanceof Vt){let e=this._def.unknownKeys;if(e===`passthrough`)for(let e of a)o.push({key:{status:`valid`,value:e},value:{status:`valid`,value:n.data[e]}});else if(e===`strict`)a.length>0&&(H(n,{code:V.unrecognized_keys,keys:a}),t.dirty());else if(e!==`strip`)throw Error(`Internal ZodObject error: invalid unknownKeys value.`)}else{let e=this._def.catchall;for(let t of a){let r=n.data[t];o.push({key:{status:`valid`,value:t},value:e._parse(new ct(n,r,n.path,t)),alwaysSet:t in n.data})}}return n.common.async?Promise.resolve().then(async()=>{let e=[];for(let t of o){let n=await t.key,r=await t.value;e.push({key:n,value:r,alwaysSet:t.alwaysSet})}return e}).then(e=>Xe.mergeObjectSync(t,e)):Xe.mergeObjectSync(t,o)}get shape(){return this._def.shape()}strict(t){return U.errToObj,new e({...this._def,unknownKeys:`strict`,...t===void 0?{}:{errorMap:(e,n)=>{var r;let i=(r=this._def).errorMap?.call(r,e,n).message??n.defaultError;return e.code===`unrecognized_keys`?{message:U.errToObj(t).message??i}:{message:i}}}})}strip(){return new e({...this._def,unknownKeys:`strip`})}passthrough(){return new e({...this._def,unknownKeys:`passthrough`})}extend(t){return new e({...this._def,shape:()=>({...this._def.shape(),...t})})}merge(t){return new e({unknownKeys:t._def.unknownKeys,catchall:t._def.catchall,shape:()=>({...this._def.shape(),...t._def.shape()}),typeName:xn.ZodObject})}setKey(e,t){return this.augment({[e]:t})}catchall(t){return new e({...this._def,catchall:t})}pick(t){let n={};return ze.objectKeys(t).forEach(e=>{t[e]&&this.shape[e]&&(n[e]=this.shape[e])}),new e({...this._def,shape:()=>n})}omit(t){let n={};return ze.objectKeys(this.shape).forEach(e=>{t[e]||(n[e]=this.shape[e])}),new e({...this._def,shape:()=>n})}deepPartial(){return Wt(this)}partial(t){let n={};return ze.objectKeys(this.shape).forEach(e=>{let r=this.shape[e];t&&!t[e]?n[e]=r:n[e]=r.optional()}),new e({...this._def,shape:()=>n})}required(t){let n={};return ze.objectKeys(this.shape).forEach(e=>{if(t&&!t[e])n[e]=this.shape[e];else{let t=this.shape[e];for(;t instanceof un;)t=t._def.innerType;n[e]=t}}),new e({...this._def,shape:()=>n})}keyof(){return an(ze.objectKeys(this.shape))}};Gt.create=(e,t)=>new Gt({shape:()=>e,unknownKeys:`strip`,catchall:Vt.create(),typeName:xn.ZodObject,...ut(t)}),Gt.strictCreate=(e,t)=>new Gt({shape:()=>e,unknownKeys:`strict`,catchall:Vt.create(),typeName:xn.ZodObject,...ut(t)}),Gt.lazycreate=(e,t)=>new Gt({shape:e,unknownKeys:`strip`,catchall:Vt.create(),typeName:xn.ZodObject,...ut(t)});var Kt=class extends dt{_parse(e){let{ctx:t}=this._processInputParams(e),n=this._def.options;function r(e){for(let t of e)if(t.result.status===`valid`)return t.result;for(let n of e)if(n.result.status===`dirty`)return t.common.issues.push(...n.ctx.common.issues),n.result;let n=e.map(e=>new Ue(e.ctx.common.issues));return H(t,{code:V.invalid_union,unionErrors:n}),Ze}if(t.common.async)return Promise.all(n.map(async e=>{let n={...t,common:{...t.common,issues:[]},parent:null};return{result:await e._parseAsync({data:t.data,path:t.path,parent:n}),ctx:n}})).then(r);{let e,r=[];for(let i of n){let n={...t,common:{...t.common,issues:[]},parent:null},a=i._parseSync({data:t.data,path:t.path,parent:n});if(a.status===`valid`)return a;a.status===`dirty`&&!e&&(e={result:a,ctx:n}),n.common.issues.length&&r.push(n.common.issues)}if(e)return t.common.issues.push(...e.ctx.common.issues),e.result;let i=r.map(e=>new Ue(e));return H(t,{code:V.invalid_union,unionErrors:i}),Ze}}get options(){return this._def.options}};Kt.create=(e,t)=>new Kt({options:e,typeName:xn.ZodUnion,...ut(t)});var qt=e=>e instanceof nn?qt(e.schema):e instanceof ln?qt(e.innerType()):e instanceof rn?[e.value]:e instanceof on?e.options:e instanceof sn?ze.objectValues(e.enum):e instanceof fn?qt(e._def.innerType):e instanceof Lt?[void 0]:e instanceof Rt?[null]:e instanceof un?[void 0,...qt(e.unwrap())]:e instanceof dn?[null,...qt(e.unwrap())]:e instanceof gn||e instanceof vn?qt(e.unwrap()):e instanceof pn?qt(e._def.innerType):[],Jt=class e extends dt{_parse(e){let{ctx:t}=this._processInputParams(e);if(t.parsedType!==B.object)return H(t,{code:V.invalid_type,expected:B.object,received:t.parsedType}),Ze;let n=this.discriminator,r=t.data[n],i=this.optionsMap.get(r);return i?t.common.async?i._parseAsync({data:t.data,path:t.path,parent:t}):i._parseSync({data:t.data,path:t.path,parent:t}):(H(t,{code:V.invalid_union_discriminator,options:Array.from(this.optionsMap.keys()),path:[n]}),Ze)}get discriminator(){return this._def.discriminator}get options(){return this._def.options}get optionsMap(){return this._def.optionsMap}static create(t,n,r){let i=new Map;for(let e of n){let n=qt(e.shape[t]);if(!n.length)throw Error(`A discriminator value for key \`${t}\` could not be extracted from all schema options`);for(let r of n){if(i.has(r))throw Error(`Discriminator property ${String(t)} has duplicate value ${String(r)}`);i.set(r,e)}}return new e({typeName:xn.ZodDiscriminatedUnion,discriminator:t,options:n,optionsMap:i,...ut(r)})}};function Yt(e,t){let n=Ve(e),r=Ve(t);if(e===t)return{valid:!0,data:e};if(n===B.object&&r===B.object){let n=ze.objectKeys(t),r=ze.objectKeys(e).filter(e=>n.indexOf(e)!==-1),i={...e,...t};for(let n of r){let r=Yt(e[n],t[n]);if(!r.valid)return{valid:!1};i[n]=r.data}return{valid:!0,data:i}}else if(n===B.array&&r===B.array){if(e.length!==t.length)return{valid:!1};let n=[];for(let r=0;r{if(et(e)||et(r))return Ze;let i=Yt(e.value,r.value);return i.valid?((tt(e)||tt(r))&&t.dirty(),{status:t.value,value:i.data}):(H(n,{code:V.invalid_intersection_types}),Ze)};return n.common.async?Promise.all([this._def.left._parseAsync({data:n.data,path:n.path,parent:n}),this._def.right._parseAsync({data:n.data,path:n.path,parent:n})]).then(([e,t])=>r(e,t)):r(this._def.left._parseSync({data:n.data,path:n.path,parent:n}),this._def.right._parseSync({data:n.data,path:n.path,parent:n}))}};Xt.create=(e,t,n)=>new Xt({left:e,right:t,typeName:xn.ZodIntersection,...ut(n)});var Zt=class e extends dt{_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==B.array)return H(n,{code:V.invalid_type,expected:B.array,received:n.parsedType}),Ze;if(n.data.lengththis._def.items.length&&(H(n,{code:V.too_big,maximum:this._def.items.length,inclusive:!0,exact:!1,type:`array`}),t.dirty());let r=[...n.data].map((e,t)=>{let r=this._def.items[t]||this._def.rest;return r?r._parse(new ct(n,e,n.path,t)):null}).filter(e=>!!e);return n.common.async?Promise.all(r).then(e=>Xe.mergeArray(t,e)):Xe.mergeArray(t,r)}get items(){return this._def.items}rest(t){return new e({...this._def,rest:t})}};Zt.create=(e,t)=>{if(!Array.isArray(e))throw Error(`You must pass an array of schemas to z.tuple([ ... ])`);return new Zt({items:e,typeName:xn.ZodTuple,rest:null,...ut(t)})};var Qt=class e extends dt{get keySchema(){return this._def.keyType}get valueSchema(){return this._def.valueType}_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==B.object)return H(n,{code:V.invalid_type,expected:B.object,received:n.parsedType}),Ze;let r=[],i=this._def.keyType,a=this._def.valueType;for(let e in n.data)r.push({key:i._parse(new ct(n,e,n.path,e)),value:a._parse(new ct(n,n.data[e],n.path,e)),alwaysSet:e in n.data});return n.common.async?Xe.mergeObjectAsync(t,r):Xe.mergeObjectSync(t,r)}get element(){return this._def.valueType}static create(t,n,r){return n instanceof dt?new e({keyType:t,valueType:n,typeName:xn.ZodRecord,...ut(r)}):new e({keyType:At.create(),valueType:t,typeName:xn.ZodRecord,...ut(n)})}},$t=class extends dt{get keySchema(){return this._def.keyType}get valueSchema(){return this._def.valueType}_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==B.map)return H(n,{code:V.invalid_type,expected:B.map,received:n.parsedType}),Ze;let r=this._def.keyType,i=this._def.valueType,a=[...n.data.entries()].map(([e,t],a)=>({key:r._parse(new ct(n,e,n.path,[a,`key`])),value:i._parse(new ct(n,t,n.path,[a,`value`]))}));if(n.common.async){let e=new Map;return Promise.resolve().then(async()=>{for(let n of a){let r=await n.key,i=await n.value;if(r.status===`aborted`||i.status===`aborted`)return Ze;(r.status===`dirty`||i.status===`dirty`)&&t.dirty(),e.set(r.value,i.value)}return{status:t.value,value:e}})}else{let e=new Map;for(let n of a){let r=n.key,i=n.value;if(r.status===`aborted`||i.status===`aborted`)return Ze;(r.status===`dirty`||i.status===`dirty`)&&t.dirty(),e.set(r.value,i.value)}return{status:t.value,value:e}}}};$t.create=(e,t,n)=>new $t({valueType:t,keyType:e,typeName:xn.ZodMap,...ut(n)});var en=class e extends dt{_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==B.set)return H(n,{code:V.invalid_type,expected:B.set,received:n.parsedType}),Ze;let r=this._def;r.minSize!==null&&n.data.sizer.maxSize.value&&(H(n,{code:V.too_big,maximum:r.maxSize.value,type:`set`,inclusive:!0,exact:!1,message:r.maxSize.message}),t.dirty());let i=this._def.valueType;function a(e){let n=new Set;for(let r of e){if(r.status===`aborted`)return Ze;r.status===`dirty`&&t.dirty(),n.add(r.value)}return{status:t.value,value:n}}let o=[...n.data.values()].map((e,t)=>i._parse(new ct(n,e,n.path,t)));return n.common.async?Promise.all(o).then(e=>a(e)):a(o)}min(t,n){return new e({...this._def,minSize:{value:t,message:U.toString(n)}})}max(t,n){return new e({...this._def,maxSize:{value:t,message:U.toString(n)}})}size(e,t){return this.min(e,t).max(e,t)}nonempty(e){return this.min(1,e)}};en.create=(e,t)=>new en({valueType:e,minSize:null,maxSize:null,typeName:xn.ZodSet,...ut(t)});var tn=class e extends dt{constructor(){super(...arguments),this.validate=this.implement}_parse(e){let{ctx:t}=this._processInputParams(e);if(t.parsedType!==B.function)return H(t,{code:V.invalid_type,expected:B.function,received:t.parsedType}),Ze;function n(e,n){return Je({data:e,path:t.path,errorMaps:[t.common.contextualErrorMap,t.schemaErrorMap,qe(),We].filter(e=>!!e),issueData:{code:V.invalid_arguments,argumentsError:n}})}function r(e,n){return Je({data:e,path:t.path,errorMaps:[t.common.contextualErrorMap,t.schemaErrorMap,qe(),We].filter(e=>!!e),issueData:{code:V.invalid_return_type,returnTypeError:n}})}let i={errorMap:t.common.contextualErrorMap},a=t.data;if(this._def.returns instanceof cn){let e=this;return $e(async function(...t){let o=new Ue([]),s=await e._def.args.parseAsync(t,i).catch(e=>{throw o.addIssue(n(t,e)),o}),c=await Reflect.apply(a,this,s);return await e._def.returns._def.type.parseAsync(c,i).catch(e=>{throw o.addIssue(r(c,e)),o})})}else{let e=this;return $e(function(...t){let o=e._def.args.safeParse(t,i);if(!o.success)throw new Ue([n(t,o.error)]);let s=Reflect.apply(a,this,o.data),c=e._def.returns.safeParse(s,i);if(!c.success)throw new Ue([r(s,c.error)]);return c.data})}}parameters(){return this._def.args}returnType(){return this._def.returns}args(...t){return new e({...this._def,args:Zt.create(t).rest(Bt.create())})}returns(t){return new e({...this._def,returns:t})}implement(e){return this.parse(e)}strictImplement(e){return this.parse(e)}static create(t,n,r){return new e({args:t||Zt.create([]).rest(Bt.create()),returns:n||Bt.create(),typeName:xn.ZodFunction,...ut(r)})}},nn=class extends dt{get schema(){return this._def.getter()}_parse(e){let{ctx:t}=this._processInputParams(e);return this._def.getter()._parse({data:t.data,path:t.path,parent:t})}};nn.create=(e,t)=>new nn({getter:e,typeName:xn.ZodLazy,...ut(t)});var rn=class extends dt{_parse(e){if(e.data!==this._def.value){let t=this._getOrReturnCtx(e);return H(t,{received:t.data,code:V.invalid_literal,expected:this._def.value}),Ze}return{status:`valid`,value:e.data}}get value(){return this._def.value}};rn.create=(e,t)=>new rn({value:e,typeName:xn.ZodLiteral,...ut(t)});function an(e,t){return new on({values:e,typeName:xn.ZodEnum,...ut(t)})}var on=class e extends dt{constructor(){super(...arguments),ot.set(this,void 0)}_parse(e){if(typeof e.data!=`string`){let t=this._getOrReturnCtx(e),n=this._def.values;return H(t,{expected:ze.joinValues(n),received:t.parsedType,code:V.invalid_type}),Ze}if(it(this,ot)||at(this,ot,new Set(this._def.values)),!it(this,ot).has(e.data)){let t=this._getOrReturnCtx(e),n=this._def.values;return H(t,{received:t.data,code:V.invalid_enum_value,options:n}),Ze}return $e(e.data)}get options(){return this._def.values}get enum(){let e={};for(let t of this._def.values)e[t]=t;return e}get Values(){let e={};for(let t of this._def.values)e[t]=t;return e}get Enum(){let e={};for(let t of this._def.values)e[t]=t;return e}extract(t,n=this._def){return e.create(t,{...this._def,...n})}exclude(t,n=this._def){return e.create(this.options.filter(e=>!t.includes(e)),{...this._def,...n})}};ot=new WeakMap,on.create=an;var sn=class extends dt{constructor(){super(...arguments),st.set(this,void 0)}_parse(e){let t=ze.getValidEnumValues(this._def.values),n=this._getOrReturnCtx(e);if(n.parsedType!==B.string&&n.parsedType!==B.number){let e=ze.objectValues(t);return H(n,{expected:ze.joinValues(e),received:n.parsedType,code:V.invalid_type}),Ze}if(it(this,st)||at(this,st,new Set(ze.getValidEnumValues(this._def.values))),!it(this,st).has(e.data)){let e=ze.objectValues(t);return H(n,{received:n.data,code:V.invalid_enum_value,options:e}),Ze}return $e(e.data)}get enum(){return this._def.values}};st=new WeakMap,sn.create=(e,t)=>new sn({values:e,typeName:xn.ZodNativeEnum,...ut(t)});var cn=class extends dt{unwrap(){return this._def.type}_parse(e){let{ctx:t}=this._processInputParams(e);return t.parsedType!==B.promise&&t.common.async===!1?(H(t,{code:V.invalid_type,expected:B.promise,received:t.parsedType}),Ze):$e((t.parsedType===B.promise?t.data:Promise.resolve(t.data)).then(e=>this._def.type.parseAsync(e,{path:t.path,errorMap:t.common.contextualErrorMap})))}};cn.create=(e,t)=>new cn({type:e,typeName:xn.ZodPromise,...ut(t)});var ln=class extends dt{innerType(){return this._def.schema}sourceType(){return this._def.schema._def.typeName===xn.ZodEffects?this._def.schema.sourceType():this._def.schema}_parse(e){let{status:t,ctx:n}=this._processInputParams(e),r=this._def.effect||null,i={addIssue:e=>{H(n,e),e.fatal?t.abort():t.dirty()},get path(){return n.path}};if(i.addIssue=i.addIssue.bind(i),r.type===`preprocess`){let e=r.transform(n.data,i);if(n.common.async)return Promise.resolve(e).then(async e=>{if(t.value===`aborted`)return Ze;let r=await this._def.schema._parseAsync({data:e,path:n.path,parent:n});return r.status===`aborted`?Ze:r.status===`dirty`||t.value===`dirty`?Qe(r.value):r});{if(t.value===`aborted`)return Ze;let r=this._def.schema._parseSync({data:e,path:n.path,parent:n});return r.status===`aborted`?Ze:r.status===`dirty`||t.value===`dirty`?Qe(r.value):r}}if(r.type===`refinement`){let e=e=>{let t=r.refinement(e,i);if(n.common.async)return Promise.resolve(t);if(t instanceof Promise)throw Error(`Async refinement encountered during synchronous parse operation. Use .parseAsync instead.`);return e};if(n.common.async===!1){let r=this._def.schema._parseSync({data:n.data,path:n.path,parent:n});return r.status===`aborted`?Ze:(r.status===`dirty`&&t.dirty(),e(r.value),{status:t.value,value:r.value})}else return this._def.schema._parseAsync({data:n.data,path:n.path,parent:n}).then(n=>n.status===`aborted`?Ze:(n.status===`dirty`&&t.dirty(),e(n.value).then(()=>({status:t.value,value:n.value}))))}if(r.type===`transform`)if(n.common.async===!1){let e=this._def.schema._parseSync({data:n.data,path:n.path,parent:n});if(!nt(e))return e;let a=r.transform(e.value,i);if(a instanceof Promise)throw Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`);return{status:t.value,value:a}}else return this._def.schema._parseAsync({data:n.data,path:n.path,parent:n}).then(e=>nt(e)?Promise.resolve(r.transform(e.value,i)).then(e=>({status:t.value,value:e})):e);ze.assertNever(r)}};ln.create=(e,t,n)=>new ln({schema:e,typeName:xn.ZodEffects,effect:t,...ut(n)}),ln.createWithPreprocess=(e,t,n)=>new ln({schema:t,effect:{type:`preprocess`,transform:e},typeName:xn.ZodEffects,...ut(n)});var un=class extends dt{_parse(e){return this._getType(e)===B.undefined?$e(void 0):this._def.innerType._parse(e)}unwrap(){return this._def.innerType}};un.create=(e,t)=>new un({innerType:e,typeName:xn.ZodOptional,...ut(t)});var dn=class extends dt{_parse(e){return this._getType(e)===B.null?$e(null):this._def.innerType._parse(e)}unwrap(){return this._def.innerType}};dn.create=(e,t)=>new dn({innerType:e,typeName:xn.ZodNullable,...ut(t)});var fn=class extends dt{_parse(e){let{ctx:t}=this._processInputParams(e),n=t.data;return t.parsedType===B.undefined&&(n=this._def.defaultValue()),this._def.innerType._parse({data:n,path:t.path,parent:t})}removeDefault(){return this._def.innerType}};fn.create=(e,t)=>new fn({innerType:e,typeName:xn.ZodDefault,defaultValue:typeof t.default==`function`?t.default:()=>t.default,...ut(t)});var pn=class extends dt{_parse(e){let{ctx:t}=this._processInputParams(e),n={...t,common:{...t.common,issues:[]}},r=this._def.innerType._parse({data:n.data,path:n.path,parent:{...n}});return rt(r)?r.then(e=>({status:`valid`,value:e.status===`valid`?e.value:this._def.catchValue({get error(){return new Ue(n.common.issues)},input:n.data})})):{status:`valid`,value:r.status===`valid`?r.value:this._def.catchValue({get error(){return new Ue(n.common.issues)},input:n.data})}}removeCatch(){return this._def.innerType}};pn.create=(e,t)=>new pn({innerType:e,typeName:xn.ZodCatch,catchValue:typeof t.catch==`function`?t.catch:()=>t.catch,...ut(t)});var mn=class extends dt{_parse(e){if(this._getType(e)!==B.nan){let t=this._getOrReturnCtx(e);return H(t,{code:V.invalid_type,expected:B.nan,received:t.parsedType}),Ze}return{status:`valid`,value:e.data}}};mn.create=e=>new mn({typeName:xn.ZodNaN,...ut(e)});var hn=Symbol(`zod_brand`),gn=class extends dt{_parse(e){let{ctx:t}=this._processInputParams(e),n=t.data;return this._def.type._parse({data:n,path:t.path,parent:t})}unwrap(){return this._def.type}},_n=class e extends dt{_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.common.async)return(async()=>{let e=await this._def.in._parseAsync({data:n.data,path:n.path,parent:n});return e.status===`aborted`?Ze:e.status===`dirty`?(t.dirty(),Qe(e.value)):this._def.out._parseAsync({data:e.value,path:n.path,parent:n})})();{let e=this._def.in._parseSync({data:n.data,path:n.path,parent:n});return e.status===`aborted`?Ze:e.status===`dirty`?(t.dirty(),{status:`dirty`,value:e.value}):this._def.out._parseSync({data:e.value,path:n.path,parent:n})}}static create(t,n){return new e({in:t,out:n,typeName:xn.ZodPipeline})}},vn=class extends dt{_parse(e){let t=this._def.innerType._parse(e),n=e=>(nt(e)&&(e.value=Object.freeze(e.value)),e);return rt(t)?t.then(e=>n(e)):n(t)}unwrap(){return this._def.innerType}};vn.create=(e,t)=>new vn({innerType:e,typeName:xn.ZodReadonly,...ut(t)});function yn(e,t={},n){return e?zt.create().superRefine((r,i)=>{if(!e(r)){let e=typeof t==`function`?t(r):typeof t==`string`?{message:t}:t,a=e.fatal??n??!0,o=typeof e==`string`?{message:e}:e;i.addIssue({code:`custom`,...o,fatal:a})}}):zt.create()}var bn={object:Gt.lazycreate},xn;(function(e){e.ZodString=`ZodString`,e.ZodNumber=`ZodNumber`,e.ZodNaN=`ZodNaN`,e.ZodBigInt=`ZodBigInt`,e.ZodBoolean=`ZodBoolean`,e.ZodDate=`ZodDate`,e.ZodSymbol=`ZodSymbol`,e.ZodUndefined=`ZodUndefined`,e.ZodNull=`ZodNull`,e.ZodAny=`ZodAny`,e.ZodUnknown=`ZodUnknown`,e.ZodNever=`ZodNever`,e.ZodVoid=`ZodVoid`,e.ZodArray=`ZodArray`,e.ZodObject=`ZodObject`,e.ZodUnion=`ZodUnion`,e.ZodDiscriminatedUnion=`ZodDiscriminatedUnion`,e.ZodIntersection=`ZodIntersection`,e.ZodTuple=`ZodTuple`,e.ZodRecord=`ZodRecord`,e.ZodMap=`ZodMap`,e.ZodSet=`ZodSet`,e.ZodFunction=`ZodFunction`,e.ZodLazy=`ZodLazy`,e.ZodLiteral=`ZodLiteral`,e.ZodEnum=`ZodEnum`,e.ZodEffects=`ZodEffects`,e.ZodNativeEnum=`ZodNativeEnum`,e.ZodOptional=`ZodOptional`,e.ZodNullable=`ZodNullable`,e.ZodDefault=`ZodDefault`,e.ZodCatch=`ZodCatch`,e.ZodPromise=`ZodPromise`,e.ZodBranded=`ZodBranded`,e.ZodPipeline=`ZodPipeline`,e.ZodReadonly=`ZodReadonly`})(xn||={});var Sn=(e,t={message:`Input not instance of ${e.name}`})=>yn(t=>t instanceof e,t),W=At.create,Cn=Mt.create,wn=mn.create,Tn=Nt.create,En=Pt.create,Dn=Ft.create,On=It.create,kn=Lt.create,An=Rt.create,jn=zt.create,Mn=Bt.create,Nn=Vt.create,Pn=Ht.create,Fn=Ut.create,G=Gt.create,In=Gt.strictCreate,Ln=Kt.create,Rn=Jt.create,zn=Xt.create,Bn=Zt.create,Vn=Qt.create,Hn=$t.create,Un=en.create,Wn=tn.create,Gn=nn.create,Kn=rn.create,qn=on.create,Jn=sn.create,Yn=cn.create,Xn=ln.create,Zn=un.create,Qn=dn.create,$n=ln.createWithPreprocess,er=_n.create,K=Object.freeze({__proto__:null,defaultErrorMap:We,setErrorMap:Ke,getErrorMap:qe,makeIssue:Je,EMPTY_PATH:Ye,addIssueToContext:H,ParseStatus:Xe,INVALID:Ze,DIRTY:Qe,OK:$e,isAborted:et,isDirty:tt,isValid:nt,isAsync:rt,get util(){return ze},get objectUtil(){return Be},ZodParsedType:B,getParsedType:Ve,ZodType:dt,datetimeRegex:Ot,ZodString:At,ZodNumber:Mt,ZodBigInt:Nt,ZodBoolean:Pt,ZodDate:Ft,ZodSymbol:It,ZodUndefined:Lt,ZodNull:Rt,ZodAny:zt,ZodUnknown:Bt,ZodNever:Vt,ZodVoid:Ht,ZodArray:Ut,ZodObject:Gt,ZodUnion:Kt,ZodDiscriminatedUnion:Jt,ZodIntersection:Xt,ZodTuple:Zt,ZodRecord:Qt,ZodMap:$t,ZodSet:en,ZodFunction:tn,ZodLazy:nn,ZodLiteral:rn,ZodEnum:on,ZodNativeEnum:sn,ZodPromise:cn,ZodEffects:ln,ZodTransformer:ln,ZodOptional:un,ZodNullable:dn,ZodDefault:fn,ZodCatch:pn,ZodNaN:mn,BRAND:hn,ZodBranded:gn,ZodPipeline:_n,ZodReadonly:vn,custom:yn,Schema:dt,ZodSchema:dt,late:bn,get ZodFirstPartyTypeKind(){return xn},coerce:{string:(e=>At.create({...e,coerce:!0})),number:(e=>Mt.create({...e,coerce:!0})),boolean:(e=>Pt.create({...e,coerce:!0})),bigint:(e=>Nt.create({...e,coerce:!0})),date:(e=>Ft.create({...e,coerce:!0}))},any:jn,array:Fn,bigint:Tn,boolean:En,date:Dn,discriminatedUnion:Rn,effect:Xn,enum:qn,function:Wn,instanceof:Sn,intersection:zn,lazy:Gn,literal:Kn,map:Hn,nan:wn,nativeEnum:Jn,never:Nn,null:An,nullable:Qn,number:Cn,object:G,oboolean:()=>En().optional(),onumber:()=>Cn().optional(),optional:Zn,ostring:()=>W().optional(),pipeline:er,preprocess:$n,promise:Yn,record:Vn,set:Un,strictObject:In,string:W,symbol:On,transformer:Xn,tuple:Bn,undefined:kn,union:Ln,unknown:Mn,void:Pn,NEVER:Ze,ZodIssueCode:V,quotelessJson:He,ZodError:Ue}),tr={exports:{}},nr;function rr(){return nr?tr.exports:(nr=1,(function(e){var t=(function(e){var n=1e7,r=9007199254740992,i=f(r),a=`0123456789abcdefghijklmnopqrstuvwxyz`,o=typeof BigInt==`function`;function s(e,t,n,r){return e===void 0?s[0]:t===void 0||+t==10&&!n?R(e):pe(e,t,n,r)}function c(e,t){this.value=e,this.sign=t,this.isSmall=!1}c.prototype=Object.create(s.prototype);function l(e){this.value=e,this.sign=e<0,this.isSmall=!0}l.prototype=Object.create(s.prototype);function u(e){this.value=e}u.prototype=Object.create(s.prototype);function d(e){return-r0?Math.floor(e):Math.ceil(e)}function _(e,t){var r=e.length,i=t.length,a=Array(r),o=0,s=n,c,l;for(l=0;l=s),a[l]=c-o*s;for(;l0&&a.push(o),a}function v(e,t){return e.length>=t.length?_(e,t):_(t,e)}function y(e,t){var r=e.length,i=Array(r),a=n,o,s;for(s=0;s0;)i[s++]=t%a,t=Math.floor(t/a);return i}c.prototype.add=function(e){var t=R(e);if(this.sign!==t.sign)return this.subtract(t.negate());var n=this.value,r=t.value;return t.isSmall?new c(y(n,Math.abs(r)),this.sign):new c(v(n,r),this.sign)},c.prototype.plus=c.prototype.add,l.prototype.add=function(e){var t=R(e),n=this.value;if(n<0!==t.sign)return this.subtract(t.negate());var r=t.value;if(t.isSmall){if(d(n+r))return new l(n+r);r=f(Math.abs(r))}return new c(y(r,Math.abs(n)),n<0)},l.prototype.plus=l.prototype.add,u.prototype.add=function(e){return new u(this.value+R(e).value)},u.prototype.plus=u.prototype.add;function b(e,t){var r=e.length,i=t.length,a=Array(r),o=0,s=n,c,l;for(c=0;c=0?r=b(e,t):(r=b(t,e),n=!n),r=p(r),typeof r==`number`?(n&&(r=-r),new l(r)):new c(r,n)}function S(e,t,r){var i=e.length,a=Array(i),o=-t,s=n,u,d;for(u=0;u=0)},l.prototype.minus=l.prototype.subtract,u.prototype.subtract=function(e){return new u(this.value-R(e).value)},u.prototype.minus=u.prototype.subtract,c.prototype.negate=function(){return new c(this.value,!this.sign)},l.prototype.negate=function(){var e=this.sign,t=new l(-this.value);return t.sign=!e,t},u.prototype.negate=function(){return new u(-this.value)},c.prototype.abs=function(){return new c(this.value,!1)},l.prototype.abs=function(){return new l(Math.abs(this.value))},u.prototype.abs=function(){return new u(this.value>=0?this.value:-this.value)};function C(e,t){var r=e.length,i=t.length,a=h(r+i),o=n,s,c,l,u,d;for(l=0;l0;)i[c++]=o%a,o=Math.floor(o/a);return i}function T(e,t){for(var n=[];t-->0;)n.push(0);return n.concat(e)}function E(e,t){var n=Math.max(e.length,t.length);if(n<=30)return C(e,t);n=Math.ceil(n/2);var r=e.slice(n),i=e.slice(0,n),a=t.slice(n),o=t.slice(0,n),s=E(i,o),c=E(r,a),l=v(v(s,T(b(b(E(v(i,r),v(o,a)),s),c),n)),T(c,2*n));return m(l),l}function D(e,t){return-.012*e-.012*t+15e-6*e*t>0}c.prototype.multiply=function(e){var t=R(e),r=this.value,i=t.value,a=this.sign!==t.sign,o;if(t.isSmall){if(i===0)return s[0];if(i===1)return this;if(i===-1)return this.negate();if(o=Math.abs(i),o=0;f--){for(d=a-1,l[f+i]!==s&&(d=Math.floor((l[f+i]*a+l[f+i-1])/s)),m=0,g=0,v=u.length,_=0;_i&&(u=(u+1)*s),c=Math.ceil(u/d);do{if(f=w(t,c),M(f,o)<=0)break;c--}while(c);a.push(c),o=b(o,f)}return a.reverse(),[p(a),p(o)]}function te(e,t){var r=e.length,i=h(r),a=n,o,s,c=0,l;for(o=r-1;o>=0;--o)l=c*a+e[o],s=g(l/t),c=l-s*t,i[o]=s|0;return[i,c|0]}function j(e,t){var r,i=R(t);if(o)return[new u(e.value/i.value),new u(e.value%i.value)];var a=e.value,d=i.value,m;if(d===0)throw Error(`Cannot divide by zero`);if(e.isSmall)return i.isSmall?[new l(g(a/d)),new l(a%d)]:[s[0],e];if(i.isSmall){if(d===1)return[e,s[0]];if(d==-1)return[e.negate(),s[0]];var h=Math.abs(d);if(ht.length?1:-1;for(var n=e.length-1;n>=0;n--)if(e[n]!==t[n])return e[n]>t[n]?1:-1;return 0}c.prototype.compareAbs=function(e){var t=R(e),n=this.value,r=t.value;return t.isSmall?1:M(n,r)},l.prototype.compareAbs=function(e){var t=R(e),n=Math.abs(this.value),r=t.value;return t.isSmall?(r=Math.abs(r),n===r?0:n>r?1:-1):-1},u.prototype.compareAbs=function(e){var t=this.value,n=R(e).value;return t=t>=0?t:-t,n=n>=0?n:-n,t===n?0:t>n?1:-1},c.prototype.compare=function(e){if(e===1/0)return-1;if(e===-1/0)return 1;var t=R(e),n=this.value,r=t.value;return this.sign===t.sign?t.isSmall?this.sign?-1:1:M(n,r)*(this.sign?-1:1):t.sign?1:-1},c.prototype.compareTo=c.prototype.compare,l.prototype.compare=function(e){if(e===1/0)return-1;if(e===-1/0)return 1;var t=R(e),n=this.value,r=t.value;return t.isSmall?n==r?0:n>r?1:-1:n<0===t.sign?n<0?1:-1:n<0?-1:1},l.prototype.compareTo=l.prototype.compare,u.prototype.compare=function(e){if(e===1/0)return-1;if(e===-1/0)return 1;var t=this.value,n=R(e).value;return t===n?0:t>n?1:-1},u.prototype.compareTo=u.prototype.compare,c.prototype.equals=function(e){return this.compare(e)===0},u.prototype.eq=u.prototype.equals=l.prototype.eq=l.prototype.equals=c.prototype.eq=c.prototype.equals,c.prototype.notEquals=function(e){return this.compare(e)!==0},u.prototype.neq=u.prototype.notEquals=l.prototype.neq=l.prototype.notEquals=c.prototype.neq=c.prototype.notEquals,c.prototype.greater=function(e){return this.compare(e)>0},u.prototype.gt=u.prototype.greater=l.prototype.gt=l.prototype.greater=c.prototype.gt=c.prototype.greater,c.prototype.lesser=function(e){return this.compare(e)<0},u.prototype.lt=u.prototype.lesser=l.prototype.lt=l.prototype.lesser=c.prototype.lt=c.prototype.lesser,c.prototype.greaterOrEquals=function(e){return this.compare(e)>=0},u.prototype.geq=u.prototype.greaterOrEquals=l.prototype.geq=l.prototype.greaterOrEquals=c.prototype.geq=c.prototype.greaterOrEquals,c.prototype.lesserOrEquals=function(e){return this.compare(e)<=0},u.prototype.leq=u.prototype.lesserOrEquals=l.prototype.leq=l.prototype.lesserOrEquals=c.prototype.leq=c.prototype.lesserOrEquals,c.prototype.isEven=function(){return(this.value[0]&1)==0},l.prototype.isEven=function(){return(this.value&1)==0},u.prototype.isEven=function(){return(this.value&BigInt(1))===BigInt(0)},c.prototype.isOdd=function(){return(this.value[0]&1)==1},l.prototype.isOdd=function(){return(this.value&1)==1},u.prototype.isOdd=function(){return(this.value&BigInt(1))===BigInt(1)},c.prototype.isPositive=function(){return!this.sign},l.prototype.isPositive=function(){return this.value>0},u.prototype.isPositive=l.prototype.isPositive,c.prototype.isNegative=function(){return this.sign},l.prototype.isNegative=function(){return this.value<0},u.prototype.isNegative=l.prototype.isNegative,c.prototype.isUnit=function(){return!1},l.prototype.isUnit=function(){return Math.abs(this.value)===1},u.prototype.isUnit=function(){return this.abs().value===BigInt(1)},c.prototype.isZero=function(){return!1},l.prototype.isZero=function(){return this.value===0},u.prototype.isZero=function(){return this.value===BigInt(0)},c.prototype.isDivisibleBy=function(e){var t=R(e);return t.isZero()?!1:t.isUnit()?!0:t.compareAbs(2)===0?this.isEven():this.mod(t).isZero()},u.prototype.isDivisibleBy=l.prototype.isDivisibleBy=c.prototype.isDivisibleBy;function ne(e){var t=e.abs();if(t.isUnit())return!1;if(t.equals(2)||t.equals(3)||t.equals(5))return!0;if(t.isEven()||t.isDivisibleBy(3)||t.isDivisibleBy(5))return!1;if(t.lesser(49))return!0}function N(e,n){for(var r=e.prev(),i=r,a=0,o,s,c;i.isEven();)i=i.divide(2),a++;next:for(s=0;s-r?new l(e-1):new c(i,!0)},u.prototype.prev=function(){return new u(this.value-BigInt(1))};for(var P=[1];2*P[P.length-1]<=n;)P.push(2*P[P.length-1]);var F=P.length,re=P[F-1];function ie(e){return Math.abs(e)<=n}c.prototype.shiftLeft=function(e){var t=R(e).toJSNumber();if(!ie(t))throw Error(String(t)+` is too large for shifting.`);if(t<0)return this.shiftRight(-t);var n=this;if(n.isZero())return n;for(;t>=F;)n=n.multiply(re),t-=F-1;return n.multiply(P[t])},u.prototype.shiftLeft=l.prototype.shiftLeft=c.prototype.shiftLeft,c.prototype.shiftRight=function(e){var t,n=R(e).toJSNumber();if(!ie(n))throw Error(String(n)+` is too large for shifting.`);if(n<0)return this.shiftLeft(-n);for(var r=this;n>=F;){if(r.isZero()||r.isNegative()&&r.isUnit())return r;t=j(r,re),r=t[1].isNegative()?t[0].prev():t[0],n-=F-1}return t=j(r,P[n]),t[1].isNegative()?t[0].prev():t[0]},u.prototype.shiftRight=l.prototype.shiftRight=c.prototype.shiftRight;function ae(e,n,r){n=R(n);for(var i=e.isNegative(),a=n.isNegative(),o=i?e.not():e,s=a?n.not():n,c=0,l=0,u=null,d=null,f=[];!o.isZero()||!s.isZero();)u=j(o,re),c=u[1].toJSNumber(),i&&(c=re-1-c),d=j(s,re),l=d[1].toJSNumber(),a&&(l=re-1-l),o=u[0],s=d[0],f.push(r(c,l));for(var p=r(+!!i,+!!a)===0?t(0):t(-1),m=f.length-1;m>=0;--m)p=p.multiply(re).add(t(f[m]));return p}c.prototype.not=function(){return this.negate().prev()},u.prototype.not=l.prototype.not=c.prototype.not,c.prototype.and=function(e){return ae(this,e,function(e,t){return e&t})},u.prototype.and=l.prototype.and=c.prototype.and,c.prototype.or=function(e){return ae(this,e,function(e,t){return e|t})},u.prototype.or=l.prototype.or=c.prototype.or,c.prototype.xor=function(e){return ae(this,e,function(e,t){return e^t})},u.prototype.xor=l.prototype.xor=c.prototype.xor;var I=1<<30,L=(n&-n)*(n&-n)|I;function oe(e){var t=e.value,r=typeof t==`number`?t|I:typeof t==`bigint`?t|BigInt(I):t[0]+t[1]*n|L;return r&-r}function se(e,n){if(n.compareTo(e)<=0){var r=se(e,n.square(n)),i=r.p,a=r.e,o=i.multiply(n);return o.compareTo(e)<=0?{p:o,e:a*2+1}:{p:i,e:a*2}}return{p:t(1),e:0}}c.prototype.bitLength=function(){var e=this;return e.compareTo(t(0))<0&&(e=e.negate().subtract(t(1))),e.compareTo(t(0))===0?t(0):t(se(e,t(2)).e).add(t(1))},u.prototype.bitLength=l.prototype.bitLength=c.prototype.bitLength;function ce(e,t){return e=R(e),t=R(t),e.greater(t)?e:t}function le(e,t){return e=R(e),t=R(t),e.lesser(t)?e:t}function ue(e,t){if(e=R(e).abs(),t=R(t).abs(),e.equals(t))return e;if(e.isZero())return t;if(t.isZero())return e;for(var n=s[1],r,i;e.isEven()&&t.isEven();)r=le(oe(e),oe(t)),e=e.divide(r),t=t.divide(r),n=n.multiply(r);for(;e.isEven();)e=e.divide(oe(e));do{for(;t.isEven();)t=t.divide(oe(t));e.greater(t)&&(i=t,t=e,e=i),t=t.subtract(e)}while(!t.isZero());return n.isUnit()?e:e.multiply(n)}function de(e,t){return e=R(e).abs(),t=R(t).abs(),e.divide(ue(e,t)).multiply(t)}function fe(e,t,r){e=R(e),t=R(t);var i=r||Math.random,a=le(e,t),o=ce(e,t).subtract(a).add(1);if(o.isSmall)return a.add(Math.floor(i()*o));for(var c=ge(o,n).value,l=[],u=!0,d=0;d=s){if(l===`1`&&s===1)continue;throw Error(l+` is not a valid digit in base `+t+`.`)}}t=R(t);var u=[],d=e[0]===`-`;for(o=+!!d;o`&&o=0;a--)r=r.add(e[a].times(i)),i=i.times(t);return n?r.negate():r}function he(e,t){return t||=a,e`}function ge(e,n){if(n=t(n),n.isZero()){if(e.isZero())return{value:[0],isNegative:!1};throw Error(`Cannot convert nonzero numbers to base 0.`)}if(n.equals(-1)){if(e.isZero())return{value:[0],isNegative:!1};if(e.isNegative())return{value:[].concat.apply([],Array.apply(null,Array(-e.toJSNumber())).map(Array.prototype.valueOf,[1,0])),isNegative:!1};var r=Array.apply(null,Array(e.toJSNumber()-1)).map(Array.prototype.valueOf,[0,1]);return r.unshift([1]),{value:[].concat.apply([],r),isNegative:!1}}var i=!1;if(e.isNegative()&&n.isPositive()&&(i=!0,e=e.abs()),n.isUnit())return e.isZero()?{value:[0],isNegative:!1}:{value:Array.apply(null,Array(e.toJSNumber())).map(Number.prototype.valueOf,1),isNegative:i};for(var a=[],o=e,s;o.isNegative()||o.compareAbs(n)>=0;){s=o.divmod(n),o=s.quotient;var c=s.remainder;c.isNegative()&&(c=n.minus(c).abs(),o=o.next()),a.push(c.toJSNumber())}return a.push(o.toJSNumber()),{value:a.reverse(),isNegative:i}}function _e(e,t,n){var r=ge(e,t);return(r.isNegative?`-`:``)+r.value.map(function(e){return he(e,n)}).join(``)}c.prototype.toArray=function(e){return ge(this,e)},l.prototype.toArray=function(e){return ge(this,e)},u.prototype.toArray=function(e){return ge(this,e)},c.prototype.toString=function(t,n){if(t===e&&(t=10),t!==10||n)return _e(this,t,n);for(var r=this.value,i=r.length,a=String(r[--i]),o=`0000000`,s;--i>=0;)s=String(r[i]),a+=o.slice(s.length)+s;return(this.sign?`-`:``)+a},l.prototype.toString=function(t,n){return t===e&&(t=10),t!=10||n?_e(this,t,n):String(this.value)},u.prototype.toString=l.prototype.toString,u.prototype.toJSON=c.prototype.toJSON=l.prototype.toJSON=function(){return this.toString()},c.prototype.valueOf=function(){return parseInt(this.toString(),10)},c.prototype.toJSNumber=c.prototype.valueOf,l.prototype.valueOf=function(){return this.value},l.prototype.toJSNumber=l.prototype.valueOf,u.prototype.valueOf=u.prototype.toJSNumber=function(){return parseInt(this.toString(),10)};function ve(e){if(d(+e)){var t=+e;if(t===g(t))return o?new u(BigInt(t)):new l(t);throw Error(`Invalid integer: `+e)}var n=e[0]===`-`;n&&(e=e.slice(1));var r=e.split(/e/i);if(r.length>2)throw Error(`Invalid integer: `+r.join(`e`));if(r.length===2){var i=r[1];if(i[0]===`+`&&(i=i.slice(1)),i=+i,i!==g(i)||!d(i))throw Error(`Invalid integer: `+i+` is not a valid exponent.`);var a=r[0],s=a.indexOf(`.`);if(s>=0&&(i-=a.length-s-1,a=a.slice(0,s)+a.slice(s+1)),i<0)throw Error(`Cannot include negative exponent part for integers`);a+=Array(i+1).join(`0`),e=a}if(!/^([0-9][0-9]*)$/.test(e))throw Error(`Invalid integer: `+e);if(o)return new u(BigInt(n?`-`+e:e));for(var f=[],p=e.length,h=7,_=p-h;p>0;)f.push(+e.slice(_,p)),_-=h,_<0&&(_=0),p-=h;return m(f),new c(f,n)}function ye(e){if(o)return new u(BigInt(e));if(d(e)){if(e!==g(e))throw Error(e+` is not an integer.`);return new l(e)}return ve(e.toString())}function R(e){return typeof e==`number`?ye(e):typeof e==`string`?ve(e):typeof e==`bigint`?new u(e):e}for(var be=0;be<1e3;be++)s[be]=R(be),be>0&&(s[-be]=R(-be));return s.one=s[1],s.zero=s[0],s.minusOne=s[-1],s.max=ce,s.min=le,s.gcd=ue,s.lcm=de,s.isInstance=function(e){return e instanceof c||e instanceof l||e instanceof u},s.randBetween=fe,s.fromArray=function(e,t,n){return me(e.map(R),R(t||10),n)},s})();e.hasOwnProperty(`exports`)&&(e.exports=t)})(tr),tr.exports)}var ir=Pe(rr()),ar=64,or=16,sr=ar/or;function cr(){try{return!0}catch{return!1}}function lr(e,t,n){let r=0;for(let i=0;ie&t[n]))}or({parts:t}){return new e(this.parts.map((e,n)=>e|t[n]))}xor({parts:t}){return new e(this.parts.map((e,n)=>e^t[n]))}not(){return new e(this.parts.map(e=>~e))}equals({parts:e}){return this.parts.every((t,n)=>t===e[n])}toString(){if(this.str!=null)return this.str;let e=Array(ar/4);return this.parts.forEach((t,n)=>{let r=ur(t.toString());for(let t=0;t<4;t++)e[t+n*4]=r[3-t]||0}),this.str=ir.fromArray(e,16).toString()}toJSON(){return this.toString()}},pr=cr();pr&&BigInt.prototype.toJSON==null&&(BigInt.prototype.toJSON=function(){return this.toString()});var mr={},hr=pr?function(e){return BigInt(e)}:function(e){return e instanceof fr?e:(typeof e==`number`&&(e=e.toString()),mr[e]??(mr[e]=fr.fromString(e)),mr[e])},gr=hr(0),_r=pr?function(e=gr,t=gr){return e&t}:function(e=gr,t=gr){return e.and(t)},vr=pr?function(e=gr,t=gr){return e|t}:function(e=gr,t=gr){return e.or(t)},yr=pr?function(e=gr,t=gr){return e^t}:function(e=gr,t=gr){return e.xor(t)},br=pr?function(e=gr){return~e}:function(e=gr){return e.not()},xr=pr?function(e,t){return e===t}:function(e,t){return e==null||t==null?e==t:e.equals(t)};function Sr(...e){let t=e[0];for(let n=1;n{let[n]=Object.entries(e).find(([,e])=>e===t)??[];return t!=null&&n===void 0?e.UNHANDLED:t},W().or(Cn()))}function Nr(e){let t=yn().transform(t=>{let n=e.safeParse(t);return n.success?n.data:e._def.defaultValue()});return t.overlayType=e,t}var Pr=K.object({image_url:K.string()}).describe(`Response for "INITIATE_IMAGE_UPLOAD" Command`),Fr=K.object({mediaUrl:K.string().max(1024)}).describe(`Request for "OPEN_SHARE_MOMENT_DIALOG" Command`),Ir=K.object({access_token:K.union([K.string(),K.null()]).optional()}).describe(`Request for "AUTHENTICATE" Command`),Lr=K.object({access_token:K.string(),user:K.object({username:K.string(),discriminator:K.string(),id:K.string(),avatar:K.union([K.string(),K.null()]).optional(),public_flags:K.number(),global_name:K.union([K.string(),K.null()]).optional()}),scopes:K.array(Nr(K.enum(`identify,identify.premium,email,connections,guilds,guilds.join,guilds.members.read,guilds.channels.read,gdm.join,bot,rpc,rpc.notifications.read,rpc.voice.read,rpc.voice.write,rpc.video.read,rpc.video.write,rpc.screenshare.read,rpc.screenshare.write,rpc.activities.write,webhook.incoming,messages.read,applications.builds.upload,applications.builds.read,applications.commands,applications.commands.permissions.update,applications.commands.update,applications.store.update,applications.entitlements,activities.read,activities.write,activities.invites.write,relationships.read,relationships.write,voice,dm_channels.read,role_connections.write,presences.read,presences.write,openid,dm_channels.messages.read,dm_channels.messages.write,gateway.connect,account.global_name.update,payment_sources.country_code,sdk.social_layer_presence,sdk.social_layer,lobbies.write,application_identities.write`.split(`,`)).or(K.literal(-1)).default(-1))),expires:K.string(),application:K.object({description:K.string(),icon:K.union([K.string(),K.null()]).optional(),id:K.string(),rpc_origins:K.array(K.string()).optional(),name:K.string()})}).describe(`Response for "AUTHENTICATE" Command`),Rr=K.object({participants:K.array(K.object({id:K.string(),username:K.string(),global_name:K.union([K.string(),K.null()]).optional(),discriminator:K.string(),avatar:K.union([K.string(),K.null()]).optional(),flags:K.number(),bot:K.boolean(),avatar_decoration_data:K.union([K.object({asset:K.union([K.string(),K.null()]).optional(),skuId:K.string().optional(),expiresAt:K.number().optional()}),K.null()]).optional(),premium_type:K.union([K.number(),K.null()]).optional(),nickname:K.string().optional()}))}).describe(`Response for "GET_ACTIVITY_INSTANCE_CONNECTED_PARTICIPANTS" Command`),zr=K.object({command:K.string(),options:K.array(K.object({name:K.string(),value:K.string()})).optional(),content:K.string().max(2e3).optional(),require_launch_channel:K.boolean().optional(),preview_image:K.object({height:K.number(),url:K.string(),width:K.number()}).optional(),components:K.array(K.object({type:K.literal(1),components:K.array(K.object({type:K.literal(2),style:K.number().gte(1).lte(5),label:K.string().max(80).optional(),custom_id:K.string().max(100).describe(`Developer-defined identifier for the button; max 100 characters`).optional()})).max(5).optional()})).optional(),pid:K.number().optional()}).describe(`Request for "SHARE_INTERACTION" Command`),Br=K.object({success:K.boolean()}).describe(`Response for "SHARE_INTERACTION" Command`),Vr=K.object({custom_id:K.string().max(64).optional(),message:K.string().max(1e3),link_id:K.string().max(64).optional()}).describe(`Request for "SHARE_LINK" Command`),Hr=K.object({success:K.boolean(),didCopyLink:K.boolean(),didSendMessage:K.boolean()}).describe(`Response for "SHARE_LINK" Command`),Ur=K.object({relationships:K.array(K.object({type:K.number(),user:K.object({id:K.string(),username:K.string(),global_name:K.union([K.string(),K.null()]).optional(),discriminator:K.string(),avatar:K.union([K.string(),K.null()]).optional(),flags:K.number(),bot:K.boolean(),avatar_decoration_data:K.union([K.object({asset:K.union([K.string(),K.null()]).optional(),skuId:K.string().optional(),expiresAt:K.number().optional()}),K.null()]).optional(),premium_type:K.union([K.number(),K.null()]).optional()}),presence:K.object({status:K.string(),activity:K.union([K.object({session_id:K.string().optional(),type:K.number().optional(),name:K.string(),url:K.union([K.string(),K.null()]).optional(),application_id:K.string().optional(),status_display_type:K.number().optional(),state:K.string().optional(),state_url:K.string().optional(),details:K.string().optional(),details_url:K.string().optional(),emoji:K.union([K.object({name:K.string(),id:K.union([K.string(),K.null()]).optional(),animated:K.union([K.boolean(),K.null()]).optional()}),K.null()]).optional(),assets:K.object({large_image:K.string().optional(),large_text:K.string().optional(),large_url:K.string().optional(),small_image:K.string().optional(),small_text:K.string().optional(),small_url:K.string().optional()}).optional(),timestamps:K.object({start:K.number().optional(),end:K.number().optional()}).optional(),party:K.object({id:K.string().optional(),size:K.array(K.number()).min(2).max(2).optional(),privacy:K.number().optional()}).optional(),secrets:K.object({match:K.string().optional(),join:K.string().optional()}).optional(),sync_id:K.string().optional(),created_at:K.number().optional(),instance:K.boolean().optional(),flags:K.number().optional(),metadata:K.object({}).optional(),platform:K.string().optional(),supported_platforms:K.array(K.string()).optional(),buttons:K.array(K.string()).optional(),hangStatus:K.string().optional()}),K.null()]).optional()}).optional()}))}).describe(`Response for "GET_RELATIONSHIPS" Command`),Wr=K.object({user_id:K.string(),content:K.string().min(0).max(1024).optional()}).describe(`Request for "INVITE_USER_EMBEDDED" Command`),Gr=K.object({id:K.string().max(64)}).describe(`Request for "GET_USER" Command`),Kr=K.union([K.object({id:K.string(),username:K.string(),global_name:K.union([K.string(),K.null()]).optional(),discriminator:K.string(),avatar:K.union([K.string(),K.null()]).optional(),flags:K.number(),bot:K.boolean(),avatar_decoration_data:K.union([K.object({asset:K.union([K.string(),K.null()]).optional(),skuId:K.string().optional(),expiresAt:K.number().optional()}),K.null()]).optional(),premium_type:K.union([K.number(),K.null()]).optional()}),K.null()]),qr=K.object({quest_id:K.string()}).describe(`Request for "GET_QUEST_ENROLLMENT_STATUS" Command`),Jr=K.object({quest_id:K.string(),is_enrolled:K.boolean(),enrolled_at:K.union([K.string(),K.null()]).optional()}).describe(`Response for "GET_QUEST_ENROLLMENT_STATUS" Command`),Yr=K.object({quest_id:K.string()}).describe(`Request for "QUEST_START_TIMER" Command`),Xr=K.object({success:K.boolean()}).describe(`Response for "QUEST_START_TIMER" Command`),Zr=K.object({quest_id:K.string(),enrolled_at:K.union([K.string(),K.null()]).optional(),completed_at:K.union([K.string(),K.null()]).optional(),external_cta_url:K.string()}).describe(`Response for "GET_QUEST" Command`),Qr=K.object({ticket:K.string()}).describe(`Response for "REQUEST_PROXY_TICKET_REFRESH" Command`),$r;(function(e){e.INITIATE_IMAGE_UPLOAD=`INITIATE_IMAGE_UPLOAD`,e.OPEN_SHARE_MOMENT_DIALOG=`OPEN_SHARE_MOMENT_DIALOG`,e.AUTHENTICATE=`AUTHENTICATE`,e.GET_ACTIVITY_INSTANCE_CONNECTED_PARTICIPANTS=`GET_ACTIVITY_INSTANCE_CONNECTED_PARTICIPANTS`,e.SHARE_INTERACTION=`SHARE_INTERACTION`,e.SHARE_LINK=`SHARE_LINK`,e.GET_RELATIONSHIPS=`GET_RELATIONSHIPS`,e.INVITE_USER_EMBEDDED=`INVITE_USER_EMBEDDED`,e.GET_USER=`GET_USER`,e.GET_QUEST_ENROLLMENT_STATUS=`GET_QUEST_ENROLLMENT_STATUS`,e.QUEST_START_TIMER=`QUEST_START_TIMER`,e.GET_QUEST=`GET_QUEST`,e.REQUEST_PROXY_TICKET_REFRESH=`REQUEST_PROXY_TICKET_REFRESH`})($r||={});var ei=K.object({}).optional().nullable(),ti=K.void(),ni={[$r.INITIATE_IMAGE_UPLOAD]:{request:ti,response:Pr},[$r.OPEN_SHARE_MOMENT_DIALOG]:{request:Fr,response:ei},[$r.AUTHENTICATE]:{request:Ir,response:Lr},[$r.GET_ACTIVITY_INSTANCE_CONNECTED_PARTICIPANTS]:{request:ti,response:Rr},[$r.SHARE_INTERACTION]:{request:zr,response:Br},[$r.SHARE_LINK]:{request:Vr,response:Hr},[$r.GET_RELATIONSHIPS]:{request:ti,response:Ur},[$r.INVITE_USER_EMBEDDED]:{request:Wr,response:ei},[$r.GET_USER]:{request:Gr,response:Kr},[$r.GET_QUEST_ENROLLMENT_STATUS]:{request:qr,response:Jr},[$r.QUEST_START_TIMER]:{request:Yr,response:Xr},[$r.GET_QUEST]:{request:ti,response:Zr},[$r.REQUEST_PROXY_TICKET_REFRESH]:{request:ti,response:Qr}},ri=t({Activity:()=>_i,Attachment:()=>Ei,CertifiedDevice:()=>Ki,CertifiedDeviceTypeObject:()=>Gi,Channel:()=>xi,ChannelMention:()=>Ti,ChannelTypesObject:()=>bi,Commands:()=>q,DISPATCH:()=>ii,Embed:()=>Ni,EmbedAuthor:()=>ji,EmbedField:()=>Mi,EmbedFooter:()=>Di,EmbedProvider:()=>Ai,Emoji:()=>fi,Entitlement:()=>Xi,EntitlementTypesObject:()=>Yi,Guild:()=>wi,GuildMember:()=>ui,GuildMemberRPC:()=>di,Image:()=>Oi,KeyTypesObject:()=>Bi,LayoutMode:()=>ia,LayoutModeTypeObject:()=>ra,Message:()=>Ri,MessageActivity:()=>Fi,MessageApplication:()=>Ii,MessageReference:()=>Li,Orientation:()=>na,OrientationLockState:()=>Qi,OrientationLockStateTypeObject:()=>Zi,OrientationTypeObject:()=>ta,PermissionOverwrite:()=>yi,PermissionOverwriteTypeObject:()=>vi,PresenceUpdate:()=>Si,Reaction:()=>Pi,ReceiveFramePayload:()=>ai,Relationship:()=>ci,Role:()=>Ci,Scopes:()=>si,ScopesObject:()=>oi,ShortcutKey:()=>Vi,Sku:()=>Ji,SkuTypeObject:()=>qi,Status:()=>gi,StatusObject:()=>hi,ThermalState:()=>ea,ThermalStateTypeObject:()=>$i,User:()=>li,UserVoiceState:()=>mi,Video:()=>ki,VoiceDevice:()=>zi,VoiceSettingModeTypeObject:()=>Hi,VoiceSettingsIO:()=>Wi,VoiceSettingsMode:()=>Ui,VoiceState:()=>pi}),ii=`DISPATCH`,q;(function(e){e.AUTHORIZE=`AUTHORIZE`,e.GET_GUILDS=`GET_GUILDS`,e.GET_GUILD=`GET_GUILD`,e.GET_CHANNEL=`GET_CHANNEL`,e.GET_CHANNELS=`GET_CHANNELS`,e.SELECT_VOICE_CHANNEL=`SELECT_VOICE_CHANNEL`,e.SELECT_TEXT_CHANNEL=`SELECT_TEXT_CHANNEL`,e.SUBSCRIBE=`SUBSCRIBE`,e.UNSUBSCRIBE=`UNSUBSCRIBE`,e.CAPTURE_SHORTCUT=`CAPTURE_SHORTCUT`,e.SET_CERTIFIED_DEVICES=`SET_CERTIFIED_DEVICES`,e.SET_ACTIVITY=`SET_ACTIVITY`,e.GET_SKUS=`GET_SKUS`,e.GET_ENTITLEMENTS=`GET_ENTITLEMENTS`,e.GET_SKUS_EMBEDDED=`GET_SKUS_EMBEDDED`,e.GET_ENTITLEMENTS_EMBEDDED=`GET_ENTITLEMENTS_EMBEDDED`,e.START_PURCHASE=`START_PURCHASE`,e.SET_CONFIG=`SET_CONFIG`,e.SEND_ANALYTICS_EVENT=`SEND_ANALYTICS_EVENT`,e.USER_SETTINGS_GET_LOCALE=`USER_SETTINGS_GET_LOCALE`,e.OPEN_EXTERNAL_LINK=`OPEN_EXTERNAL_LINK`,e.ENCOURAGE_HW_ACCELERATION=`ENCOURAGE_HW_ACCELERATION`,e.CAPTURE_LOG=`CAPTURE_LOG`,e.SET_ORIENTATION_LOCK_STATE=`SET_ORIENTATION_LOCK_STATE`,e.OPEN_INVITE_DIALOG=`OPEN_INVITE_DIALOG`,e.GET_PLATFORM_BEHAVIORS=`GET_PLATFORM_BEHAVIORS`,e.GET_CHANNEL_PERMISSIONS=`GET_CHANNEL_PERMISSIONS`,e.AUTHENTICATE=`AUTHENTICATE`,e.GET_ACTIVITY_INSTANCE_CONNECTED_PARTICIPANTS=`GET_ACTIVITY_INSTANCE_CONNECTED_PARTICIPANTS`,e.GET_QUEST=`GET_QUEST`,e.GET_QUEST_ENROLLMENT_STATUS=`GET_QUEST_ENROLLMENT_STATUS`,e.GET_RELATIONSHIPS=`GET_RELATIONSHIPS`,e.GET_USER=`GET_USER`,e.INITIATE_IMAGE_UPLOAD=`INITIATE_IMAGE_UPLOAD`,e.INVITE_USER_EMBEDDED=`INVITE_USER_EMBEDDED`,e.OPEN_SHARE_MOMENT_DIALOG=`OPEN_SHARE_MOMENT_DIALOG`,e.QUEST_START_TIMER=`QUEST_START_TIMER`,e.REQUEST_PROXY_TICKET_REFRESH=`REQUEST_PROXY_TICKET_REFRESH`,e.SHARE_INTERACTION=`SHARE_INTERACTION`,e.SHARE_LINK=`SHARE_LINK`})(q||={});var ai=G({cmd:W(),data:Mn(),evt:An(),nonce:W()}).passthrough(),oi=Object.assign(Object.assign({},Lr.shape.scopes.element.overlayType._def.innerType.options[0].Values),{UNHANDLED:-1}),si=Mr(oi),ci=Ur.shape.relationships.element,li=G({id:W(),username:W(),discriminator:W(),global_name:W().optional().nullable(),avatar:W().optional().nullable(),avatar_decoration_data:G({asset:W(),sku_id:W().optional()}).nullable(),bot:En(),flags:Cn().optional().nullable(),premium_type:Cn().optional().nullable()}),ui=G({user:li,nick:W().optional().nullable(),roles:Fn(W()),joined_at:W(),deaf:En(),mute:En()}),di=G({user_id:W(),nick:W().optional().nullable(),guild_id:W(),avatar:W().optional().nullable(),avatar_decoration_data:G({asset:W(),sku_id:W().optional().nullable()}).optional().nullable(),color_string:W().optional().nullable()}),fi=G({id:W(),name:W().optional().nullable(),roles:Fn(W()).optional().nullable(),user:li.optional().nullable(),require_colons:En().optional().nullable(),managed:En().optional().nullable(),animated:En().optional().nullable(),available:En().optional().nullable()}),pi=G({mute:En(),deaf:En(),self_mute:En(),self_deaf:En(),suppress:En()}),mi=G({mute:En(),nick:W(),user:li,voice_state:pi,volume:Cn()}),hi={UNHANDLED:-1,IDLE:`idle`,DND:`dnd`,ONLINE:`online`,OFFLINE:`offline`},gi=Mr(hi),_i=G({name:W(),type:Cn(),url:W().optional().nullable(),created_at:Cn().optional().nullable(),timestamps:G({start:Cn(),end:Cn()}).partial().optional().nullable(),application_id:W().optional().nullable(),details:W().optional().nullable(),details_url:W().url().optional().nullable(),state:W().optional().nullable(),state_url:W().url().optional().nullable(),emoji:fi.optional().nullable(),party:G({id:W().optional().nullable(),size:Fn(Cn()).optional().nullable()}).optional().nullable(),assets:G({large_image:W().nullable(),large_text:W().nullable(),large_url:W().url().optional().nullable(),small_image:W().nullable(),small_text:W().nullable(),small_url:W().url().optional().nullable()}).partial().optional().nullable(),secrets:G({join:W(),match:W()}).partial().optional().nullable(),instance:En().optional().nullable(),flags:Cn().optional().nullable()}),vi={UNHANDLED:-1,ROLE:0,MEMBER:1},yi=G({id:W(),type:Mr(vi),allow:W(),deny:W()}),bi={UNHANDLED:-1,DM:1,GROUP_DM:3,GUILD_TEXT:0,GUILD_VOICE:2,GUILD_CATEGORY:4,GUILD_ANNOUNCEMENT:5,GUILD_STORE:6,ANNOUNCEMENT_THREAD:10,PUBLIC_THREAD:11,PRIVATE_THREAD:12,GUILD_STAGE_VOICE:13,GUILD_DIRECTORY:14,GUILD_FORUM:15},xi=G({id:W(),type:Mr(bi),guild_id:W().optional().nullable(),position:Cn().optional().nullable(),permission_overwrites:Fn(yi).optional().nullable(),name:W().optional().nullable(),topic:W().optional().nullable(),nsfw:En().optional().nullable(),last_message_id:W().optional().nullable(),bitrate:Cn().optional().nullable(),user_limit:Cn().optional().nullable(),rate_limit_per_user:Cn().optional().nullable(),recipients:Fn(li).optional().nullable(),icon:W().optional().nullable(),owner_id:W().optional().nullable(),application_id:W().optional().nullable(),parent_id:W().optional().nullable(),last_pin_timestamp:W().optional().nullable()}),Si=G({user:li,guild_id:W(),status:gi,activities:Fn(_i),client_status:G({desktop:gi,mobile:gi,web:gi}).partial()}),Ci=G({id:W(),name:W(),color:Cn(),hoist:En(),position:Cn(),permissions:W(),managed:En(),mentionable:En()}),wi=G({id:W(),name:W(),owner_id:W(),icon:W().nullable(),icon_hash:W().optional().nullable(),splash:W().nullable(),discovery_splash:W().nullable(),owner:En().optional().nullable(),permissions:W().optional().nullable(),region:W(),afk_channel_id:W().nullable(),afk_timeout:Cn(),widget_enabled:En().optional().nullable(),widget_channel_id:W().optional().nullable(),verification_level:Cn(),default_message_notifications:Cn(),explicit_content_filter:Cn(),roles:Fn(Ci),emojis:Fn(fi),features:Fn(W()),mfa_level:Cn(),application_id:W().nullable(),system_channel_id:W().nullable(),system_channel_flags:Cn(),rules_channel_id:W().nullable(),joined_at:W().optional().nullable(),large:En().optional().nullable(),unavailable:En().optional().nullable(),member_count:Cn().optional().nullable(),voice_states:Fn(pi).optional().nullable(),members:Fn(ui).optional().nullable(),channels:Fn(xi).optional().nullable(),presences:Fn(Si).optional().nullable(),max_presences:Cn().optional().nullable(),max_members:Cn().optional().nullable(),vanity_url_code:W().nullable(),description:W().nullable(),banner:W().nullable(),premium_tier:Cn(),premium_subscription_count:Cn().optional().nullable(),preferred_locale:W(),public_updates_channel_id:W().nullable(),max_video_channel_users:Cn().optional().nullable(),approximate_member_count:Cn().optional().nullable(),approximate_presence_count:Cn().optional().nullable()}),Ti=G({id:W(),guild_id:W(),type:Cn(),name:W()}),Ei=G({id:W(),filename:W(),size:Cn(),url:W(),proxy_url:W(),height:Cn().optional().nullable(),width:Cn().optional().nullable()}),Di=G({text:W(),icon_url:W().optional().nullable(),proxy_icon_url:W().optional().nullable()}),Oi=G({url:W().optional().nullable(),proxy_url:W().optional().nullable(),height:Cn().optional().nullable(),width:Cn().optional().nullable()}),ki=Oi.omit({proxy_url:!0}),Ai=G({name:W().optional().nullable(),url:W().optional().nullable()}),ji=G({name:W().optional().nullable(),url:W().optional().nullable(),icon_url:W().optional().nullable(),proxy_icon_url:W().optional().nullable()}),Mi=G({name:W(),value:W(),inline:En()}),Ni=G({title:W().optional().nullable(),type:W().optional().nullable(),description:W().optional().nullable(),url:W().optional().nullable(),timestamp:W().optional().nullable(),color:Cn().optional().nullable(),footer:Di.optional().nullable(),image:Oi.optional().nullable(),thumbnail:Oi.optional().nullable(),video:ki.optional().nullable(),provider:Ai.optional().nullable(),author:ji.optional().nullable(),fields:Fn(Mi).optional().nullable()}),Pi=G({count:Cn(),me:En(),emoji:fi}),Fi=G({type:Cn(),party_id:W().optional().nullable()}),Ii=G({id:W(),cover_image:W().optional().nullable(),description:W(),icon:W().optional().nullable(),name:W()}),Li=G({message_id:W().optional().nullable(),channel_id:W().optional().nullable(),guild_id:W().optional().nullable()}),Ri=G({id:W(),channel_id:W(),guild_id:W().optional().nullable(),author:li.optional().nullable(),member:ui.optional().nullable(),content:W(),timestamp:W(),edited_timestamp:W().optional().nullable(),tts:En(),mention_everyone:En(),mentions:Fn(li),mention_roles:Fn(W()),mention_channels:Fn(Ti),attachments:Fn(Ei),embeds:Fn(Ni),reactions:Fn(Pi).optional().nullable(),nonce:Ln([W(),Cn()]).optional().nullable(),pinned:En(),webhook_id:W().optional().nullable(),type:Cn(),activity:Fi.optional().nullable(),application:Ii.optional().nullable(),message_reference:Li.optional().nullable(),flags:Cn().optional().nullable(),stickers:Fn(Mn()).optional().nullable(),referenced_message:Mn().optional().nullable()}),zi=G({id:W(),name:W()}),Bi={UNHANDLED:-1,KEYBOARD_KEY:0,MOUSE_BUTTON:1,KEYBOARD_MODIFIER_KEY:2,GAMEPAD_BUTTON:3},Vi=G({type:Mr(Bi),code:Cn(),name:W()}),Hi={UNHANDLED:-1,PUSH_TO_TALK:`PUSH_TO_TALK`,VOICE_ACTIVITY:`VOICE_ACTIVITY`},Ui=G({type:Mr(Hi),auto_threshold:En(),threshold:Cn(),shortcut:Fn(Vi),delay:Cn()}),Wi=G({device_id:W(),volume:Cn(),available_devices:Fn(zi)}),Gi={UNHANDLED:-1,AUDIO_INPUT:`AUDIO_INPUT`,AUDIO_OUTPUT:`AUDIO_OUTPUT`,VIDEO_INPUT:`VIDEO_INPUT`},Ki=G({type:Mr(Gi),id:W(),vendor:G({name:W(),url:W()}),model:G({name:W(),url:W()}),related:Fn(W()),echo_cancellation:En().optional().nullable(),noise_suppression:En().optional().nullable(),automatic_gain_control:En().optional().nullable(),hardware_mute:En().optional().nullable()}),qi={UNHANDLED:-1,APPLICATION:1,DLC:2,CONSUMABLE:3,BUNDLE:4,SUBSCRIPTION:5},Ji=G({id:W(),name:W(),type:Mr(qi),price:G({amount:Cn(),currency:W()}),application_id:W(),flags:Cn(),release_date:W().nullable()}),Yi={UNHANDLED:-1,PURCHASE:1,PREMIUM_SUBSCRIPTION:2,DEVELOPER_GIFT:3,TEST_MODE_PURCHASE:4,FREE_PURCHASE:5,USER_GIFT:6,PREMIUM_PURCHASE:7},Xi=G({id:W(),sku_id:W(),application_id:W(),user_id:W(),gift_code_flags:Cn(),type:Mr(Yi),gifter_user_id:W().optional().nullable(),branches:Fn(W()).optional().nullable(),starts_at:W().optional().nullable(),ends_at:W().optional().nullable(),parent_id:W().optional().nullable(),consumed:En().optional().nullable(),deleted:En().optional().nullable(),gift_code_batch_id:W().optional().nullable()}),Zi={UNHANDLED:-1,UNLOCKED:1,PORTRAIT:2,LANDSCAPE:3},Qi=Mr(Zi),$i={UNHANDLED:-1,NOMINAL:0,FAIR:1,SERIOUS:2,CRITICAL:3},ea=Mr($i),ta={UNHANDLED:-1,PORTRAIT:0,LANDSCAPE:1},na=Mr(ta),ra={UNHANDLED:-1,FOCUSED:0,PIP:1,GRID:2},ia=Mr(ra),aa=`ERROR`,oa;(function(e){e.READY=`READY`,e.VOICE_STATE_UPDATE=`VOICE_STATE_UPDATE`,e.SPEAKING_START=`SPEAKING_START`,e.SPEAKING_STOP=`SPEAKING_STOP`,e.ACTIVITY_LAYOUT_MODE_UPDATE=`ACTIVITY_LAYOUT_MODE_UPDATE`,e.ORIENTATION_UPDATE=`ORIENTATION_UPDATE`,e.CURRENT_USER_UPDATE=`CURRENT_USER_UPDATE`,e.CURRENT_GUILD_MEMBER_UPDATE=`CURRENT_GUILD_MEMBER_UPDATE`,e.ENTITLEMENT_CREATE=`ENTITLEMENT_CREATE`,e.THERMAL_STATE_UPDATE=`THERMAL_STATE_UPDATE`,e.ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE=`ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE`,e.RELATIONSHIP_UPDATE=`RELATIONSHIP_UPDATE`,e.ACTIVITY_JOIN=`ACTIVITY_JOIN`,e.QUEST_ENROLLMENT_STATUS_UPDATE=`QUEST_ENROLLMENT_STATUS_UPDATE`})(oa||={});var sa=ai.extend({evt:Jn(oa),nonce:W().nullable(),cmd:Kn(ii),data:G({}).passthrough()}),ca=ai.extend({evt:Kn(aa),data:G({code:Cn(),message:W().optional()}).passthrough(),cmd:Jn(q),nonce:W().nullable()}),la=Ln([sa,sa.extend({evt:W()}),ca]);function ua(e){let t=e.evt;if(!(t in oa))throw Error(`Unrecognized event type ${e.evt}`);return da[t].payload.parse(e)}var da={[oa.READY]:{payload:sa.extend({evt:Kn(oa.READY),data:G({v:Cn(),config:G({cdn_host:W().optional(),api_endpoint:W(),environment:W()}),user:G({id:W(),username:W(),discriminator:W(),avatar:W().optional()}).optional()})})},[oa.VOICE_STATE_UPDATE]:{payload:sa.extend({evt:Kn(oa.VOICE_STATE_UPDATE),data:mi}),subscribeArgs:G({channel_id:W()})},[oa.SPEAKING_START]:{payload:sa.extend({evt:Kn(oa.SPEAKING_START),data:G({lobby_id:W().optional(),channel_id:W().optional(),user_id:W()})}),subscribeArgs:G({lobby_id:W().nullable().optional(),channel_id:W().nullable().optional()})},[oa.SPEAKING_STOP]:{payload:sa.extend({evt:Kn(oa.SPEAKING_STOP),data:G({lobby_id:W().optional(),channel_id:W().optional(),user_id:W()})}),subscribeArgs:G({lobby_id:W().nullable().optional(),channel_id:W().nullable().optional()})},[oa.ACTIVITY_LAYOUT_MODE_UPDATE]:{payload:sa.extend({evt:Kn(oa.ACTIVITY_LAYOUT_MODE_UPDATE),data:G({layout_mode:Mr(ra)})})},[oa.ORIENTATION_UPDATE]:{payload:sa.extend({evt:Kn(oa.ORIENTATION_UPDATE),data:G({screen_orientation:Mr(ta),orientation:Jn(Ar)})})},[oa.CURRENT_USER_UPDATE]:{payload:sa.extend({evt:Kn(oa.CURRENT_USER_UPDATE),data:li})},[oa.CURRENT_GUILD_MEMBER_UPDATE]:{payload:sa.extend({evt:Kn(oa.CURRENT_GUILD_MEMBER_UPDATE),data:di}),subscribeArgs:G({guild_id:W()})},[oa.ENTITLEMENT_CREATE]:{payload:sa.extend({evt:Kn(oa.ENTITLEMENT_CREATE),data:G({entitlement:Xi})})},[oa.THERMAL_STATE_UPDATE]:{payload:sa.extend({evt:Kn(oa.THERMAL_STATE_UPDATE),data:G({thermal_state:ea})})},[oa.ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE]:{payload:sa.extend({evt:Kn(oa.ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE),data:G({participants:Rr.shape.participants})})},[oa.RELATIONSHIP_UPDATE]:{payload:sa.extend({evt:Kn(oa.RELATIONSHIP_UPDATE),data:ci})},[oa.ACTIVITY_JOIN]:{payload:sa.extend({evt:Kn(oa.ACTIVITY_JOIN),data:G({applicationId:W(),secret:W()})})},[oa.QUEST_ENROLLMENT_STATUS_UPDATE]:{payload:sa.extend({evt:Kn(oa.QUEST_ENROLLMENT_STATUS_UPDATE),data:G({quest_id:W(),is_enrolled:En(),enrolled_at:W().date()})})}};function fa(e,t){throw t}var pa=G({}).nullable(),ma=G({code:W()}),ha=G({guilds:Fn(G({id:W(),name:W()}))}),ga=G({id:W(),name:W(),icon_url:W().optional(),members:Fn(ui)}),_a=G({id:W(),type:Mr(bi),guild_id:W().optional().nullable(),name:W().optional().nullable(),topic:W().optional().nullable(),bitrate:Cn().optional().nullable(),user_limit:Cn().optional().nullable(),position:Cn().optional().nullable(),voice_states:Fn(mi),messages:Fn(Ri)}),va=G({channels:Fn(xi)});_a.nullable();var ya=_a.nullable(),ba=_a.nullable();G({input:Wi,output:Wi,mode:Ui,automatic_gain_control:En(),echo_cancellation:En(),noise_suppression:En(),qos:En(),silence_warning:En(),deaf:En(),mute:En()});var xa=G({evt:W()}),Sa=G({shortcut:Vi}),Ca=_i,wa=G({skus:Fn(Ji)}),Ta=G({entitlements:Fn(Xi)}),Ea=Fn(Xi).nullable(),Da=G({use_interactive_pip:En()}),Oa=G({locale:W()}),ka=G({enabled:En()}),Aa=G({permissions:Tn().or(W())}),ja=Nr(G({opened:En().or(An())}).default({opened:null})),Ma=G({iosKeyboardResizesView:Zn(En())}),Na=ai.extend({cmd:Jn(q),evt:An()});function Pa({cmd:e,data:t}){switch(e){case q.AUTHORIZE:return ma.parse(t);case q.CAPTURE_SHORTCUT:return Sa.parse(t);case q.ENCOURAGE_HW_ACCELERATION:return ka.parse(t);case q.GET_CHANNEL:return _a.parse(t);case q.GET_CHANNELS:return va.parse(t);case q.GET_CHANNEL_PERMISSIONS:return Aa.parse(t);case q.GET_GUILD:return ga.parse(t);case q.GET_GUILDS:return ha.parse(t);case q.GET_PLATFORM_BEHAVIORS:return Ma.parse(t);case q.GET_CHANNEL:return _a.parse(t);case q.SELECT_TEXT_CHANNEL:return ba.parse(t);case q.SELECT_VOICE_CHANNEL:return ya.parse(t);case q.SET_ACTIVITY:return Ca.parse(t);case q.GET_SKUS_EMBEDDED:return wa.parse(t);case q.GET_ENTITLEMENTS_EMBEDDED:return Ta.parse(t);case q.SET_CONFIG:return Da.parse(t);case q.START_PURCHASE:return Ea.parse(t);case q.SUBSCRIBE:case q.UNSUBSCRIBE:return xa.parse(t);case q.USER_SETTINGS_GET_LOCALE:return Oa.parse(t);case q.OPEN_EXTERNAL_LINK:return ja.parse(t);case q.SET_ORIENTATION_LOCK_STATE:case q.SET_CERTIFIED_DEVICES:case q.SEND_ANALYTICS_EVENT:case q.OPEN_INVITE_DIALOG:case q.CAPTURE_LOG:case q.GET_SKUS:case q.GET_ENTITLEMENTS:return pa.parse(t);case q.AUTHENTICATE:case q.GET_ACTIVITY_INSTANCE_CONNECTED_PARTICIPANTS:case q.GET_QUEST:case q.GET_QUEST_ENROLLMENT_STATUS:case q.GET_RELATIONSHIPS:case q.GET_USER:case q.INITIATE_IMAGE_UPLOAD:case q.INVITE_USER_EMBEDDED:case q.OPEN_SHARE_MOMENT_DIALOG:case q.QUEST_START_TIMER:case q.REQUEST_PROXY_TICKET_REFRESH:case q.SHARE_INTERACTION:case q.SHARE_LINK:let{response:n}=ni[e];return n.parse(t);default:fa(e,Error(`Unrecognized command ${e}`))}}function Fa(e){return Object.assign(Object.assign({},e),{data:Pa(e)})}G({frame_id:W(),platform:Jn(jr).optional().nullable()}),G({v:Kn(1),encoding:Kn(`json`).optional(),client_id:W(),frame_id:W()});var Ia=G({code:Cn(),message:W().optional()}),La=G({evt:W().nullable(),nonce:W().nullable(),data:Mn().nullable(),cmd:W()}).passthrough();function Ra(e){let t=La.parse(e);return t.evt==null?Fa(Na.passthrough().parse(t)):t.evt===`ERROR`?ca.parse(t):ua(la.parse(t))}function za(e,t,n,r=()=>void 0){let i=ai.extend({cmd:Kn(t),data:n});return async n=>{let a=await e({cmd:t,args:n,transfer:r(n)});return i.parse(a).data}}function Ba(e,t=()=>void 0){let n=ni[e].response,r=ai.extend({cmd:Kn(e),data:n});return n=>async i=>{let a=await n({cmd:e,args:i,transfer:t(i)});return r.parse(a).data}}var Va=e=>za(e,q.AUTHORIZE,ma),Ha=e=>za(e,q.CAPTURE_LOG,pa),Ua=e=>za(e,q.ENCOURAGE_HW_ACCELERATION,ka),Wa=e=>za(e,q.GET_CHANNEL,_a),Ga=e=>za(e,q.GET_ENTITLEMENTS_EMBEDDED,Ta),Ka=e=>za(e,q.GET_SKUS_EMBEDDED,wa),qa=e=>za(e,q.GET_CHANNEL_PERMISSIONS,Aa),Ja=e=>za(e,q.GET_PLATFORM_BEHAVIORS,Ma),Ya=e=>za(e,q.OPEN_EXTERNAL_LINK,ja),Xa=e=>za(e,q.OPEN_INVITE_DIALOG,pa);_i.pick({state:!0,state_url:!0,details:!0,details_url:!0,timestamps:!0,assets:!0,party:!0,secrets:!0,instance:!0,type:!0}).extend({type:_i.shape.type.optional(),instance:_i.shape.instance.optional()}).nullable();var Za=e=>za(e,q.SET_ACTIVITY,Ca),Qa=e=>za(e,q.SET_CONFIG,Da);function $a({sendCommand:e,cmd:t,response:n,fallbackTransform:r,transferTransform:i=()=>void 0}){let a=ai.extend({cmd:Kn(t),data:n});return async n=>{try{let r=await e({cmd:t,args:n,transfer:i(n)});return a.parse(r).data}catch(o){if(o.code===kr.INVALID_PAYLOAD){let o=r(n),s=await e({cmd:t,args:o,transfer:i(o)});return a.parse(s).data}else throw o}}}var eo=e=>({lock_state:e.lock_state,picture_in_picture_lock_state:e.picture_in_picture_lock_state}),to=e=>$a({sendCommand:e,cmd:q.SET_ORIENTATION_LOCK_STATE,response:pa,fallbackTransform:eo}),no=e=>za(e,q.START_PURCHASE,Ea),ro=e=>za(e,q.USER_SETTINGS_GET_LOCALE,Oa),io=Ba($r.AUTHENTICATE),ao=Ba($r.GET_ACTIVITY_INSTANCE_CONNECTED_PARTICIPANTS),oo=Ba($r.GET_QUEST),so=Ba($r.GET_QUEST_ENROLLMENT_STATUS),co=Ba($r.GET_RELATIONSHIPS),lo=Ba($r.GET_USER),uo=Ba($r.INITIATE_IMAGE_UPLOAD),fo=Ba($r.INVITE_USER_EMBEDDED),po=Ba($r.OPEN_SHARE_MOMENT_DIALOG),mo=Ba($r.QUEST_START_TIMER),ho=Ba($r.REQUEST_PROXY_TICKET_REFRESH),go=Ba($r.SHARE_INTERACTION),_o=Ba($r.SHARE_LINK);function vo(e){return{authorize:Va(e),captureLog:Ha(e),encourageHardwareAcceleration:Ua(e),getChannel:Wa(e),getChannelPermissions:qa(e),getEntitlements:Ga(e),getPlatformBehaviors:Ja(e),getSkus:Ka(e),openExternalLink:Ya(e),openInviteDialog:Xa(e),setActivity:Za(e),setConfig:Qa(e),setOrientationLockState:to(e),startPurchase:no(e),userSettingsGetLocale:ro(e),getInstanceConnectedParticipants:ao(e),authenticate:io(e),getActivityInstanceConnectedParticipants:ao(e),getQuest:oo(e),getQuestEnrollmentStatus:so(e),getRelationships:co(e),getUser:lo(e),initiateImageUpload:uo(e),inviteUserEmbedded:fo(e),openShareMomentDialog:po(e),questStartTimer:mo(e),requestProxyTicketRefresh:ho(e),shareInteraction:go(e),shareLink:_o(e)}}var yo=class extends Error{constructor(e,t=``){super(t),this.code=e,this.message=t,this.name=`Discord SDK Error`}};function bo(){return{disableConsoleLogOverride:!1}}var xo=[`log`,`warn`,`debug`,`info`,`error`];function So(e,t,n){let r=e[t],i=e;r&&(e[t]=function(){let e=[].slice.call(arguments);n(t,``+e.join(` `)),r.apply(i,e)})}var Co=`2.5.0`,wo={randomUUID:typeof crypto<`u`&&crypto.randomUUID&&crypto.randomUUID.bind(crypto)},To,Eo=new Uint8Array(16);function Do(){if(!To){if(typeof crypto>`u`||!crypto.getRandomValues)throw Error(`crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported`);To=crypto.getRandomValues.bind(crypto)}return To(Eo)}var Oo=[];for(let e=0;e<256;++e)Oo.push((e+256).toString(16).slice(1));function ko(e,t=0){return(Oo[e[t+0]]+Oo[e[t+1]]+Oo[e[t+2]]+Oo[e[t+3]]+`-`+Oo[e[t+4]]+Oo[e[t+5]]+`-`+Oo[e[t+6]]+Oo[e[t+7]]+`-`+Oo[e[t+8]]+Oo[e[t+9]]+`-`+Oo[e[t+10]]+Oo[e[t+11]]+Oo[e[t+12]]+Oo[e[t+13]]+Oo[e[t+14]]+Oo[e[t+15]]).toLowerCase()}function Ao(e,t,n){if(wo.randomUUID&&!t&&!e)return wo.randomUUID();e||={};let r=e.random??e.rng?.()??Do();if(r.length<16)throw Error(`Random bytes length must be >= 16`);return r[6]=r[6]&15|64,r[8]=r[8]&63|128,ko(r)}var jo;(function(e){e[e.HANDSHAKE=0]=`HANDSHAKE`,e[e.FRAME=1]=`FRAME`,e[e.CLOSE=2]=`CLOSE`,e[e.HELLO=3]=`HELLO`})(jo||={});var Mo=new Set(No());function No(){return typeof window>`u`?[]:[window.location.origin,`https://discord.com`,`https://discordapp.com`,`https://ptb.discord.com`,`https://ptb.discordapp.com`,`https://canary.discord.com`,`https://canary.discordapp.com`,`https://staging.discord.co`,`http://localhost:3333`,`https://pax.discord.com`,`null`]}function Po(){return[window.parent.opener??window.parent,document.referrer?document.referrer:`*`]}var Fo=class{getTransfer(e){switch(e.cmd){case q.SUBSCRIBE:case q.UNSUBSCRIBE:return;default:return e.transfer??void 0}}constructor(e,t){if(this.sdkVersion=Co,this.mobileAppVersion=null,this.source=null,this.sourceOrigin=``,this.eventBus=new Re,this.pendingCommands=new Map,this.sendCommand=e=>{var t;if(this.source==null)throw Error(`Attempting to send message before initialization`);let n=Ao();return(t=this.source)==null||t.postMessage([jo.FRAME,Object.assign(Object.assign({},e),{nonce:n})],this.sourceOrigin,this.getTransfer(e)),new Promise((e,t)=>{this.pendingCommands.set(n,{resolve:e,reject:t})})},this.commands=vo(this.sendCommand),this.handleMessage=e=>{if(!Mo.has(e.origin))return;let t=e.data;if(!Array.isArray(t))return;let[n,r]=t;switch(n){case jo.HELLO:return;case jo.CLOSE:return this.handleClose(r);case jo.HANDSHAKE:return this.handleHandshake();case jo.FRAME:return this.handleFrame(r);default:throw Error(`Invalid message format`)}},this.isReady=!1,this.clientId=e,this.configuration=t??bo(),typeof window<`u`&&window.addEventListener(`message`,this.handleMessage),typeof window>`u`){this.frameId=``,this.instanceId=``,this.customId=null,this.referrerId=null,this.platform=jr.DESKTOP,this.guildId=null,this.channelId=null,this.locationId=null;return}let n=new URLSearchParams(this._getSearch()),r=n.get(`frame_id`);if(!r)throw Error(`frame_id query param is not defined`);this.frameId=r;let i=n.get(`instance_id`);if(!i)throw Error(`instance_id query param is not defined`);this.instanceId=i;let a=n.get(`platform`);if(!a)throw Error(`platform query param is not defined`);if(a!==jr.DESKTOP&&a!==jr.MOBILE)throw Error(`Invalid query param "platform" of "${a}". Valid values are "${jr.DESKTOP}" or "${jr.MOBILE}"`);this.platform=a,this.customId=n.get(`custom_id`),this.referrerId=n.get(`referrer_id`),this.guildId=n.get(`guild_id`),this.channelId=n.get(`channel_id`),this.locationId=n.get(`location_id`),this.mobileAppVersion=n.get(`mobile_app_version`),[this.source,this.sourceOrigin]=Po(),this.addOnReadyListener(),this.handshake()}close(e,t){var n;window.removeEventListener(`message`,this.handleMessage);let r=Ao();(n=this.source)==null||n.postMessage([jo.CLOSE,{code:e,message:t,nonce:r}],this.sourceOrigin)}async subscribe(e,t,...n){let[r]=n,i=this.eventBus.listenerCount(e),a=this.eventBus.on(e,t);return Object.values(oa).includes(e)&&e!==oa.READY&&i===0&&await this.sendCommand({cmd:q.SUBSCRIBE,args:r,evt:e}),a}async unsubscribe(e,t,...n){let[r]=n;return e!==oa.READY&&this.eventBus.listenerCount(e)===1&&await this.sendCommand({cmd:q.UNSUBSCRIBE,evt:e,args:r}),this.eventBus.off(e,t)}async ready(){this.isReady||await new Promise(e=>{this.eventBus.once(oa.READY,e)})}parseMajorMobileVersion(){if(this.mobileAppVersion&&this.mobileAppVersion.includes(`.`))try{return parseInt(this.mobileAppVersion.split(`.`)[0])}catch{return-1}return-1}handshake(){var e;let t={v:1,encoding:`json`,client_id:this.clientId,frame_id:this.frameId},n=this.parseMajorMobileVersion();(this.platform===jr.DESKTOP||n>=250)&&(t.sdk_version=this.sdkVersion),(e=this.source)==null||e.postMessage([jo.HANDSHAKE,t],this.sourceOrigin)}addOnReadyListener(){this.eventBus.once(oa.READY,()=>{this.overrideConsoleLogging(),this.isReady=!0})}overrideConsoleLogging(){if(this.configuration.disableConsoleLogOverride)return;let e=(e,t)=>{this.commands.captureLog({level:e,message:t})};xo.forEach(t=>{So(console,t,e)})}handleClose(e){Ia.parse(e)}handleHandshake(){}handleFrame(e){var t,n;let r;try{r=Ra(e)}catch(t){console.error(`Failed to parse`,e),console.error(t);return}if(r.cmd===`DISPATCH`)this.eventBus.emit(r.evt,r.data);else{if(r.evt===`ERROR`){if(r.nonce!=null){(t=this.pendingCommands.get(r.nonce))==null||t.reject(r.data),this.pendingCommands.delete(r.nonce);return}this.eventBus.emit(`error`,new yo(r.data.code,r.data.message))}if(r.nonce==null){console.error(`Missing nonce`,e);return}(n=this.pendingCommands.get(r.nonce))==null||n.resolve(r),this.pendingCommands.delete(r.nonce)}}_getSearch(){return typeof window>`u`?``:window.location.search}},Io=1e9,Lo={precision:20,rounding:4,toExpNeg:-7,toExpPos:21,LN10:`2.302585092994045684017991454684364207601101488628772976033327900967572609677352480235997205089598298341967784042286`},Ro=!0,zo=`[DecimalError] `,Bo=zo+`Invalid argument: `,Vo=zo+`Exponent out of range: `,Ho=Math.floor,Uo=Math.pow,Wo=/^(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i,Go,Ko=1e7,qo=7,Jo=9007199254740991,Yo=Ho(Jo/qo),J={};J.absoluteValue=J.abs=function(){var e=new this.constructor(this);return e.s&&=1,e},J.comparedTo=J.cmp=function(e){var t,n,r,i,a=this;if(e=new a.constructor(e),a.s!==e.s)return a.s||-e.s;if(a.e!==e.e)return a.e>e.e^a.s<0?1:-1;for(r=a.d.length,i=e.d.length,t=0,n=re.d[t]^a.s<0?1:-1;return r===i?0:r>i^a.s<0?1:-1},J.decimalPlaces=J.dp=function(){var e=this,t=e.d.length-1,n=(t-e.e)*qo;if(t=e.d[t],t)for(;t%10==0;t/=10)n--;return n<0?0:n},J.dividedBy=J.div=function(e){return $o(this,new this.constructor(e))},J.dividedToIntegerBy=J.idiv=function(e){var t=this,n=t.constructor;return os($o(t,new n(e),0,1),n.precision)},J.equals=J.eq=function(e){return!this.cmp(e)},J.exponent=function(){return ts(this)},J.greaterThan=J.gt=function(e){return this.cmp(e)>0},J.greaterThanOrEqualTo=J.gte=function(e){return this.cmp(e)>=0},J.isInteger=J.isint=function(){return this.e>this.d.length-2},J.isNegative=J.isneg=function(){return this.s<0},J.isPositive=J.ispos=function(){return this.s>0},J.isZero=function(){return this.s===0},J.lessThan=J.lt=function(e){return this.cmp(e)<0},J.lessThanOrEqualTo=J.lte=function(e){return this.cmp(e)<1},J.logarithm=J.log=function(e){var t,n=this,r=n.constructor,i=r.precision,a=i+5;if(e===void 0)e=new r(10);else if(e=new r(e),e.s<1||e.eq(Go))throw Error(zo+`NaN`);if(n.s<1)throw Error(zo+(n.s?`NaN`:`-Infinity`));return n.eq(Go)?new r(0):(Ro=!1,t=$o(is(n,a),is(e,a),a),Ro=!0,os(t,i))},J.minus=J.sub=function(e){var t=this;return e=new t.constructor(e),t.s==e.s?ss(t,e):Xo(t,(e.s=-e.s,e))},J.modulo=J.mod=function(e){var t,n=this,r=n.constructor,i=r.precision;if(e=new r(e),!e.s)throw Error(zo+`NaN`);return n.s?(Ro=!1,t=$o(n,e,0,1).times(e),Ro=!0,n.minus(t)):os(new r(n),i)},J.naturalExponential=J.exp=function(){return es(this)},J.naturalLogarithm=J.ln=function(){return is(this)},J.negated=J.neg=function(){var e=new this.constructor(this);return e.s=-e.s||0,e},J.plus=J.add=function(e){var t=this;return e=new t.constructor(e),t.s==e.s?Xo(t,e):ss(t,(e.s=-e.s,e))},J.precision=J.sd=function(e){var t,n,r,i=this;if(e!==void 0&&e!==!!e&&e!==1&&e!==0)throw Error(Bo+e);if(t=ts(i)+1,r=i.d.length-1,n=r*qo+1,r=i.d[r],r){for(;r%10==0;r/=10)n--;for(r=i.d[0];r>=10;r/=10)n++}return e&&t>n?t:n},J.squareRoot=J.sqrt=function(){var e,t,n,r,i,a,o,s=this,c=s.constructor;if(s.s<1){if(!s.s)return new c(0);throw Error(zo+`NaN`)}for(e=ts(s),Ro=!1,i=Math.sqrt(+s),i==0||i==1/0?(t=Qo(s.d),(t.length+e)%2==0&&(t+=`0`),i=Math.sqrt(t),e=Ho((e+1)/2)-(e<0||e%2),i==1/0?t=`5e`+e:(t=i.toExponential(),t=t.slice(0,t.indexOf(`e`)+1)+e),r=new c(t)):r=new c(i.toString()),n=c.precision,i=o=n+3;;)if(a=r,r=a.plus($o(s,a,o+2)).times(.5),Qo(a.d).slice(0,o)===(t=Qo(r.d)).slice(0,o)){if(t=t.slice(o-3,o+1),i==o&&t==`4999`){if(os(a,n+1,0),a.times(a).eq(s)){r=a;break}}else if(t!=`9999`)break;o+=4}return Ro=!0,os(r,n)},J.times=J.mul=function(e){var t,n,r,i,a,o,s,c,l,u=this,d=u.constructor,f=u.d,p=(e=new d(e)).d;if(!u.s||!e.s)return new d(0);for(e.s*=u.s,n=u.e+e.e,c=f.length,l=p.length,c=0;){for(t=0,i=c+r;i>r;)s=a[i]+p[r]*f[i-r-1]+t,a[i--]=s%Ko|0,t=s/Ko|0;a[i]=(a[i]+t)%Ko|0}for(;!a[--o];)a.pop();return t?++n:a.shift(),e.d=a,e.e=n,Ro?os(e,d.precision):e},J.toDecimalPlaces=J.todp=function(e,t){var n=this,r=n.constructor;return n=new r(n),e===void 0?n:(Zo(e,0,Io),t===void 0?t=r.rounding:Zo(t,0,8),os(n,e+ts(n)+1,t))},J.toExponential=function(e,t){var n,r=this,i=r.constructor;return e===void 0?n=cs(r,!0):(Zo(e,0,Io),t===void 0?t=i.rounding:Zo(t,0,8),r=os(new i(r),e+1,t),n=cs(r,!0,e+1)),n},J.toFixed=function(e,t){var n,r,i=this,a=i.constructor;return e===void 0?cs(i):(Zo(e,0,Io),t===void 0?t=a.rounding:Zo(t,0,8),r=os(new a(i),e+ts(i)+1,t),n=cs(r.abs(),!1,e+ts(r)+1),i.isneg()&&!i.isZero()?`-`+n:n)},J.toInteger=J.toint=function(){var e=this,t=e.constructor;return os(new t(e),ts(e)+1,t.rounding)},J.toNumber=function(){return+this},J.toPower=J.pow=function(e){var t,n,r,i,a,o,s=this,c=s.constructor,l=12,u=+(e=new c(e));if(!e.s)return new c(Go);if(s=new c(s),!s.s){if(e.s<1)throw Error(zo+`Infinity`);return s}if(s.eq(Go))return s;if(r=c.precision,e.eq(Go))return os(s,r);if(t=e.e,n=e.d.length-1,o=t>=n,a=s.s,!o){if(a<0)throw Error(zo+`NaN`)}else if((n=u<0?-u:u)<=Jo){for(i=new c(Go),t=Math.ceil(r/qo+4),Ro=!1;n%2&&(i=i.times(s),ls(i.d,t)),n=Ho(n/2),n!==0;)s=s.times(s),ls(s.d,t);return Ro=!0,e.s<0?new c(Go).div(i):os(i,r)}return a=a<0&&e.d[Math.max(t,n)]&1?-1:1,s.s=1,Ro=!1,i=e.times(is(s,r+l)),Ro=!0,i=es(i),i.s=a,i},J.toPrecision=function(e,t){var n,r,i=this,a=i.constructor;return e===void 0?(n=ts(i),r=cs(i,n<=a.toExpNeg||n>=a.toExpPos)):(Zo(e,1,Io),t===void 0?t=a.rounding:Zo(t,0,8),i=os(new a(i),e,t),n=ts(i),r=cs(i,e<=n||n<=a.toExpNeg,e)),r},J.toSignificantDigits=J.tosd=function(e,t){var n=this,r=n.constructor;return e===void 0?(e=r.precision,t=r.rounding):(Zo(e,1,Io),t===void 0?t=r.rounding:Zo(t,0,8)),os(new r(n),e,t)},J.toString=J.valueOf=J.val=J.toJSON=J[Symbol.for(`nodejs.util.inspect.custom`)]=function(){var e=this,t=ts(e),n=e.constructor;return cs(e,t<=n.toExpNeg||t>=n.toExpPos)};function Xo(e,t){var n,r,i,a,o,s,c,l,u=e.constructor,d=u.precision;if(!e.s||!t.s)return t.s||(t=new u(e)),Ro?os(t,d):t;if(c=e.d,l=t.d,o=e.e,i=t.e,c=c.slice(),a=o-i,a){for(a<0?(r=c,a=-a,s=l.length):(r=l,i=o,s=c.length),o=Math.ceil(d/qo),s=o>s?o+1:s+1,a>s&&(a=s,r.length=1),r.reverse();a--;)r.push(0);r.reverse()}for(s=c.length,a=l.length,s-a<0&&(a=s,r=l,l=c,c=r),n=0;a;)n=(c[--a]=c[a]+l[a]+n)/Ko|0,c[a]%=Ko;for(n&&(c.unshift(n),++i),s=c.length;c[--s]==0;)c.pop();return t.d=c,t.e=i,Ro?os(t,d):t}function Zo(e,t,n){if(e!==~~e||en)throw Error(Bo+e)}function Qo(e){var t,n,r,i=e.length-1,a=``,o=e[0];if(i>0){for(a+=o,t=1;tr?1:-1;else for(i=a=0;it[i]?1:-1;break}return a}function n(e,t,n){for(var r=0;n--;)e[n]-=r,r=+(e[n]1;)e.shift()}return function(r,i,a,o){var s,c,l,u,d,f,p,m,h,g,_,v,y,b,x,S,C,w,T=r.constructor,E=r.s==i.s?1:-1,D=r.d,O=i.d;if(!r.s)return new T(r);if(!i.s)throw Error(zo+`Division by zero`);for(c=r.e-i.e,C=O.length,x=D.length,p=new T(E),m=p.d=[],l=0;O[l]==(D[l]||0);)++l;if(O[l]>(D[l]||0)&&--c,v=a==null?a=T.precision:o?a+(ts(r)-ts(i))+1:a,v<0)return new T(0);if(v=v/qo+2|0,l=0,C==1)for(u=0,O=O[0],v++;(l1&&(O=e(O,u),D=e(D,u),C=O.length,x=D.length),b=C,h=D.slice(0,C),g=h.length;g=Ko/2&&++S;do u=0,s=t(O,h,C,g),s<0?(_=h[0],C!=g&&(_=_*Ko+(h[1]||0)),u=_/S|0,u>1?(u>=Ko&&(u=Ko-1),d=e(O,u),f=d.length,g=h.length,s=t(d,h,f,g),s==1&&(u--,n(d,C16)throw Error(Vo+ts(e));if(!e.s)return new u(Go);for(t==null?(Ro=!1,s=d):s=t,o=new u(.03125);e.abs().gte(.1);)e=e.times(o),l+=5;for(r=Math.log(Uo(2,l))/Math.LN10*2+5|0,s+=r,n=i=a=new u(Go),u.precision=s;;){if(i=os(i.times(e),s),n=n.times(++c),o=a.plus($o(i,n,s)),Qo(o.d).slice(0,s)===Qo(a.d).slice(0,s)){for(;l--;)a=os(a.times(a),s);return u.precision=d,t==null?(Ro=!0,os(a,d)):a}a=o}}function ts(e){for(var t=e.e*qo,n=e.d[0];n>=10;n/=10)t++;return t}function ns(e,t,n){if(t>e.LN10.sd())throw Ro=!0,n&&(e.precision=n),Error(zo+`LN10 precision limit exceeded`);return os(new e(e.LN10),t)}function rs(e){for(var t=``;e--;)t+=`0`;return t}function is(e,t){var n,r,i,a,o,s,c,l,u,d=1,f=10,p=e,m=p.d,h=p.constructor,g=h.precision;if(p.s<1)throw Error(zo+(p.s?`NaN`:`-Infinity`));if(p.eq(Go))return new h(0);if(t==null?(Ro=!1,l=g):l=t,p.eq(10))return t??(Ro=!0),ns(h,l);if(l+=f,h.precision=l,n=Qo(m),r=n.charAt(0),a=ts(p),Math.abs(a)<0x5543df729c000){for(;r<7&&r!=1||r==1&&n.charAt(1)>3;)p=p.times(e),n=Qo(p.d),r=n.charAt(0),d++;a=ts(p),r>1?(p=new h(`0.`+n),a++):p=new h(r+`.`+n.slice(1))}else return c=ns(h,l+2,g).times(a+``),p=is(new h(r+`.`+n.slice(1)),l-f).plus(c),h.precision=g,t==null?(Ro=!0,os(p,g)):p;for(s=o=p=$o(p.minus(Go),p.plus(Go),l),u=os(p.times(p),l),i=3;;){if(o=os(o.times(u),l),c=s.plus($o(o,new h(i),l)),Qo(c.d).slice(0,l)===Qo(s.d).slice(0,l))return s=s.times(2),a!==0&&(s=s.plus(ns(h,l+2,g).times(a+``))),s=$o(s,new h(d),l),h.precision=g,t==null?(Ro=!0,os(s,g)):s;s=c,i+=2}}function as(e,t){var n,r,i;for((n=t.indexOf(`.`))>-1&&(t=t.replace(`.`,``)),(r=t.search(/e/i))>0?(n<0&&(n=r),n+=+t.slice(r+1),t=t.substring(0,r)):n<0&&(n=t.length),r=0;t.charCodeAt(r)===48;)++r;for(i=t.length;t.charCodeAt(i-1)===48;)--i;if(t=t.slice(r,i),t){if(i-=r,n=n-r-1,e.e=Ho(n/qo),e.d=[],r=(n+1)%qo,n<0&&(r+=qo),rYo||e.e<-Yo))throw Error(Vo+n)}else e.s=0,e.e=0,e.d=[0];return e}function os(e,t,n){var r,i,a,o,s,c,l,u,d=e.d;for(o=1,a=d[0];a>=10;a/=10)o++;if(r=t-o,r<0)r+=qo,i=t,l=d[u=0];else{if(u=Math.ceil((r+1)/qo),a=d.length,u>=a)return e;for(l=a=d[u],o=1;a>=10;a/=10)o++;r%=qo,i=r-qo+o}if(n!==void 0&&(a=Uo(10,o-i-1),s=l/a%10|0,c=t<0||d[u+1]!==void 0||l%a,c=n<4?(s||c)&&(n==0||n==(e.s<0?3:2)):s>5||s==5&&(n==4||c||n==6&&(r>0?i>0?l/Uo(10,o-i):0:d[u-1])%10&1||n==(e.s<0?8:7))),t<1||!d[0])return c?(a=ts(e),d.length=1,t=t-a-1,d[0]=Uo(10,(qo-t%qo)%qo),e.e=Ho(-t/qo)||0):(d.length=1,d[0]=e.e=e.s=0),e;if(r==0?(d.length=u,a=1,u--):(d.length=u+1,a=Uo(10,qo-r),d[u]=i>0?(l/Uo(10,o-i)%Uo(10,i)|0)*a:0),c)for(;;)if(u==0){(d[0]+=a)==Ko&&(d[0]=1,++e.e);break}else{if(d[u]+=a,d[u]!=Ko)break;d[u--]=0,a=1}for(r=d.length;d[--r]===0;)d.pop();if(Ro&&(e.e>Yo||e.e<-Yo))throw Error(Vo+ts(e));return e}function ss(e,t){var n,r,i,a,o,s,c,l,u,d,f=e.constructor,p=f.precision;if(!e.s||!t.s)return t.s?t.s=-t.s:t=new f(e),Ro?os(t,p):t;if(c=e.d,d=t.d,r=t.e,l=e.e,c=c.slice(),o=l-r,o){for(u=o<0,u?(n=c,o=-o,s=d.length):(n=d,r=l,s=c.length),i=Math.max(Math.ceil(p/qo),s)+2,o>i&&(o=i,n.length=1),n.reverse(),i=o;i--;)n.push(0);n.reverse()}else{for(i=c.length,s=d.length,u=i0;--i)c[s++]=0;for(i=d.length;i>o;){if(c[--i]0?a=a.charAt(0)+`.`+a.slice(1)+rs(r):o>1&&(a=a.charAt(0)+`.`+a.slice(1)),a=a+(i<0?`e`:`e+`)+i):i<0?(a=`0.`+rs(-i-1)+a,n&&(r=n-o)>0&&(a+=rs(r))):i>=o?(a+=rs(i+1-o),n&&(r=n-i-1)>0&&(a=a+`.`+rs(r))):((r=i+1)0&&(i+1===o&&(a+=`.`),a+=rs(r))),e.s<0?`-`+a:a}function ls(e,t){if(e.length>t)return e.length=t,!0}function us(e){var t,n,r;function i(e){var t=this;if(!(t instanceof i))return new i(e);if(t.constructor=i,e instanceof i){t.s=e.s,t.e=e.e,t.d=(e=e.d)?e.slice():e;return}if(typeof e==`number`){if(e*0!=0)throw Error(Bo+e);if(e>0)t.s=1;else if(e<0)e=-e,t.s=-1;else{t.s=0,t.e=0,t.d=[0];return}if(e===~~e&&e<1e7){t.e=0,t.d=[e];return}return as(t,e.toString())}else if(typeof e!=`string`)throw Error(Bo+e);if(e.charCodeAt(0)===45?(e=e.slice(1),t.s=-1):t.s=1,Wo.test(e))as(t,e);else throw Error(Bo+e)}if(i.prototype=J,i.ROUND_UP=0,i.ROUND_DOWN=1,i.ROUND_CEIL=2,i.ROUND_FLOOR=3,i.ROUND_HALF_UP=4,i.ROUND_HALF_DOWN=5,i.ROUND_HALF_EVEN=6,i.ROUND_HALF_CEIL=7,i.ROUND_HALF_FLOOR=8,i.clone=us,i.config=i.set=ds,e===void 0&&(e={}),e)for(r=[`precision`,`rounding`,`toExpNeg`,`toExpPos`,`LN10`],t=0;t=i[t+1]&&r<=i[t+2])this[n]=r;else throw Error(Bo+n+`: `+r);if((r=e[n=`LN10`])!==void 0)if(r==Math.LN10)this[n]=new this(r);else throw Error(Bo+n+`: `+r);return this}Go=new(us(Lo))(1);var Y;(function(e){e.AED=`aed`,e.AFN=`afn`,e.ALL=`all`,e.AMD=`amd`,e.ANG=`ang`,e.AOA=`aoa`,e.ARS=`ars`,e.AUD=`aud`,e.AWG=`awg`,e.AZN=`azn`,e.BAM=`bam`,e.BBD=`bbd`,e.BDT=`bdt`,e.BGN=`bgn`,e.BHD=`bhd`,e.BIF=`bif`,e.BMD=`bmd`,e.BND=`bnd`,e.BOB=`bob`,e.BOV=`bov`,e.BRL=`brl`,e.BSD=`bsd`,e.BTN=`btn`,e.BWP=`bwp`,e.BYN=`byn`,e.BYR=`byr`,e.BZD=`bzd`,e.CAD=`cad`,e.CDF=`cdf`,e.CHE=`che`,e.CHF=`chf`,e.CHW=`chw`,e.CLF=`clf`,e.CLP=`clp`,e.CNY=`cny`,e.COP=`cop`,e.COU=`cou`,e.CRC=`crc`,e.CUC=`cuc`,e.CUP=`cup`,e.CVE=`cve`,e.CZK=`czk`,e.DJF=`djf`,e.DKK=`dkk`,e.DOP=`dop`,e.DZD=`dzd`,e.EGP=`egp`,e.ERN=`ern`,e.ETB=`etb`,e.EUR=`eur`,e.FJD=`fjd`,e.FKP=`fkp`,e.GBP=`gbp`,e.GEL=`gel`,e.GHS=`ghs`,e.GIP=`gip`,e.GMD=`gmd`,e.GNF=`gnf`,e.GTQ=`gtq`,e.GYD=`gyd`,e.HKD=`hkd`,e.HNL=`hnl`,e.HRK=`hrk`,e.HTG=`htg`,e.HUF=`huf`,e.IDR=`idr`,e.ILS=`ils`,e.INR=`inr`,e.IQD=`iqd`,e.IRR=`irr`,e.ISK=`isk`,e.JMD=`jmd`,e.JOD=`jod`,e.JPY=`jpy`,e.KES=`kes`,e.KGS=`kgs`,e.KHR=`khr`,e.KMF=`kmf`,e.KPW=`kpw`,e.KRW=`krw`,e.KWD=`kwd`,e.KYD=`kyd`,e.KZT=`kzt`,e.LAK=`lak`,e.LBP=`lbp`,e.LKR=`lkr`,e.LRD=`lrd`,e.LSL=`lsl`,e.LTL=`ltl`,e.LVL=`lvl`,e.LYD=`lyd`,e.MAD=`mad`,e.MDL=`mdl`,e.MGA=`mga`,e.MKD=`mkd`,e.MMK=`mmk`,e.MNT=`mnt`,e.MOP=`mop`,e.MRO=`mro`,e.MUR=`mur`,e.MVR=`mvr`,e.MWK=`mwk`,e.MXN=`mxn`,e.MXV=`mxv`,e.MYR=`myr`,e.MZN=`mzn`,e.NAD=`nad`,e.NGN=`ngn`,e.NIO=`nio`,e.NOK=`nok`,e.NPR=`npr`,e.NZD=`nzd`,e.OMR=`omr`,e.PAB=`pab`,e.PEN=`pen`,e.PGK=`pgk`,e.PHP=`php`,e.PKR=`pkr`,e.PLN=`pln`,e.PYG=`pyg`,e.QAR=`qar`,e.RON=`ron`,e.RSD=`rsd`,e.RUB=`rub`,e.RWF=`rwf`,e.SAR=`sar`,e.SBD=`sbd`,e.SCR=`scr`,e.SDG=`sdg`,e.SEK=`sek`,e.SGD=`sgd`,e.SHP=`shp`,e.SLL=`sll`,e.SOS=`sos`,e.SRD=`srd`,e.SSP=`ssp`,e.STD=`std`,e.SVC=`svc`,e.SYP=`syp`,e.SZL=`szl`,e.THB=`thb`,e.TJS=`tjs`,e.TMT=`tmt`,e.TND=`tnd`,e.TOP=`top`,e.TRY=`try`,e.TTD=`ttd`,e.TWD=`twd`,e.TZS=`tzs`,e.UAH=`uah`,e.UGX=`ugx`,e.USD=`usd`,e.USN=`usn`,e.USS=`uss`,e.UYI=`uyi`,e.UYU=`uyu`,e.UZS=`uzs`,e.VEF=`vef`,e.VND=`vnd`,e.VUV=`vuv`,e.WST=`wst`,e.XAF=`xaf`,e.XAG=`xag`,e.XAU=`xau`,e.XBA=`xba`,e.XBB=`xbb`,e.XBC=`xbc`,e.XBD=`xbd`,e.XCD=`xcd`,e.XDR=`xdr`,e.XFU=`xfu`,e.XOF=`xof`,e.XPD=`xpd`,e.XPF=`xpf`,e.XPT=`xpt`,e.XSU=`xsu`,e.XTS=`xts`,e.XUA=`xua`,e.YER=`yer`,e.ZAR=`zar`,e.ZMW=`zmw`,e.ZWL=`zwl`})(Y||={}),Y.AED,Y.AFN,Y.ALL,Y.AMD,Y.ANG,Y.AOA,Y.ARS,Y.AUD,Y.AWG,Y.AZN,Y.BAM,Y.BBD,Y.BDT,Y.BGN,Y.BHD,Y.BIF,Y.BMD,Y.BND,Y.BOB,Y.BOV,Y.BRL,Y.BSD,Y.BTN,Y.BWP,Y.BYR,Y.BYN,Y.BZD,Y.CAD,Y.CDF,Y.CHE,Y.CHF,Y.CHW,Y.CLF,Y.CLP,Y.CNY,Y.COP,Y.COU,Y.CRC,Y.CUC,Y.CUP,Y.CVE,Y.CZK,Y.DJF,Y.DKK,Y.DOP,Y.DZD,Y.EGP,Y.ERN,Y.ETB,Y.EUR,Y.FJD,Y.FKP,Y.GBP,Y.GEL,Y.GHS,Y.GIP,Y.GMD,Y.GNF,Y.GTQ,Y.GYD,Y.HKD,Y.HNL,Y.HRK,Y.HTG,Y.HUF,Y.IDR,Y.ILS,Y.INR,Y.IQD,Y.IRR,Y.ISK,Y.JMD,Y.JOD,Y.JPY,Y.KES,Y.KGS,Y.KHR,Y.KMF,Y.KPW,Y.KRW,Y.KWD,Y.KYD,Y.KZT,Y.LAK,Y.LBP,Y.LKR,Y.LRD,Y.LSL,Y.LTL,Y.LVL,Y.LYD,Y.MAD,Y.MDL,Y.MGA,Y.MKD,Y.MMK,Y.MNT,Y.MOP,Y.MRO,Y.MUR,Y.MVR,Y.MWK,Y.MXN,Y.MXV,Y.MYR,Y.MZN,Y.NAD,Y.NGN,Y.NIO,Y.NOK,Y.NPR,Y.NZD,Y.OMR,Y.PAB,Y.PEN,Y.PGK,Y.PHP,Y.PKR,Y.PLN,Y.PYG,Y.QAR,Y.RON,Y.RSD,Y.RUB,Y.RWF,Y.SAR,Y.SBD,Y.SCR,Y.SDG,Y.SEK,Y.SGD,Y.SHP,Y.SLL,Y.SOS,Y.SRD,Y.SSP,Y.STD,Y.SVC,Y.SYP,Y.SZL,Y.THB,Y.TJS,Y.TMT,Y.TND,Y.TOP,Y.TRY,Y.TTD,Y.TWD,Y.TZS,Y.UAH,Y.UGX,Y.USD,Y.USN,Y.USS,Y.UYI,Y.UYU,Y.UZS,Y.VEF,Y.VND,Y.VUV,Y.WST,Y.XAF,Y.XAG,Y.XAU,Y.XBA,Y.XBB,Y.XBC,Y.XBD,Y.XCD,Y.XDR,Y.XFU,Y.XOF,Y.XPD,Y.XPF,Y.XPT,Y.XSU,Y.XTS,Y.XUA,Y.YER,Y.ZAR,Y.ZMW,Y.ZWL;var fs={exports:{}};fs.exports;var ps;function ms(){return ps?fs.exports:(ps=1,(function(e,t){var n=`__lodash_hash_undefined__`,r=9007199254740991,i=`[object Arguments]`,a=`[object Array]`,o=`[object Boolean]`,s=`[object Date]`,c=`[object Error]`,l=`[object Function]`,u=`[object Map]`,d=`[object Number]`,f=`[object Object]`,p=`[object Promise]`,m=`[object RegExp]`,h=`[object Set]`,g=`[object String]`,_=`[object Symbol]`,v=`[object WeakMap]`,y=`[object ArrayBuffer]`,b=`[object DataView]`,x=`[object Float32Array]`,S=`[object Float64Array]`,C=`[object Int8Array]`,w=`[object Int16Array]`,T=`[object Int32Array]`,E=`[object Uint8Array]`,D=`[object Uint8ClampedArray]`,O=`[object Uint16Array]`,ee=`[object Uint32Array]`,k=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,A=/^\w*$/,te=/^\./,j=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,M=/[\\^$.*+?()[\]{}|]/g,ne=/\\(\\)?/g,N=/^\[object .+?Constructor\]$/,P=/^(?:0|[1-9]\d*)$/,F={};F[x]=F[S]=F[C]=F[w]=F[T]=F[E]=F[D]=F[O]=F[ee]=!0,F[i]=F[a]=F[y]=F[o]=F[b]=F[s]=F[c]=F[l]=F[u]=F[d]=F[f]=F[m]=F[h]=F[g]=F[v]=!1;var re=typeof Ne==`object`&&Ne&&Ne.Object===Object&&Ne,ie=typeof self==`object`&&self&&self.Object===Object&&self,ae=re||ie||globalThis,I=t&&!t.nodeType&&t,L=I&&e&&!e.nodeType&&e,oe=L&&L.exports===I&&re.process,se=function(){try{return oe&&oe.binding(`util`)}catch{}}(),ce=se&&se.isTypedArray;function le(e,t){for(var n=-1,r=e?e.length:0;++n-1}function nt(e,t){var n=this.__data__,r=vt(n,e);return r<0?n.push([e,t]):n[r][1]=t,this}Ze.prototype.clear=Qe,Ze.prototype.delete=$e,Ze.prototype.get=et,Ze.prototype.has=tt,Ze.prototype.set=nt;function rt(e){var t=-1,n=e?e.length:0;for(this.clear();++ts))return!1;var l=a.get(e);if(l&&a.get(t))return l==t;var u=-1,d=!0,f=i&1?new ct:void 0;for(a.set(e,t),a.set(t,e);++u-1&&e%1==0&&e-1&&e%1==0&&e<=r}function dn(e){var t=typeof e;return!!e&&(t==`object`||t==`function`)}function fn(e){return!!e&&typeof e==`object`}function pn(e){return typeof e==`symbol`||fn(e)&&we.call(e)==_}var mn=ce?pe(ce):kt;function hn(e){return e==null?``:Ft(e)}function gn(e,t,n){var r=e==null?void 0:St(e,t);return r===void 0?n:r}function _n(e,t){return e!=null&&Gt(e,t,wt)}function vn(e){return sn(e)?_t(e):jt(e)}function yn(e,t,n){var r=on(e)||mn(e);if(t=At(t),n==null)if(r||dn(e)){var i=e.constructor;n=r?on(e)?new i:[]:ln(i)?yt(Oe(e)):{}}else n={};return(r?le:xt)(e,function(e,r,i){return t(n,e,r,i)}),n}function bn(e){return e}function xn(e){return qt(e)?de(en(e)):Pt(e)}e.exports=yn})(fs,fs.exports),fs.exports)}ms();var{Commands:hs}=ri,gs=[`identify`,`guilds.members.read`];function _s(e,t){if(e instanceof Ee)return e;if(typeof e==`object`&&e&&`code`in e){let n=e.code,r=Object.values(kr).filter(e=>typeof e==`number`);if(typeof n==`number`&&r.includes(n))return new Ee(`${t}_${n}`)}return new Ee(t)}var vs=15e3,ys=12e4,bs=15e3;function xs(e=window.location.search){let t=new URLSearchParams(e),n=t.getAll(`frame_id`),r=t.getAll(`platform`);if(n.length!==1||n[0]===void 0||n[0].length===0||n[0].length>255||r.length!==1||![`desktop`,`mobile`].includes(r[0]??``))throw new Ee(`invalid_launch`)}async function Ss(e,t,n,r,i){let a=null,o=new Promise((e,n)=>{t!==void 0&&(a=()=>n(new Ee(`cancelled`)),t.addEventListener(`abort`,a,{once:!0}),t.aborted&&a())}),s=null,c=new Promise((e,t)=>{s=setTimeout(()=>t(new Ee(r)),i)}),l=Promise.resolve().then(()=>{if(t?.aborted)throw new Ee(`cancelled`);return e()}).catch(e=>{throw _s(e,n)});try{return await Promise.race([l,o,c])}finally{s!==null&&clearTimeout(s),t!==void 0&&a!==null&&t.removeEventListener(`abort`,a)}}var Cs=class{makeSDK;sdk=null;authenticated=!1;closed=!1;constructor(e=e=>new Fo(e)){this.makeSDK=e}async authorize(e){let t=Oe();xs();let n=await Ae(t,e),r=this.open(n.client_id),i=()=>{if(e?.aborted)throw new Ee(`cancelled`)};try{if(i(),await Ss(()=>r.ready(),e,`sdk_ready_failed`,`sdk_ready_timeout`,vs),i(),r.instanceId!==t)throw new Ee(`instance_mismatch`);let a=await Ss(()=>r.commands.authorize({client_id:n.client_id,response_type:`code`,state:n.state,prompt:`none`,scope:[...gs]}),e,`authorize_failed`,`authorize_timeout`,ys);i();let o=await je(a.code,n.state,e);if(i(),!this.authenticated){if(await Ss(()=>r.commands.authenticate({access_token:o.access_token}),e,`sdk_authenticate_failed`,`sdk_authenticate_timeout`,bs)==null)throw new Ee(`sdk_authenticate_failed`);this.authenticated=!0,i()}let s=o.ticket;return{sdk:r,bootstrap:n,player:o.player,takeTicket:()=>{let e=s;return s=null,e},destroy:()=>{s=null}}}catch(t){throw e?.aborted?new Ee(`cancelled`):new Ee(_s(t,`authorization_failed`).code,!0)}}close(){if(!this.closed&&(this.closed=!0,this.sdk!==null))try{this.sdk.close(Or.CLOSE_NORMAL,`Hands session closed`)}catch{}}open(e){if(this.closed)throw new Ee(`cancelled`);if(this.sdk!==null)return this.sdk;try{this.sdk=this.makeSDK(e)}catch(e){throw new Ee(_s(e,`sdk_initialization_failed`).code,!0)}return this.sdk}},ws={invalid_launch:`open Hands from a Discord voice channel with Play now`,cancelled:`the session was cancelled`,authorization_failed:`Discord did not authorize the session`,authorize_failed:`Discord did not authorize the session`,authorize_timeout:`Discord took too long to authorize the session`,sdk_ready_failed:`the Discord client did not respond`,sdk_ready_timeout:`the Discord client took too long to respond`,sdk_authenticate_failed:`Discord did not confirm your identity`,sdk_authenticate_timeout:`Discord took too long to confirm your identity`,sdk_initialization_failed:`the Discord client could not start the activity`,instance_mismatch:`this window belongs to a different activity instance`,bootstrap_failed:`the Hands server could not be reached`,token_failed:`the Hands server rejected the sign-in`,invalid_bootstrap:`the Hands server sent an unexpected reply`,invalid_token_response:`the Hands server sent an unexpected reply`,ticket_unavailable:`the session ticket was already used`,protocol_error:`the server and client disagree on the protocol`,unexpected_error:`something unexpected went wrong`,authentication_timeout:`the connection took too long to authenticate`,authentication_required:`the connection was not authenticated`,invalid_ticket:`the session ticket was rejected`,invalid_request:`the server rejected the request`,invalid_input:`the server rejected an input frame`,input_queue_full:`the server could not keep up with the inputs`,rate_limited:`too many requests were sent; wait a moment`,service_busy:`the server is busy right now`,service_unavailable:`the server is unavailable right now`,upstream_unavailable:`Discord is unavailable right now`,room_closed:`the bout has already ended; use Play now to start another`,room_full:`both seats in this bout are taken`,already_in_room:`you are already in a bout in another channel`,match_not_started:`the bout has not started yet`,connection_replaced:`this seat was taken over by a newer connection`,spectator_read_only:`spectators cannot send input`,server_shutting_down:`the server is restarting`,internal_error:`the server hit an internal error`,not_in_activity:`you are not a participant of this activity`,invalid_activity:`this activity instance is not valid`,invalid_state:`the sign-in state expired; retry`,invalid_guild:`this activity belongs to another server`,persistence_failed:`the result could not be saved`,rematch_unavailable:`the ring could not be cleared for a rematch; use Play now to start another bout`,network_unavailable:`the connection to the Hands server could not be opened`,match_abandoned:`both fighters lost their connection and the bout was called off`};function Ts(e){if(e===`client_outdated`)return`Hands was updated. Reload to continue (client_outdated).`;let t=ws[e]??ws[e.replace(/_\d+$/u,``)];return t===void 0?`Unable to continue (${e}).`:`Unable to continue: ${t} (${e}).`}function Es(){try{if(window.matchMedia?.(`(pointer: coarse)`).matches)return!0}catch{}return(navigator.maxTouchPoints??0)>0&&new URLSearchParams(window.location.search).get(`platform`)===`mobile`}var Ds=[{label:`JAB`,punchClass:`jab`},{label:`STRAIGHT`,punchClass:`straight`},{label:`HOOK`,punchClass:`hook`},{label:`UPPER`,punchClass:`uppercut`}],Os=[{label:`◀ SLIP`,name:`Slip left`,kind:`slip_left`},{label:`WEAVE`,name:`Weave`,kind:`weave`},{label:`SLIP ▶`,name:`Slip right`,kind:`slip_right`},{label:`PULL`,name:`Pull`,kind:`pull`},{label:`CLINCH`,name:`Clinch`,kind:`clinch`}],ks=class{sharedActions;root;stickZone;stickBase;stickKnob;pads=[];moves=[];stickPointer=null;stickOrigin={x:0,y:0};moveX=0;moveY=0;body=!1;power=!1;guard=`none`;knockdown=!1;enabled=!0;heldPointers=new Map;constructor(e,t){this.sharedActions=t,this.root=document.createElement(`div`),this.root.className=`touch-controls`,this.root.setAttribute(`aria-label`,`Touch controls`),this.root.innerHTML=`
+var e=Object.defineProperty,t=(t,n)=>{let r={};for(var i in t)e(r,i,{get:t[i],enumerable:!0});return n||e(r,Symbol.toStringTag,{value:`Module`}),r};(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var n={format:2,tick_rate:30,ring:{half_width:500,half_height:500,fighter_radius:38},limits:{max_stamina:1e3,max_conditioning:1e3,max_guard:700,max_poise:600},action_buffer_ticks:6,facing:{scale:1e3,turn_degrees_per_second:75},combos:{recovery_cancel_percent:50,chains:[[`jab`,`straight`],[`jab`,`hook`],[`straight`,`hook`],[`hook`,`uppercut`],[`uppercut`,`hook`]]},knockdown:{referee_walk_speed:5,mandatory_count:8,box_pause_ticks:60,get_up_stun_ticks:20,get_up_stamina:500,get_up_base:34,get_up_per_knockdown:20,get_up_trauma_divisor:18},countdown:{opening_ticks:240},tired:{startup_ticks:3,recovery_ticks:5,impact_percent:40},foul:{separation:45},hitstop_ms:{hit:78,block:45},fatigue_scaling:{startup_formula:`max(2, startup * 100 // fatigue - lead_jab_bonus)`,recovery_formula:`max(4, recovery * 100 // fatigue)`,lead_jab_bonus:1,rear_straight_impact_bonus:8},punches:{jab:{startup:4,active:2,recovery:7,reach:150,lateral_arc:42,impact:34,stamina_cost:42,whiff_cost:18,guard_damage:44,poise_damage:35,combo_window:7,startup_vulnerability:2,recovery_vulnerability:3},straight:{startup:6,active:2,recovery:10,reach:168,lateral_arc:38,impact:52,stamina_cost:65,whiff_cost:28,guard_damage:66,poise_damage:58,combo_window:8,startup_vulnerability:4,recovery_vulnerability:5},hook:{startup:7,active:3,recovery:12,reach:126,lateral_arc:92,impact:64,stamina_cost:78,whiff_cost:36,guard_damage:82,poise_damage:72,combo_window:9,startup_vulnerability:5,recovery_vulnerability:7},uppercut:{startup:8,active:2,recovery:13,reach:108,lateral_arc:54,impact:72,stamina_cost:88,whiff_cost:44,guard_damage:88,poise_damage:84,combo_window:10,startup_vulnerability:6,recovery_vulnerability:8}},variants:{body:{startup_add:1,reach_min:80,reach_sub:14,lateral_arc_add:8,impact_mul_num:92,impact_mul_den:100,stamina_cost_add:4,whiff_cost_add:3,poise_damage_mul_num:72,poise_damage_mul_den:100},power:{startup_add:2,active_add:1,recovery_add:3,reach_add:5,impact_mul_num:152,impact_mul_den:100,stamina_cost_mul_num:155,stamina_cost_mul_den:100,whiff_cost_mul_num:175,whiff_cost_mul_den:100,guard_damage_mul_num:145,guard_damage_mul_den:100,poise_damage_mul_num:145,poise_damage_mul_den:100,startup_vulnerability_add:2,recovery_vulnerability_add:2}},hurtboxes:{head:{radius:.13,length:.16,joint:`head`,offset_y:.12},torso:{radius:.21,length:.34,joint:`chest`,offset_y:.16}},corners:{reach:733},rest:{corner_offset:365,walk_speed:5},clinch:{hold_distance:60,draw_speed:6},corner:{cut:{worse_cut:250,other_cut:100,bleeding_kept_percent:30},swelling:{swelling:250,eyes:200},breath:{conditioning:180,body:150},balanced:{worse_cut:80,bleeding_kept_percent:75,swelling:80,eyes:60}},blind_side:{eye_threshold:700,impact_percent:120},doctor:{cut:800,swelling:900},parry:{stagger_ticks:20},body:{wind_percent:90,collapse_body_trauma:650,collapse_stamina:350,collapse_minimum_damage:40,collapse_delay_ticks:10,collapse_cooldown_ticks:300},styles:{balanced:{},boxer:{recovery_ticks:{jab:-1},reach_percent:102,impact_percent:98,poise_damage_percent:98},slugger:{impact_percent:108,poise_damage_percent:108,poise_taken_percent:95,stamina_cost_percent:115,recovery_ticks:{hook:1,uppercut:1},move_speed_percent:96},swarmer:{move_speed_percent:103,startup_ticks:{hook:-1,uppercut:-1},body_damage_percent:110,conditioning_loss_percent:85,impact_percent:96,poise_damage_percent:96,evasion_ticks:1},counter_puncher:{counter_bonus_percent:38,counter_window_ticks:3,evasion_ticks:1,perfect_block_ticks:1,impact_percent:96,poise_damage_percent:96}},stun:{flinch_minimum_damage:36,flinch_base_ticks:5,flinch_damage_divisor:16,rocked_counter_damage:50,rocked_power_damage:70,rocked_hurt_poise:180,rocked_base_ticks:16,rocked_damage_divisor:6,rocked_max_ticks:36,chain_max_ticks:45,immunity_ticks:24,rocked_immunity_ticks:8,moving_speed_percent:75},guard:{block_poise_percent:25,perfect_block_poise_percent:0,leak_base_percent:70,leak_minimum_percent:10,block_minimum:60,perfect_rearm_ticks:6,held_regen_every_ticks:4,damage_percent:60,stamina_regen_percent:50},trauma:{head_per_damage_percent:100,eye_per_damage_percent:70,cut_per_damage_percent:50,jab_cut_percent:25,jab_swelling_percent:50,swelling_per_damage_percent:33,poise_ceiling_per_head_percent:30,poise_ceiling_floor:220,body_per_damage_percent:100,poise_regen_every_ticks:2,poise_damage_percent:100},hitbox:{glove_radius:.11}},r=n.punches,i=n.variants;n.tick_rate,n.action_buffer_ticks,n.hitstop_ms;var a=n.hurtboxes,o=n.hitbox.glove_radius;n.fatigue_scaling;var s=n.rest.corner_offset,c=n.ring.half_width,l=n.ring.half_height,u=n.ring.fighter_radius,d=n.blind_side.eye_threshold,f=n.guard.block_minimum,p=n.corner,m=n.corners.reach,h=n.combos.recovery_cancel_percent,g=new Set(n.combos.chains.map(([e,t])=>`${e}:${t}`));function _(e,t){return g.has(`${e}:${t}`)}function v(e){return e.startup+e.active+Math.floor(e.recovery*h/100)}function y(e,t,n,r,i,a=`balanced`){return t&&!i&&_(e,n.class)&&r>=j(a,n.class,n.target,n.power)}function b(e){return r[e].combo_window}var x=n.stun.rocked_base_ticks,S=n.stun.rocked_max_ticks,C=n.knockdown,w=1400,T=3,E=C.get_up_base,D=C.get_up_base+T*C.get_up_per_knockdown+Math.floor(w/C.get_up_trauma_divisor),O=n.stun.moving_speed_percent,ee=n.tired.startup_ticks,te=n.tired.recovery_ticks,k=n.doctor.cut,ne=n.doctor.swelling;function A(e,t,n){let a=r[e],o=a.startup,s=a.active,c=a.recovery,l=a.reach,u=a.lateral_arc;if(t===`body`){let e=i.body;o+=e.startup_add??0,c+=e.recovery_add??0,l=Math.max(e.reach_min??0,l-(e.reach_sub??0)),u+=e.lateral_arc_add??0}if(n===`power`){let e=i.power;o+=e.startup_add??0,s+=e.active_add??0,c+=e.recovery_add??0,l+=e.reach_add??0}return{startup:o,active:s,recovery:c,reach:l,lateralArc:u}}function re(e,t,n){let a=r[e].stamina_cost;return t===`body`&&(a+=i.body.stamina_cost_add??0),n===`power`&&(a=Math.floor(a*(i.power.stamina_cost_mul_num??1)/(i.power.stamina_cost_mul_den??1))),a}var ie=n.styles,ae=new Map;function oe(e){let t=ae.get(e);if(t!==void 0)return t;let n=ie[e]??{},r={startupTicks:n.startup_ticks??{},recoveryTicks:n.recovery_ticks??{},staminaCostPercent:n.stamina_cost_percent??100,conditioningLossPercent:n.conditioning_loss_percent??100,moveSpeedPercent:n.move_speed_percent??100,evasionTicks:n.evasion_ticks??0};return ae.set(e,r),r}function j(e,t,n,r){return Math.floor(re(t,n,r)*oe(e).staminaCostPercent/100)}function se(e){return e.startup+e.active+e.recovery}function ce(e){return`${e.wins}-${e.losses}${e.draws>0?`-${e.draws}`:``}`}var le=e=>e.wins+e.losses+e.draws===0;function ue(e){return le(e)?`PRO DEBUT`:`${ce(e)} (${e.knockouts} KO)`}var M=class extends Error{name=`ProtocolError`},N=2147483647,de=new TextEncoder;function fe(e){let t=0,n=()=>{for(;/\s/u.test(e[t]??``);)t+=1},r=()=>{let n=t;if(e[t++]!==`"`)throw new M(`invalid JSON`);for(;t{n();let a=e[t];if(a===`{`){t+=1,n();let a=new Set;if(e[t]===`}`){t+=1;return}for(;;){n();let o=r();if(a.has(o))throw new M(`duplicate field ${o}`);if(a.add(o),n(),e[t++]!==`:`)throw new M(`invalid JSON`);i(),n();let s=e[t++];if(s===`}`)return;if(s!==`,`)throw new M(`invalid JSON`)}}if(a===`[`){if(t+=1,n(),e[t]===`]`){t+=1;return}for(;;){i(),n();let r=e[t++];if(r===`]`)return;if(r!==`,`)throw new M(`invalid JSON`)}}if(a===`"`){r();return}let o=e.slice(t),s=/^(?:-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)/u.exec(o);if(!s)throw new M(`invalid JSON`);t+=s[0].length};if(i(),n(),t!==e.length)throw new M(`invalid JSON`)}function pe(e){let t,n;if(typeof e==`string`?(t=de.encode(e),n=e):(t=e instanceof Uint8Array?e:new Uint8Array(e),n=new TextDecoder(`utf-8`,{fatal:!0}).decode(t)),t.byteLength>65536)throw new M(`server frame too large`);try{return fe(n),JSON.parse(n)}catch(e){throw e instanceof M?e:new M(`invalid JSON`)}}function me(e,t){if(typeof e!=`object`||!e||Array.isArray(e)||Object.getPrototypeOf(e)!==Object.prototype)throw new M(`${t} must be an object`);return e}function P(e,t,n=[]){let r=new Set([...t,...n]),i=Object.keys(e);if(t.some(t=>!Object.hasOwn(e,t))||i.some(e=>!r.has(e)))throw new M(`schema mismatch`)}function F(e,t,n=0,r=N){if(!Number.isSafeInteger(e)||er)throw new M(`${t} must be a bounded integer`);return e}function he(e,t){if(typeof e!=`boolean`)throw new M(`${t} must be boolean`);return e}function ge(e,t,n=255,r=1){if(typeof e!=`string`||e.lengthn||/[\u0000-\u001f\u007f]/u.test(e))throw new M(`${t} must be a bounded string`);return e}function _e(e,t,n=255){return e===null?null:ge(e,t,n)}function ve(e,t,n){if(typeof e!=`string`||!t.includes(e))throw new M(`invalid ${n}`);return e}function ye(e,t,n){if(!Array.isArray(e)||e.length>n)throw new M(`${t} must be a bounded array`);return e}var be=[`left`,`right`],xe=[`jab`,`straight`,`hook`,`uppercut`],I=[`head`,`body`],Se=[`normal`,`power`],Ce=[`orthodox`,`southpaw`],we=[`balanced`,`boxer`,`slugger`,`swarmer`,`counter_puncher`],L=[`none`,`guard_high`,`guard_low`,`slip_left`,`slip_right`,`weave`,`pull`],Te=[`none`,`guard_high`,`guard_low`],Ee=[`slip_left`,`slip_right`,`weave`,`pull`,`clinch`,`switch_stance`,`get_up_left`,`get_up_right`,`taunt`,`corner_cut`,`corner_swelling`,`corner_breath`],De=[`cut`,`swelling`,`breath`,`balanced`],Oe=[`low_blow`,`headbutt`],ke=[`countdown`,`fight`,`knockdown`,`foul_recovery`,`rest`,`complete`],Ae=[`ko`,`flash_ko`,`tko`,`doctor_stoppage`,`disqualification`,`decision`,`draw`,`forfeit`];function je(e){let t=me(e,`record`);return P(t,[`wins`,`losses`,`draws`,`knockouts`]),{wins:F(t.wins,`record.wins`),losses:F(t.losses,`record.losses`),draws:F(t.draws,`record.draws`),knockouts:F(t.knockouts,`record.knockouts`)}}function Me(e){let t=me(e,`player`);P(t,[`id`,`name`,`avatar`,`rating`,`connected`],[`cpu`,`record`,`style`]);let n=t.style===void 0?{}:{style:ve(t.style,we,`player style`)},r=t.cpu===void 0?{}:{cpu:he(t.cpu,`cpu`)},i=t.record===void 0?{}:{record:je(t.record)};return{id:ge(t.id,`player.id`),name:ge(t.name,`player.name`,80),avatar:_e(t.avatar,`player.avatar`,128),rating:F(t.rating,`rating`),connected:he(t.connected,`connected`),...r,...i,...n}}function Ne(e,t,n=1){let r=ye(e,`players`,2).map(Me);if(r.lengthe.id)).size!==r.length)throw new M(`players must be present and distinct`);return r}function Pe(e){let t=me(e,`trauma`);return P(t,[`head`,`body`,`left_eye`,`right_eye`,`left_cut`,`right_cut`,`swelling`,`bleeding`]),{head:F(t.head,`head`,0,1400),body:F(t.body,`body`,0,1200),left_eye:F(t.left_eye,`left_eye`,0,1e3),right_eye:F(t.right_eye,`right_eye`,0,1e3),left_cut:F(t.left_cut,`left_cut`,0,1e3),right_cut:F(t.right_cut,`right_cut`,0,1e3),swelling:F(t.swelling,`swelling`,0,1e3),bleeding:F(t.bleeding,`bleeding`,0,1e3)}}var Fe=7;function Ie(e,t){return Math.max(-7,Math.min(Fe,F(e,t,-2147483647,N)))}function Le(e){let t=me(e,`fighter`);P(t,`player_id.x.y.facing.facing_x.facing_y.velocity_x.velocity_y.stance.style.defense.stamina.maximum_stamina.conditioning.guard.poise.trauma.knockdowns.warnings.deductions.stunned_ticks.is_downed.action.action_hand.action_target.action_power.action_id.action_key.action_start_tick.action_startup_ticks.action_active_ticks.action_recovery_ticks.action_contact_tick.queued_actions.clinch_startup_ticks.clinch_ticks.is_foul_recovery_target.taunt_ticks.corner_choice.get_up_prompt.get_up_meter.get_up_required.get_up_count.get_up_window_start_tick.get_up_window_end_tick.last_input_sequence`.split(`.`));let n=t.action===null?null:ve(t.action,xe,`action`),r=t.action_hand===null?null:ve(t.action_hand,be,`action hand`),i=t.action_target===null?null:ve(t.action_target,I,`action target`),a=t.action_power===null?null:ve(t.action_power,Se,`action power`);if([n,r,i,a].some(e=>e===null)&&[n,r,i,a].some(e=>e!==null))throw new M(`partial punch action`);let o=t.action_id===null?null:ge(t.action_id,`action_id`,32),s=t.action_key===null?null:ge(t.action_key,`action_key`,64);if([o,s].some(e=>e===null)!==[o,s].every(e=>e===null))throw new M(`partial action instance`);if(o!==null&&n===null)throw new M(`action instance without class`);let c=t.get_up_prompt===null?null:ve(t.get_up_prompt,[`get_up_left`,`get_up_right`],`get-up prompt`),l=F(t.maximum_stamina,`maximum_stamina`,330,1e3),u=F(t.stamina,`stamina`,0,l),d=F(t.get_up_required,`get_up_required`,0,D),f=F(t.facing,`facing`,-1,1);if(f===0)throw new M(`invalid facing`);let p=F(t.facing_x,`facing_x`,-1e3,1e3),m=F(t.facing_y,`facing_y`,-1e3,1e3);if(p===0&&m===0)throw new M(`invalid facing vector`);if(d!==0&&dye(e,`round scores`,15).map(e=>F(e,`score`,6,10)),r=n(t.player_one),i=n(t.player_two);if(r.length!==i.length)throw new M(`scorecard round mismatch`);return{judge:ge(t.judge,`judge`,80),player_one:r,player_two:i}}function Be(e){let t=ye(e,`scorecards`,3).map(ze);if(t.length!==3)throw new M(`exactly three scorecards required`);return t}function Ve(e,t){if(t===`draw`!=(e===null))throw new M(`incoherent finish result`)}function He(e){let t=me(e,`result`);P(t,[`match_id`,`activity_instance_id`,`guild_id`,`player_one_id`,`player_two_id`,`winner_id`,`finish_method`,`round_number`,`tick`,`scorecards`,`player_one_knockdowns`,`player_two_knockdowns`,`player_one_damage`,`player_two_damage`]);let n=ge(t.player_one_id,`player_one_id`),r=ge(t.player_two_id,`player_two_id`);if(n===r)throw new M(`result players must be distinct`);let i=_e(t.winner_id,`winner_id`);if(i!==null&&i!==n&&i!==r)throw new M(`invalid winner`);let a=ve(t.finish_method,Ae,`finish method`);return Ve(i,a),{match_id:ge(t.match_id,`match_id`),activity_instance_id:ge(t.activity_instance_id,`activity_instance_id`),guild_id:ge(t.guild_id,`guild_id`),player_one_id:n,player_two_id:r,winner_id:i,finish_method:a,round_number:F(t.round_number,`round`,1,15),tick:F(t.tick,`tick`),scorecards:Be(t.scorecards),player_one_knockdowns:F(t.player_one_knockdowns,`knockdowns`,0,3),player_two_knockdowns:F(t.player_two_knockdowns,`knockdowns`,0,3),player_one_damage:F(t.player_one_damage,`damage`),player_two_damage:F(t.player_two_damage,`damage`)}}function Ue(e){let t=me(e,`snapshot`);P(t,[`tick`,`phase`,`round_number`,`phase_ticks_remaining`,`fighters`,`events`,`result`,`checksum`]);let n=ye(t.fighters,`fighters`,2).map(Le);if(n.length!==2||n[0]?.player_id===n[1]?.player_id)throw new M(`snapshot requires two distinct fighters`);let r=ge(t.checksum,`checksum`,64);if(!/^[a-f0-9]{64}$/u.test(r))throw new M(`invalid checksum`);return{tick:F(t.tick,`tick`),phase:ve(t.phase,ke,`phase`),round_number:F(t.round_number,`round`,1,15),phase_ticks_remaining:F(t.phase_ticks_remaining,`phase ticks`),fighters:[n[0],n[1]],events:ye(t.events,`events`,256).map(Re),result:t.result===null?null:He(t.result),checksum:r}}function We(e){let t=me(pe(e),`envelope`);if(t.version!==4)throw new M(`unsupported protocol version`);let n=ge(t.type,`type`,32);if(n===`welcome`){let e=ve(t.role,[`fighter`,`spectator`],`connection role`),r=ge(t.player_id,`player_id`),i=t.reconnect_ticket===void 0?{}:{reconnect_ticket:ge(t.reconnect_ticket,`reconnect_ticket`,4096)};if(e===`fighter`){P(t,[`version`,`type`,`role`,`player_id`,`seat`,`rating`,`players`,`server_tick`,`next_sequence`],[`reconnect_ticket`]);let a=Ne(t.players);if(!a.some(e=>e.id===r))throw new M(`welcome fighter is absent`);return{version:4,type:n,role:e,player_id:r,seat:F(t.seat,`seat`,1,2),rating:F(t.rating,`rating`),players:a,server_tick:F(t.server_tick,`server_tick`),next_sequence:F(t.next_sequence,`next_sequence`),...i}}P(t,[`version`,`type`,`role`,`player_id`,`players`,`server_tick`],[`reconnect_ticket`]);let a=Ne(t.players,void 0,0);if(a.some(e=>e.id===r))throw new M(`spectator cannot be a fighter`);return{version:4,type:n,role:e,player_id:r,players:a,server_tick:F(t.server_tick,`server_tick`),...i}}if(n===`ticket`)return P(t,[`version`,`type`,`reconnect_ticket`,`refresh_id`]),{version:4,type:n,reconnect_ticket:ge(t.reconnect_ticket,`reconnect_ticket`,4096),refresh_id:ge(t.refresh_id,`refresh_id`,128,16)};if(n===`waiting`){if(P(t,[`version`,`type`,`open_seats`]),t.open_seats!==1&&t.open_seats!==2)throw new M(`invalid open seats`);return{version:4,type:n,open_seats:t.open_seats}}if(n===`select`){P(t,[`version`,`type`,`deadline_ms`,`players`,`ready`]);let e=Ne(t.players,2),r=ye(t.ready,`ready`,2).map(e=>ge(e,`ready player id`));if(new Set(r).size!==r.length||r.some(t=>!e.some(e=>e.id===t)))throw new M(`invalid ready fighters`);return{version:4,type:n,deadline_ms:F(t.deadline_ms,`deadline_ms`,0,6e4),players:[e[0],e[1]],ready:r}}if(n===`ready`){P(t,[`version`,`type`,`players`]);let e=Ne(t.players,2);return{version:4,type:n,players:[e[0],e[1]]}}if(n===`paused`)return P(t,[`version`,`type`,`player_id`,`grace_ms`]),{version:4,type:n,player_id:ge(t.player_id,`player_id`),grace_ms:F(t.grace_ms,`grace_ms`,0,6e4)};if(n===`resumed`)return P(t,[`version`,`type`,`player_id`]),{version:4,type:n,player_id:ge(t.player_id,`player_id`)};if(n===`snapshot`)return P(t,[`version`,`type`,`payload`]),{version:4,type:n,payload:Ue(t.payload)};if(n===`error`)return P(t,[`version`,`type`,`code`]),{version:4,type:n,code:ge(t.code,`error code`,80)};if(n===`final`)return Ke(t,n);throw new M(`unsupported server message type`)}function Ge(e,t){let n=Object.entries(me(e,`punches`));if(n.length!==2||n.some(([e])=>!t.includes(e)))throw new M(`punches must count both fighters`);let r=Object.create(null);for(let[e,t]of n){let n=me(t,`punch totals`);P(n,[`thrown`,`landed`,`jabs_thrown`,`jabs_landed`]);let i=F(n.thrown,`thrown`),a=F(n.landed,`landed`,0,i),o=F(n.jabs_thrown,`jabs_thrown`,0,i);r[e]={thrown:i,landed:a,jabs_thrown:o,jabs_landed:F(n.jabs_landed,`jabs_landed`,0,Math.min(a,o))}}return r}function Ke(e,t){P(e,[`version`,`type`,`match_id`,`winner_id`,`method`,`round`,`scorecards`,`ratings`],[`punches`]);let n=me(e.ratings,`ratings`),r=Object.entries(n);if(r.length!==2)throw new M(`final requires two ratings`);let i=Object.create(null);for(let[e,t]of r){ge(e,`rating player id`);let n=me(t,`rating`);P(n,[`before`,`after`]),i[e]={before:F(n.before,`before`),after:F(n.after,`after`)}}let a=_e(e.winner_id,`winner_id`);if(a!==null&&!Object.hasOwn(i,a))throw new M(`winner absent from ratings`);let o=ve(e.method,Ae,`finish method`);Ve(a,o);let s=e.punches===void 0?{}:{punches:Ge(e.punches,Object.keys(i))};return{version:4,type:t,match_id:ge(e.match_id,`match_id`),winner_id:a,method:o,round:F(e.round,`round`,1,15),scorecards:Be(e.scorecards),ratings:i,...s}}function qe(e,t,n){return JSON.stringify({version:4,type:`input`,sequence:F(e,`sequence`),client_tick:F(t,`client tick`),move:{x:Math.max(-1e3,Math.min(1e3,Math.round(Number.isFinite(n.moveX)?n.moveX:0))),y:Math.max(-1e3,Math.min(1e3,Math.round(Number.isFinite(n.moveY)?n.moveY:0)))},defense:ve(n.defense,Te,`held defense`),actions:n.actions.slice(0,4).map(e=>{let t=e.id===void 0?{}:{id:e.id.slice(0,32)};return e.kind===`punch`?{kind:`punch`,hand:ve(e.hand,be,`hand`),class:ve(e.class,xe,`class`),target:ve(e.target,I,`target`),power:ve(e.power,Se,`power`),...t}:e.kind===`foul`?{kind:`foul`,foul:ve(e.foul,Oe,`foul`),...t}:{kind:ve(e.kind,Ee,`action`),...t}})})}function Je(e,t){return JSON.stringify({version:4,type:`style`,style:ve(e,we,`fighter style`),ready:t===!0})}var Ye=[`rookie`,`contender`,`champion`];function Xe(e){return JSON.stringify({version:4,type:`cpu`,level:ve(e,Ye,`computer level`)})}function Ze(e){let t=me(e,`bootstrap`);if(P(t,[`client_id`,`state`,`protocol`,`simulation`]),t.protocol!==4)throw new M(`unsupported protocol version`);let n=me(t.simulation,`simulation`);return P(n,[`tick_rate`,`ring_half_width`,`ring_half_height`]),{client_id:ge(t.client_id,`client_id`,36),state:ge(t.state,`state`,128),protocol:4,simulation:{tick_rate:F(n.tick_rate,`tick rate`,1,120),ring_half_width:F(n.ring_half_width,`ring width`,1),ring_half_height:F(n.ring_half_height,`ring height`,1)}}}function Qe(e){let t=me(e,`token response`);P(t,[`access_token`,`ticket`,`player`]);let n=me(t.player,`player`);return P(n,[`id`,`name`,`avatar`,`rating`]),{access_token:ge(t.access_token,`access token`,4096),ticket:ge(t.ticket,`ticket`,4096),player:{id:ge(n.id,`id`),name:ge(n.name,`name`,80),avatar:_e(n.avatar,`avatar`,128),rating:F(n.rating,`rating`)}}}var $e=[`Power`,`Hand speed`,`Footwork`,`Reach`,`Stamina`,`Defence`],et=[{style:`balanced`,name:`Balanced`,tag:`BALANCED`,detail:`Even at everything, no holes to exploit.`,stats:[3,3,3,3,3,3]},{style:`boxer`,name:`Boxer`,tag:`BOXER`,detail:`Snappy jab and long reach, lighter hands.`,stats:[2,4,3,4,3,3]},{style:`slugger`,name:`Slugger`,tag:`SLUGGER`,detail:`Heavy hands, hard chin, slow feet, tires sooner.`,stats:[5,2,2,3,2,4]},{style:`swarmer`,name:`Swarmer`,tag:`SWARMER`,detail:`Quick feet and head, digs to the body, never stops.`,stats:[2,4,4,3,4,3]},{style:`counter_puncher`,name:`Counter-puncher`,tag:`COUNTER`,detail:`Slips and parries, makes every miss cost.`,stats:[2,3,3,3,3,5]}],tt=e=>et.find(t=>t.style===e)??et[0];function nt(e){return e===void 0||e===`balanced`?null:tt(e).tag}function rt(e){return e===void 0||e===`balanced`?null:`the ${tt(e).name.toLowerCase()}`}var it=`hands.style.v1`;function at(e=st()){try{let t=e?.getItem(it);return we.find(e=>e===t)??`balanced`}catch{return`balanced`}}function ot(e,t=st()){try{t?.setItem(it,e)}catch{}}function st(){try{return window.localStorage}catch{return null}}var ct=400,lt={Digit1:0,Digit2:1,Digit3:2,Digit4:3,Digit5:4,Numpad1:0,Numpad2:1,Numpad3:2,Numpad4:3,Numpad5:4},ut=.6;function dt(){try{return[...navigator.getGamepads?.()??[]].find(e=>e!==null&&e.connected&&e.mapping===`standard`)??null}catch{return null}}function ft(e){let t=new Set;if(e===null)return t;let n=t=>e.buttons[t]?.pressed===!0,r=e.axes[0]??0,i=e.axes[1]??0;return(n(12)||n(14)||r<-.6||i<-.6)&&t.add(`previous`),(n(13)||n(15)||r>ut||i>ut)&&t.add(`next`),n(0)&&t.add(`settle`),t}var pt=class{send;now;element;buttons=new Map;clock;status;visible=!1;choosing=!1;highlighted;settled=!1;deadlineAt=0;timer=null;padFrame=0;padHeld=new Set;context=null;keydown=e=>{if(!this.visible||!this.choosing||e.repeat||e.ctrlKey||e.metaKey||e.altKey)return;let t=e.target;if(t instanceof HTMLElement&&t!==document.body&&!this.element.contains(t))return;let n=lt[e.code];n===void 0?e.code===`ArrowRight`||e.code===`ArrowDown`?(e.preventDefault(),this.move(this.neighbour(1))):e.code===`ArrowLeft`||e.code===`ArrowUp`?(e.preventDefault(),this.move(this.neighbour(-1))):(e.code===`Enter`||e.code===`Space`)&&!(t instanceof HTMLButtonElement)&&(e.preventDefault(),this.pick(this.highlighted,!0)):(e.preventDefault(),this.pick(et[n].style,!0))};constructor(e,t,n=()=>performance.now(),r=at()){this.send=t,this.now=n,this.highlighted=r;let i=document.createElement(`section`);i.className=`style-picker`,i.dataset.stylePicker=``,i.hidden=!0,i.setAttribute(`aria-label`,`Pick how your fighter boxes`);let a=document.createElement(`header`),o=document.createElement(`p`);o.className=`style-title`,o.textContent=`CHOOSE YOUR STYLE`,this.clock=document.createElement(`p`),this.clock.className=`style-clock`,a.append(o,this.clock);let s=document.createElement(`div`);s.className=`style-cards`;for(let[e,t]of et.entries()){let n=document.createElement(`button`);n.type=`button`,n.dataset.style=t.style,n.setAttribute(`aria-pressed`,`false`);let r=document.createElement(`kbd`);r.textContent=String(e+1);let i=document.createElement(`strong`);i.textContent=t.name;let a=document.createElement(`span`);a.textContent=t.detail;let o=document.createElement(`ul`);o.className=`style-stats`;for(let[e,n]of t.stats.entries()){let t=document.createElement(`li`);t.setAttribute(`aria-label`,`${$e[e]} ${n} of 5`);let r=document.createElement(`small`);r.textContent=$e[e];let i=document.createElement(`i`);i.dataset.value=String(n),t.append(r,i),o.append(t)}n.append(r,i,a,o),n.addEventListener(`click`,()=>this.pick(t.style,!0)),s.append(n),this.buttons.set(t.style,n)}this.status=document.createElement(`p`),this.status.className=`style-status`,this.status.setAttribute(`role`,`status`),i.append(a,s,this.status),e.append(i),this.element=i,window.addEventListener(`keydown`,this.keydown)}update(e,t,n){if(e===null){this.setVisible(!1),this.context=null,this.settled=!1;return}let r=this.context===null||!this.choosing&&n===`fighter`;this.context?.select!==e&&(this.deadlineAt=this.now()+e.deadline_ms-ct),this.context={select:e,viewerId:t},this.choosing=n===`fighter`&&t!==null,this.settled=this.choosing&&(this.settled||e.ready.includes(t));let i=e.players.find(e=>e.id===t)?.style;this.settled&&i!==void 0&&(this.highlighted=i),r&&this.choosing&&!this.settled&&this.send(this.highlighted,!1),this.setVisible(!0),this.render()}pick(e,t){!this.choosing||this.settled||this.closed||(this.highlighted=e,this.send(e,t)&&(t&&(this.settled=!0),this.render()))}get closed(){return this.now()>=this.deadlineAt}neighbour(e){return et[(et.findIndex(e=>e.style===this.highlighted)+e+et.length)%et.length].style}move(e){this.pick(e,!1),this.settled||this.buttons.get(this.highlighted)?.focus()}render(){let e=this.context;if(e===null)return;let{select:t,viewerId:n}=e;for(let e of et){let t=this.buttons.get(e.style),n=this.choosing&&e.style===this.highlighted;t.disabled=!this.choosing||this.settled||this.closed,t.setAttribute(`aria-pressed`,String(n)),t.toggleAttribute(`data-chosen`,n),t.tabIndex=e.style===this.highlighted?0:-1}let r=`${Math.max(0,Math.ceil((this.deadlineAt-this.now())/1e3))}s`;this.clock.textContent!==r&&(this.clock.textContent=r);let i=t.players.filter(e=>e.id!==n&&(!e.connected||t.ready.includes(e.id))).map(e=>e.connected?`${e.name} is ready.`:`Waiting for ${e.name} to reconnect.`).join(` `),a=this.choosing?this.settled?i.length>0?`Ready as ${tt(this.highlighted).name}. ${i}`:`Ready as ${tt(this.highlighted).name}. Waiting for your opponent.`:i.length>0?`${i} Tap a style or press Enter to settle on yours.`:`Tap a style, or use the arrow keys and Enter.`:i.length>0?`The fighters are choosing. ${i}`:`The fighters are choosing their styles.`;this.status.textContent!==a&&(this.status.textContent=a)}setVisible(e){this.visible!==e&&(this.visible=e,this.element.hidden=!e,e?(this.timer=window.setInterval(()=>this.render(),250),this.padHeld=ft(dt()),this.padFrame=requestAnimationFrame(()=>this.pollPad())):(this.timer!==null&&window.clearInterval(this.timer),this.timer=null,cancelAnimationFrame(this.padFrame)))}pollPad(){if(!this.visible)return;let e=ft(dt());for(let t of e)this.padHeld.has(t)||(t===`settle`?this.pick(this.highlighted,!0):this.move(this.neighbour(t===`next`?1:-1)));this.padHeld=e,this.padFrame=requestAnimationFrame(()=>this.pollPad())}destroy(){this.setVisible(!1),window.removeEventListener(`keydown`,this.keydown),this.element.remove()}},mt=/^[0-9]{17,20}$/u,ht=/^[A-Za-z0-9_]{1,128}$/u,gt=`https://cdn.discordapp.com/avatars/`,_t=128,vt=8;function yt(e){return e.avatar===null||!mt.test(e.id)||!ht.test(e.avatar)?null:new URL(`${e.id}/${e.avatar}.png?size=${_t}`,gt).href}function bt(e){let t=Array.from(e).find(e=>/[\p{L}\p{N}]/u.test(e));return t===void 0?`?`:t.toUpperCase()}var xt=class{create;pictures=new Map;addresses=new Map;constructor(e=()=>new Image){this.create=e}get(e){if(e===void 0)return null;let t=this.addresses.get(e.id);(t===void 0||t.avatar!==e.avatar)&&(this.addresses.size>=vt&&this.addresses.clear(),t={avatar:e.avatar,url:yt(e)},this.addresses.set(e.id,t));let n=t.url;if(n===null)return null;let r=this.pictures.get(n);if(r!==void 0)return r.ready?r.image:null;if(this.pictures.size>=vt){let e=this.pictures.entries().next().value;e!==void 0&&this.drop(e[0],e[1])}let i={image:this.create(),ready:!1};return this.pictures.set(n,i),i.image.crossOrigin=`anonymous`,i.image.referrerPolicy=`no-referrer`,i.image.decoding=`async`,i.image.onload=()=>{i.ready=i.image.naturalWidth>0},i.image.onerror=()=>{i.ready=!1},i.image.src=n,null}dispose(){for(let[e,t]of[...this.pictures])this.drop(e,t);this.addresses.clear()}drop(e,t){t.image.onload=null,t.image.onerror=null,t.ready=!1,this.pictures.delete(e)}},St=1e3,Ct=e=>e.reduce((e,t)=>e+t,0),wt=(e,t,n)=>{if(e.measureText(t).width<=n)return t;let r=t;for(;r.length>1&&e.measureText(`${r}…`).width>n;)r=r.slice(0,-1);return`${r}…`},Tt=(e,t,n,r)=>{for(let i=n;i>r;--i)if(e(i)<=t)return i;return r},Et=`#ff7066`;function Dt(e,t,n,r,i,a){let o=i.spentBelow!==void 0&&i.value{e.beginPath(),e.arc(t,n,r,0,Math.PI*2),e.closePath()};e.save(),s(),e.fillStyle=`#0b1220`,e.fill(),i===null?(e.globalAlpha=.4,e.fillStyle=o,e.fill(),e.globalAlpha=1,e.fillStyle=`#f5f8ff`,e.textAlign=`center`,e.font=`800 ${Math.round(r*1.1)}px Inter, system-ui, sans-serif`,e.fillText(bt(a),t,n+r*.39)):(e.save(),e.clip(),e.drawImage(i,t-r,n-r,r*2,r*2),e.restore()),s(),e.lineWidth=Math.max(1.5,r/9),e.strokeStyle=o,e.stroke(),e.restore()}function kt(e){let t=e.left_eye>=d,n=e.right_eye>=d;return t&&n?`BOTH EYES SHUT`:t?`LEFT EYE SHUT`:n?`RIGHT EYE SHUT`:null}var At={cut:`closing the cut`,swelling:`icing the swelling`,breath:`catching breath`,balanced:`a little of everything`};function jt(e){let t=e.flatMap((e,t)=>e.corner_choice===null?[]:[`${t===0?`Blue`:`Red`}: ${At[e.corner_choice]}`]);return t.length===0?null:t.join(` · `)}function Mt(e,t,n,r,i){e.save(),e.font=`800 9px Inter, system-ui, sans-serif`;let a=e.measureText(r).width+12,o=i?t-a:t;e.fillStyle=`rgba(150,22,18,0.92)`,e.fillRect(o,n,a,14),e.fillStyle=`#ffe8e3`,e.textAlign=`left`,e.fillText(r,o+6,n+10),e.restore()}function Nt(e,t){return Math.max(1,Math.min(1.6,e/1280,t/720))}function Pt(e,t,n){let r=nt(t.style),i=e?.record===void 0?null:le(e.record)?`DEBUT`:ce(e.record),a=e?.cpu===!0?`CPU`:`ELO ${e?.rating??`—`}`,o=t.knockdowns>0?`${t.knockdowns} KD`:null,s=[o,t.warnings>0?`${t.warnings} WARNING${t.warnings===1?``:`S`}`:null,t.deductions>0?`−${t.deductions} PT${t.deductions===1?``:`S`}`:null],c=[o,t.warnings>0?`W${t.warnings}`:null,t.deductions>0?`−${t.deductions}`:null],l=(...e)=>e.filter(e=>e!==null).join(` · `),u=n?[l(r,...c),l(...c)]:[l(r,i,a,...s),l(r,a,...s),l(r,...s),l(r,...c),l(...c)];return u.filter((e,t)=>e!==``&&u.indexOf(e)===t)}var Ft=168;function It(e,t,n,r,i,a,o,s,c,l){e.save(),e.beginPath(),s?(e.moveTo(t+14,n),e.lineTo(t+r,n),e.lineTo(t+r-14,n+62),e.lineTo(t,n+62)):(e.moveTo(t,n),e.lineTo(t+r-14,n),e.lineTo(t+r,n+62),e.lineTo(t+14,n+62)),e.closePath(),e.fillStyle=`rgba(3,6,12,0.82)`,e.fill(),e.strokeStyle=`rgba(190,200,220,0.22)`,e.lineWidth=1,e.stroke(),e.fillStyle=c,e.fillRect(s?t+r-5:t,n+4,5,54);let u=l===void 0?20:51;l!==void 0&&Ot(e,s?t+r-11-16:t+11+16,n+4+16,16,l,i,c);let d=s?t+r-u:t+u;e.textAlign=s?`right`:`left`,e.fillStyle=`#f5f8ff`;let f=i.toUpperCase(),p=r-u-24;e.font=`800 ${Tt(t=>(e.font=`800 ${t}px Inter, system-ui, sans-serif`,e.measureText(f).width),p,16,11)}px Inter, system-ui, sans-serif`,e.fillText(wt(e,f,p),d,n+21),e.fillStyle=`#93a3bd`;let m=r-u-14,h=(t,n)=>(e.font=`600 ${n}px Inter, system-ui, sans-serif`,e.measureText(t).width),g=a.find(e=>h(e,8)<=m)??a.at(-1)??``;e.font=`600 ${Tt(e=>h(g,e),m,10,8)}px Inter, system-ui, sans-serif`,e.fillText(wt(e,g,m),d,n+35);let _=(r-52)/o.length,v=o.length*_+(o.length-1)*12,y=s?t+r-20-v:t+20;o.forEach((t,r)=>{Dt(e,y+(_+12)*r,n+48,_,t,s)}),e.restore()}function Lt(e,t,n,r,i,a){let o=Ft;e.save(),e.beginPath(),e.moveTo(t-o/2+12,n),e.lineTo(t+o/2-12,n),e.lineTo(t+o/2,n+58/2),e.lineTo(t+o/2-12,n+58),e.lineTo(t-o/2+12,n+58),e.lineTo(t-o/2,n+58/2),e.closePath(),e.fillStyle=`rgba(4,6,12,0.88)`,e.fill();let s=e.createLinearGradient(t-o/2,n,t+o/2,n+58);s.addColorStop(0,`#8a6a26`),s.addColorStop(.5,`#f6d57a`),s.addColorStop(1,`#8a6a26`),e.strokeStyle=s,e.lineWidth=2,e.stroke(),e.textAlign=`center`,e.fillStyle=`#ffffff`,e.font=`800 22px ui-monospace, monospace`,e.fillText(r,t,n+27),e.fillStyle=`#f6d57a`,e.font=`800 12px Inter, system-ui, sans-serif`,e.fillText(i,t,n+44),a!==`FIGHT`&&(e.fillStyle=`#93a3bd`,e.font=`700 9px Inter, system-ui, sans-serif`,e.fillText(a,t,n+55)),e.restore()}var Rt=78,zt=50,Bt=.22,Vt=e=>e<480?zt:Rt,Ht=(e,t)=>Math.max(e<640?112:56,t*.1)+Vt(t)/2-t/2,Ut={upright:{bottom:118,height:274,moves:0,grid:118},landscape:{bottom:118,height:196,moves:102,grid:118},short:{bottom:112,height:154,moves:86,grid:92},shortHeight:350,reach:216,movesReach:134},Wt=(e,t)=>e<=t?Ut.upright:t<=Ut.shortHeight?Ut.short:Ut.landscape;function Gt(e,t,n){let r=Wt(e,t);return n<=t-r.bottom-r.height?0:Ut.reach+(r.moves>0&&n>t-r.bottom-r.moves?Ut.movesReach:0)}var Kt=160;function qt(e,t,n=!1){let r=Vt(t),i=t/2+Ht(e,t)-r/2,a=Math.min(320,e-24),o=e/2+a/2;if(n&&e<=t)Gt(e,t,i+r+8)>0&&(r=zt);else if(n){let n=e<640?Math.min(e/2-Ft/2-8,e-Gt(e,t,56+r)):0,s=n-12>=Kt;s&&(i=56);let c=s?n:e-Gt(e,t,i+r);o>c&&(a=Math.min(a,c-12),o=c)}return{x:o-a,y:i,width:a,height:r}}var Jt={width:380,height:150},Yt=112;function Xt(e,t,n=!1){let r=Wt(e,t),i=t-r.bottom-r.grid,a=e<=t,o=n&&a?Math.min(t-Yt,i-8):t-Yt,s=Math.max(56,Math.min(Math.max(t*.24,cn(t,e<640)+34),o-Jt.height)),c=Math.min(Jt.width,e-24),l=e/2+c/2;if(n&&!a&&s+Jt.height>i){let t=e-Ut.reach;l>t&&(c=Math.min(c,t-12),l=t)}return{x:l-c,y:s,width:c,height:Jt.height}}function Zt(e,t,n,r){let i=t.height===zt,a=t.x+t.width/2;e.fillStyle=`rgba(3,6,12,0.88)`,e.fillRect(t.x,t.y,t.width,t.height),e.strokeStyle=`rgba(246,213,122,0.45)`,e.lineWidth=1.5,e.strokeRect(t.x+2,t.y+2,t.width-4,t.height-4),e.textAlign=`center`,e.fillStyle=`#ffd77a`,dn(e,n,a,t.y+(i?22:34),t.width-24,800,i?17:22,12),e.fillStyle=`#c8d3e6`,e.font=`600 ${i?11:12}px Inter, system-ui, sans-serif`,e.fillText(wt(e,r,t.width-24),a,t.y+(i?39:58))}var Qt=class{fightTicks=null;round=0;ticks(e){return e.round_number!==this.round&&(this.round=e.round_number,this.fightTicks=null),e.phase===`fight`&&(this.fightTicks=e.phase_ticks_remaining),(e.phase===`knockdown`||e.phase===`foul_recovery`||e.phase===`complete`)&&this.fightTicks!==null?this.fightTicks:e.phase_ticks_remaining}},$t=()=>({thrown:0,landed:0,jabsThrown:0,jabsLanded:0});function en(e,t){return e.action_id!==null&&t.some(t=>(t.kind===`block`||t.kind===`perfect_block`)&&t.tick===e.tick&&t.action_id===e.action_id&&t.actor_id===e.target_id)}var tn=class{stats=new Map;totals=new Map;record(e,t=[]){if(e.kind===`bell`){e.detail===`round_start`&&this.stats.clear();return}if(e.actor_id!==null){if(e.kind===`punch_start`){let t=+(e.detail.split(`:`)[1]===`jab`);for(let n of[this.entry(this.stats,e.actor_id),this.entry(this.totals,e.actor_id)])n.thrown+=1,n.jabsThrown+=t}else if((e.kind===`hit`||e.kind===`counter_hit`)&&!en(e,t)){let t=+(e.detail.split(`:`)[0]===`jab`);for(let n of[this.entry(this.stats,e.actor_id),this.entry(this.totals,e.actor_id)])n.landed+=1,n.jabsLanded+=t}}}get(e){return this.stats.get(e)??$t()}total(e){return this.totals.get(e)??$t()}entry(e,t){let n=e.get(t);return n===void 0&&(n=$t(),e.set(t,n)),n}};function nn(e){let t=e.method.replaceAll(`_`,` `).toUpperCase();if(e.method!==`decision`&&e.method!==`draw`)return t;let n=0,r=0,i=0;for(let t of e.scorecards){let e=Ct(t.player_one),a=Ct(t.player_two);e>a?n+=1:a>e?r+=1:i+=1}let a=e.scorecards.length;return a===0?t:e.method===`draw`?i===a?`UNANIMOUS DRAW`:n===r?`SPLIT DRAW`:`MAJORITY DRAW`:Math.max(n,r)===a?`UNANIMOUS DECISION`:i>0?`MAJORITY DECISION`:`SPLIT DECISION`}var rn=new Set([`ko`,`flash_ko`,`tko`]),an=3.6,on=2.4;function sn(e){return e===null?0:rn.has(e.method)?an:e.method===`disqualification`||e.method===`doctor_stoppage`?on:0}var cn=(e,t)=>Math.max(e*.16,(t?112:56)+40),ln=(e,t)=>e!==null&&e>=(t?100:120);function un(e,t,n,r,i,a,o,s,c=30,l=null,u=null,d=null,p=null,m=null,h=null,g=null,_=!1){e.save();let v=Nt(t,n);e.scale(v,v),t/=v,n/=v,e.textBaseline=`alphabetic`;let y=e=>{let t=i[e.player_id];return t===void 0||h===null?null:h(t)},b=t<640,x=b?(t-56)/2:Math.min(300,t*.38,t/2-Ft/2-8-24),S=n-84;o===null&&r.fighters.forEach((r,a)=>{let o=a===1,s=o?t-24-x:24,c=i[r.player_id],l=[{label:`${b?`STA`:`STAMINA`} ${Math.round(r.stamina)}`,value:r.stamina,maximum:r.maximum_stamina,from:`#ffe08a`,to:`#d9a53a`},{label:`${b?`HP`:`HEALTH`} ${Math.round(r.conditioning)}`,value:r.conditioning,maximum:St,from:`#ff8a7a`,to:`#b02a20`},{label:`GUARD`,value:r.guard,maximum:700,from:`#9ec7ff`,to:`#3d6fb8`,spentBelow:f},{label:`POISE ${Math.round(r.poise)}`,value:r.poise,maximum:600,from:`#e8c890`,to:`#8a6a34`}],u=a===0?`#3d6fb8`:`#b02a20`;It(e,s,S,x,c?.name??`Fighter`,Pt(c,r,b),l.slice(0,2),o,u,b?void 0:y(r)),b&&!(_&&t>n)&&Ot(e,t/2+(o?1:-1)*112,83,20,y(r),c?.name??`Fighter`,u);let d=S-12,p=Math.min(64,(x-52)/2);Dt(e,o?s+x-20-2*p:s+20,d,p,l[2],o),Dt(e,o?s+x-8-p:s+32+p,d,p,l[3],o);let m=kt(r.trauma);m!==null&&Mt(e,o?s+x-20:s+20,d-34,m,o)});let C=Math.floor((m??r.phase_ticks_remaining)/c),w=`${Math.floor(C/60)}:${String(C%60).padStart(2,`0`)}`,T=r.phase===`knockdown`&&u===null&&r.fighters.some(e=>e.player_id===a&&e.is_downed)?Xt(t,n,_):null,E=T!==null&&b&&T.y<112;o===null&&!E&&Lt(e,t/2,b?54:n-84,w,`ROUND ${r.round_number}`,r.phase===`complete`?`FINAL`:r.phase.replace(`_`,` `).toUpperCase());let D=d===null?0:Math.round(d);if(D>=100&&!b&&o===null&&!(_&&u!==null)&&r.fighters.some(e=>e.player_id===a)&&(e.save(),e.textAlign=_?`left`:`right`,e.font=`700 11px ui-monospace, monospace`,e.fillStyle=D<200?`rgba(240,200,110,0.9)`:`rgba(255,110,100,0.95)`,e.fillText(`SLOW CONNECTION ${D} ms`,_?24:t-24,76),e.restore()),u!==null){let t=b?120:64;e.save(),e.fillStyle=`rgba(3,6,12,0.82)`,e.fillRect(24,t,196,34),e.fillStyle=`#ff3b3b`,e.beginPath(),e.arc(42,t+17,6,0,Math.PI*2),e.fill(),e.fillStyle=`#f6f7fb`,e.font=`800 14px Inter, system-ui, sans-serif`,e.textAlign=`left`,e.fillText(u,56,t+22),e.restore()}p!==null&&r.phase===`fight`&&(e.save(),e.textAlign=`center`,e.font=`900 46px Inter, system-ui, sans-serif`,e.lineWidth=6,e.strokeStyle=`rgba(0,0,0,0.75)`,e.strokeText(p,t/2,n*Bt),e.fillStyle=`#f6d57a`,e.fillText(p,t/2,n*Bt),e.restore()),r.phase===`countdown`&&Zt(e,qt(t,n,_),`ROUND ${r.round_number}`,`Touch gloves. Protect yourself at all times.`);let O=cn(n,b);if(r.phase===`knockdown`&&u===null){let n=r.fighters.find(e=>e.is_downed),a=n??r.fighters.find(e=>e.player_id===g),o=a===void 0?null:(i[a.player_id]?.name??`Fighter`).toUpperCase();e.save(),e.textAlign=`center`,e.font=`900 44px Inter, system-ui, sans-serif`,e.lineWidth=6,e.strokeStyle=`rgba(0,0,0,0.75)`,(T===null||T.y>=O+6)&&(e.strokeText(`KNOCKDOWN`,t/2,O),e.fillStyle=`#ff4d4d`,e.fillText(`KNOCKDOWN`,t/2,O)),e.font=`700 15px Inter, system-ui, sans-serif`,e.fillStyle=`#e6ecf7`;let s=n===void 0?` BEAT THE COUNT`:` IS DOWN`,c=o===null?`STANDING EIGHT`:`${wt(e,o,Math.min(t*.5,t-32-e.measureText(s).width))}${s}`;(T===null||T.y>=O+30)&&e.fillText(c,t/2,O+24),e.restore()}if(r.phase===`knockdown`&&u===null){let i=r.fighters.find(e=>e.player_id===a),o=Math.max(...r.fighters.map(e=>e.get_up_count));if(i?.is_downed!==!0)o>=1&&(e.save(),e.textAlign=`center`,e.font=`900 36px Inter, system-ui, sans-serif`,e.lineWidth=5,e.strokeStyle=`rgba(0,0,0,0.75)`,e.strokeText(`COUNT ${o}`,t/2,O+64),e.fillStyle=`#ffd77a`,e.fillText(`COUNT ${o}`,t/2,O+64),e.restore());else{let{x:a,y:s,width:c,height:l}=T??Xt(t,n,_),u=a+c/2;e.fillStyle=`rgba(3,6,12,0.88)`,e.fillRect(a,s,c,l),e.strokeStyle=`rgba(246,213,122,0.5)`,e.lineWidth=2,e.strokeRect(a+2,s+2,c-4,l-4),e.textAlign=`center`,e.fillStyle=`#ffd77a`,e.font=`800 30px Inter, system-ui, sans-serif`,e.fillText(o>=1?`COUNT ${o}`:`GET UP!`,u,s+40);let d=i?.is_downed===!0?i.get_up_prompt:null;if(d!=null&&i!==void 0){let t=r.tick>=i.get_up_window_start_tick&&r.tick<=i.get_up_window_end_tick,n=d===`get_up_left`?`←`:`→`;t?(e.fillStyle=`#ffe9a8`,e.font=`900 64px Inter, system-ui, sans-serif`,e.fillText(n,u-90,s+106),e.fillStyle=`#7dffa8`,e.font=`900 34px Inter, system-ui, sans-serif`,e.fillText(`NOW!`,u+62,s+100)):(e.fillStyle=`#8fa3c8`,e.font=`800 30px Inter, system-ui, sans-serif`,e.fillText(`GET READY ${n}`,u,s+98));let o=c-60,l=Math.max(0,Math.min(1,i.get_up_meter/Math.max(1,i.get_up_required)));e.fillStyle=`rgba(2,4,9,0.9)`,e.fillRect(a+30,s+120,o,16);let f=e.createLinearGradient(a+30,0,a+30+o,0);f.addColorStop(0,`#7dffa8`),f.addColorStop(1,`#2ea860`),e.fillStyle=f,e.fillRect(a+30,s+120,o*l,16),e.strokeStyle=`rgba(210,220,235,0.5)`,e.lineWidth=1,e.strokeRect(a+29.5,s+119.5,o+1,17)}else e.fillStyle=`#c8d3e6`,e.font=`600 14px Inter, system-ui, sans-serif`,e.fillText(wt(e,`Waiting for your rhythm instruction…`,c-24),u,s+100)}}if(r.phase===`foul_recovery`){let a=r.fighters.find(e=>e.is_foul_recovery_target);Zt(e,qt(t,n,_),`FOUL RECOVERY`,`${i[a?.player_id??``]?.name??`Fighter`} is recovering`)}if(r.phase===`rest`){let i=r.fighters.map((e,t)=>({corner:t===0?`Blue`:`Red`,stats:l?.get(e.player_id)??$t()})).filter(({stats:e})=>e.thrown>0||e.landed>0).map(({corner:e,stats:t})=>`${e} ${t.landed} of ${t.thrown}`).join(` · `),a=jt(r.fighters);Zt(e,qt(t,n,_),`CORNERS · RECOVER`,a??(i.length>0?`Landed this round: ${i}`:`Conditioning governs recovery`))}if(s>0){let r=Math.min(320,t-24),i=Vt(n);Zt(e,_?qt(t,n,!0):{x:t/2-r/2,y:n/2-i/2,width:r,height:i},`OPPONENT RECONNECTING · ${Math.ceil(s/1e3)}s`,`The bout is paused`)}if(o!==null){let a=r.fighters.map(e=>l?.total(e.player_id)??$t());xn(e,t,n,mn(o,r.fighters,i,a),!0,[y(r.fighters[0]),y(r.fighters[1])])}e.restore()}function dn(e,t,n,r,i,a,o,s,c=`Inter, system-ui, sans-serif`){e.font=`${a} ${Tt(n=>(e.font=`${a} ${n}px ${c}`,e.measureText(t).width),i,o,s)}px ${c}`,e.fillText(wt(e,t,i),n,r)}var fn={ko:`KNOCKOUT`,flash_ko:`FLASH KNOCKOUT`,tko:`TECHNICAL KNOCKOUT`,doctor_stoppage:`DOCTOR STOPPAGE`,disqualification:`DISQUALIFICATION`,forfeit:`FORFEIT`},pn=(e,t)=>e>t?0:t>e?1:null;function mn(e,t,n,r){let i=t.map(e=>n[e.player_id]?.name??`Fighter`),a=e.winner_id===null?null:t[0].player_id===e.winner_id?0:t[1].player_id===e.winner_id?1:null,o=[],s=[];if(e.method===`decision`||e.method===`draw`)for(let t of e.scorecards){let e=Ct(t.player_one),n=Ct(t.player_two);s.push({label:t.judge.toUpperCase(),values:[String(e),String(n)],lead:pn(e,n)})}let c=[t[1].knockdowns,t[0].knockdowns];(c[0]>0||c[1]>0)&&o.push({label:`KNOCKDOWNS`,values:[String(c[0]),String(c[1])],lead:pn(c[0],c[1])});let[l,u]=t.map(t=>e.punches?.[t.player_id]),d=e=>({thrown:e.thrown,landed:e.landed,jabsThrown:e.jabs_thrown,jabsLanded:e.jabs_landed}),f=l!==void 0&&u!==void 0?[d(l),d(u)]:r;if(f[0].thrown>0||f[1].thrown>0){let e=(e,t)=>t>0?`${Math.min(e,t)}/${t} (${Math.round(Math.min(e,t)/t*100)}%)`:`0/0`,t=f.map(e=>({landed:e.landed-e.jabsLanded,thrown:e.thrown-e.jabsThrown}));o.push({label:`TOTAL PUNCHES`,values:[e(f[0].landed,f[0].thrown),e(f[1].landed,f[1].thrown)],lead:pn(f[0].landed,f[1].landed)}),o.push({label:`JABS`,values:[e(f[0].jabsLanded,f[0].jabsThrown),e(f[1].jabsLanded,f[1].jabsThrown)],lead:pn(f[0].jabsLanded,f[1].jabsLanded)}),o.push({label:`POWER PUNCHES`,values:[e(t[0].landed,t[0].thrown),e(t[1].landed,t[1].thrown)],lead:pn(t[0].landed,t[1].landed)})}let[p,m]=t.map(t=>e.ratings[t.player_id]);if(t.some(e=>n[e.player_id]?.cpu===!0))o.push({label:`RATING`,values:[`Unrated`,`Unrated`],lead:null});else if(p!==void 0&&m!==void 0){let e=e=>`${e.after} (${e.after>=e.before?`+`:`−`}${Math.abs(e.after-e.before)})`;o.push({label:`RATING`,values:[e(p),e(m)],lead:null,news:[p.after>=p.before,m.after>=m.before]})}return{headline:fn[e.method]??nn(e),detail:`ROUND ${e.round}`,verdict:a===null?e.winner_id===null?`DRAW`:`${(n[e.winner_id]?.name??`Winner`).toUpperCase()} WINS`:`${i[a].toUpperCase()} WINS`,winnerSeat:a,names:i,judges:s,rows:o}}var hn=[`#4f86d9`,`#d9483c`],gn=14;function _n(e,t,n,r){let i=Nt(e,t),a=bn(e/i,t/i,n,r);return i===1?a:{...a,x:a.x*i,y:a.y*i,width:a.width*i,height:a.height*i,rowHeight:a.rowHeight*i,verdictHeight:a.verdictHeight*i,judgesHeight:a.judgesHeight*i}}var vn=new Set([`KNOCKDOWNS`,`TOTAL PUNCHES`,`RATING`]),yn=(e,t)=>t?e.rows.filter(e=>vn.has(e.label)):e.rows;function bn(e,t,n,r){let i=t<480,a=e>=900||e>=640&&i,o=Math.min(e-24,a?980:720),s=r?62:gn,c=i?72:112,l=n.judges.length===0?0:i?36:52,u=c+l,d=yn(n,i).length+1,f=t-(e<640?118:70)-14-gn-s-(a?0:u),p=Math.max(16,Math.min(i?20:28,f/d)),m=gn+(a?Math.max(u,d*p):u+d*p)+s;return{x:(e-o)/2,y:t-14-m,width:o,height:m,wide:a,dense:i,rowHeight:p,verdictHeight:c,judgesHeight:l}}function xn(e,t,n,r,i,a){let o=bn(t,n,r,i),{x:s,y:c}=o;e.fillStyle=`rgba(3,6,12,0.9)`,e.fillRect(s,c,o.width,o.height),e.strokeStyle=`rgba(246,213,122,0.55)`,e.lineWidth=2,e.strokeRect(s+1,c+1,o.width-2,o.height-2),e.textAlign=`center`;let l=(o.wide?o.width*.42:o.width)-32,u=o.wide?s+o.width*.21+4:t/2,d=c+gn,f=o.verdictHeight/112;e.fillStyle=`#f6d57a`,dn(e,r.headline,u,d+34*f,l,800,(o.wide||t>=640?32:28)*f,14),e.fillStyle=`#aebbd0`,dn(e,r.detail,u,d+56*f,l,700,13*f,9),e.fillStyle=r.winnerSeat===null?`#f6f7fb`:hn[r.winnerSeat],dn(e,r.verdict,u,d+92*f,l,800,25*f,12);let p=l/Math.max(1,r.judges.length),m=d+o.verdictHeight;r.judges.forEach((t,n)=>{let r=u-l/2+p*(n+.5);e.fillStyle=`rgba(255,255,255,0.06)`,e.fillRect(r-p/2+3,m,p-6,o.judgesHeight-8),e.fillStyle=`#8fa3c8`,dn(e,t.label,r,m+o.judgesHeight*.3,p-14,700,10,7),e.fillStyle=`#f6f7fb`,dn(e,`${t.values[0]} – ${t.values[1]}`,r,m+o.judgesHeight*.7,p-14,800,o.dense?14:17,10)});let h=o.wide?s+o.width*.42:s+12,g=s+o.width-12-h,_=o.wide?d:m+o.judgesHeight,v=g*.36,y=(g-v)/2,b=[h+y/2,h+g-y/2],x=o.rowHeight,S=Math.min(16,x*.6),C=_+x*.7;e.fillStyle=`rgba(255,255,255,0.06)`,e.fillRect(h,_,g,x);let w=Math.max(6,x/2-2);for(let t of[0,1]){let n=t===0?-1:1;e.fillStyle=hn[t],e.fillRect(t===0?h:h+g-4,_,4,x),Ot(e,b[t]+n*(y/2-10-w),_+x/2,w,a[t],r.names[t],hn[t]),e.textAlign=`center`,e.fillStyle=`#f6f7fb`,dn(e,r.names[t],b[t]-n*(w+3+v/4),C,y+v/2-22-w*2,800,S,9)}for(let t of yn(r,o.dense)){C+=x,e.fillStyle=`#8fa3c8`,dn(e,t.label,h+g/2,C,v,700,Math.min(12,S),8);for(let n of[0,1])e.fillStyle=t.news===void 0?t.lead===n?`#f6f7fb`:`#aebbd0`:t.news[n]?`#55df9b`:`#ff7b74`,dn(e,t.values[n],b[n],C,y-16,t.lead===n?800:600,S,9)}}var Sn=6;function Cn(){return typeof window<`u`&&typeof window.speechSynthesis==`object`&&window.speechSynthesis!==null&&typeof window.SpeechSynthesisUtterance==`function`}function wn(e){let t=e.filter(e=>e.localService&&e.lang.toLowerCase().startsWith(`en`));return t.find(e=>e.default)??t[0]??null}var Tn=class{settings;synthesis;Utterance;queue=[];speaking=null;destroyed=!1;voice=null;voicesListed=!1;onVoicesChanged=null;cutoff=null;voicesChanged=()=>{this.listVoices(),this.voice===null&&this.cancel(),this.onVoicesChanged?.()};visibility=()=>{document.hidden&&this.cancel()};constructor(e,t=Cn()?window.speechSynthesis:null,n=Cn()?window.SpeechSynthesisUtterance:null){this.settings=e,this.synthesis=t,this.Utterance=n,this.listVoices(),t?.addEventListener?.(`voiceschanged`,this.voicesChanged),document.addEventListener(`visibilitychange`,this.visibility)}get supported(){return this.synthesis!==null&&this.Utterance!==null}get hasVoice(){return this.supported&&(this.voice!==null||!this.voicesListed)}speak(e){if(!this.supported||this.destroyed||document.hidden)return;let t=this.settings();!t.announcer||t.volume<=0||(this.voice===null&&this.listVoices(),this.voice!==null&&(this.queue.push(...e),this.queue.length>Sn&&this.queue.splice(0,this.queue.length-Sn),this.speaking===null&&this.next()))}settingsChanged(){let e=this.settings();(!e.announcer||e.volume<=0)&&this.cancel()}finishLine(e){this.queue.length=0,this.clearCutoff();let t=this.speaking;t!==null&&(this.cutoff=window.setTimeout(()=>{this.cutoff=null,this.speaking===t&&this.cancel()},e))}cancel(){this.queue.length=0,this.clearCutoff();let e=this.speaking;this.speaking=null,e!==null&&(e.onend=null,e.onerror=null,this.synthesis?.cancel())}destroy(){this.destroyed=!0,this.synthesis?.removeEventListener?.(`voiceschanged`,this.voicesChanged),document.removeEventListener(`visibilitychange`,this.visibility),this.onVoicesChanged=null,this.cancel()}clearCutoff(){this.cutoff!==null&&window.clearTimeout(this.cutoff),this.cutoff=null}listVoices(){let e=this.synthesis?.getVoices()??[];e.length>0&&(this.voicesListed=!0),this.voice=wn(e)}next(){let e=this.synthesis,t=this.Utterance,n=this.voice,r=this.queue.shift();if(e===null||t===null||r===void 0)return;let i=this.settings();if(!i.announcer||i.volume<=0||n===null||document.hidden){this.queue.length=0;return}let a=new t(r);a.volume=Math.min(1,i.volume),a.rate=.96,a.pitch=.82,a.voice=n,a.lang=n.lang;let o=()=>{this.speaking===a&&(this.speaking=null,this.clearCutoff(),this.next())};a.onend=o,a.onerror=o,this.speaking=a,e.speak(a)}},En=[{name:`decapitation`,sampleRate:12e3,frames:7920,wav:`UklGRgQ+AABXQVZFZm10IBAAAAABAAEA4C4AAMBdAAACABAAZGF0YeA9AAAGAGAAd/9r/1b/Dv+d/kH+Of69/vL/gAGaAhUD9wKOAmICHwPSBLYGQ/pC+4r72/rQ+Sr5sPle+7X+MQM2BmQHHwfgBSEFpAU+CKkLYA649Q331faL9fnzlfMw9ST5+/5iBV4JDAtaCuYIVgdWB0kKSw/iE2HuFvGL8lnxVO/g7TXt4+6L8wT7agMsCvAN0g4gDb0K4QjzCOEMBhO+GHQdcuis667sVOsZ6dTmDeaH54Hr4fE9+0wECgylELYRuBDhDU0LfAnWCb8OABXvG6QiDyiLKwjk3uUl5fziMuAc3v/cvtx23kbj2Op98xH8EQYQD/ETiBVzFgkUHxCYDc0MWAtYC4sPrhWTHE4kpitIMV41DDlm11XZV9pG2ZPXOtVN0yrRjc/wzkXPstA51HDae+DX6I/xjfq1BBYNYRMgGAYaSRwnHU8dkxoAFz0USRJLECwP5A0ZDyUSzha7HdclaS2nNJw6o0JgRw9LVE6kUMZROMnOyWDLQMlWxRzEScGKv0u+eLx5uq+5xbnxuii8ub7nwOLDMMrT0IXWnt1r54DwWPp2A/YLiRBdF9wbex6gIAkibyGNH3IdGBpMFdESOA45C08JEQoUC1gMIBImGl0fFiogM7A6FEMZS1ZThlfAXGhgxGHyY1S6Cbszuki4dLensvmwaq+crAesQqy5rfiv1bR/un/B2swO2f3mdfQbAw8OrRS+HC0duyDeHmwZUhXJDocJfAmGC0ATOBemJCoytT+YTKJX0V1qYl9lbWeztN+z4K8JrOeo/ahZq6SvurVXwVLQGOOf94YKuBgvHU4f5B3rFjkS1gyMCgcQKhoGKkw9PEzkWhhiZ2aZZ2St4ai1pG6h6qIKphixgsTW3e/4/guGGmweeRq0FCwHEwP2BwwZyTACRwBZg2M7Z4ZoVaiRpKCgDKHkqFq4iNHe7h8LXxibGUARPArNA8UFWhWgMIBLOl9CZ9JqEKzepjah0KBBqfS3VdGH9OwNIxVxDgUG8v/C/lETLjDrS1Fe62gLa6anjKD8nTmgU61jxsDlmwFlD4gNHQJ38/TykQOGJq9GQFxCZn1oE2URnQiaVqC3rDrKK+k9AtkKPADV9k30rfo5Fk44x1WZY4pnGWf1nCqZO5vhplO9seDF/1MQsQykA9r3VfRl/yUePTy3VaNiJWZzYtCYSZc1nHGr+MZo5q4ADwxeCLj9DPOK82X9UBr9PRdWqWTKaS5oh2PYmZmZfp5xq4rEPd5T9a3+yfzA8/npoOdf8XsFYiXlQutYMmIYZphjS1/pmPGY7pw1pvm30NKu6Jf7GgCr+5vuDufK5Czm6va+C6IlfT29T4laJF5mXdFajlE4kjyUlZdxnamq9by30VXmivRW/Y35QfJO7hjsuuZh6ub2DAioGBssJUFPTD9T0Fg8WEVVFFU5VENTzJXSl82Z8J9Eqv+3CMZq09Ph3/AL9TH5QfVM7vDnROO44VnfyN3X5C7t7/xeERwiSC+qPM1HlFIkWFxaWVsAXFVaUllVVjpRD0phRoBIppSulcqXy5v0oDypUbV0wNvHk9Bu2cLcTuC946rjsd4L3UTdGt1X2SnYlNTn1MLaMuDm5drwK/dnAJUMvRmgKM8utTgwPzpE/0oNT81TOVazVPlWPlZMVRtSqU0CTMtIA0qLS3NKZprGnROgl6SRqvGwf7W7u2/G9dA92Ink4+uZ8CfzkfFT7TzmEOHK3RnZ99TczxHQjNO02d7gfupp9hMAbg8yHIoqEzWlO6RD1ElkSplNHkxOSS1GA0PjQEtAEz/mmiudmKBhqHGv7Lqyx3PV0OB57Fruqe827T3l8dwe27HWIdgS2nflQ/O5ATEXQSq5NpE/X0CJQNY96TpyNpwyAy6SM9mbyKJurdS6kclG2vPl0+8R7BHpjuDh2lbYYN936sL+ehW3KuY7UUU6SB1Hj0HyOzU4iDsXo5WpKLZgxHjV9N/q4jne2dYx0TjU89x38boMsCYwOtlE8kQWP3M2pzKRNm+k3q9qwDHShd/E4bza0NLp0J/YoulEBQwlgTlAQUI/bDdrLhAt+Tcoq/C4WMoh2cXal9M3zQPN69os+C4c2DXWPJw7GTO1LsQ0aUPusxDHwNnS4U/brNGqynPQMev1D2MtjzboNdwvFSgVMOtBe7ZCy0Tbgt1Z1DXKN80a3rX/ACD2Mv03IzIpKe8qMznGsPbDQdZe3QTXyc6PzDPZO/XVF8MukzVYMZMmfySgMh9HpcCG05HcBNcmzK7JCNMH7cYNyycmNP4ywC1lKPcxIkLFUQzOKt183gbUvsxDzyXd5PwxHL4u2jKTLTMloCBLKcQ75U2txafSxdQ2zT7Gt8cy1dnuSgx9KKIzTTKbKukiICSDL25BH1CSxi7QotBDy2PE+8F9yXHZ5PZmEPkjZiywJzEiPxrOGOQlvDinSVa+xMhR0LbMcsbMwj7BUcaZ19fu9guHH+IpXixGJ6UgQxrAF/AePS2VPvlMxFS+xxfM9MmaxRPCvr+WwenIK9nK7IYBDRZ5I1koZijvI3scyBc0FVQcniZyMylCyEySVDrE4si6ytjHxcM3v0u8WLu6vD/Et89q4HbzlAjIFwEhySaGKagmECLGHJIXgRJqFB0dYijKMzY/30hlUBVWQ1m8xg3HCcZAw4HAHb0du6e4/rnpvfHCF8sq1+vjMPQqAc0OahivG7Yfmh+8HFwYEBWuETsQCBFnFAwZgyEqLFs4vkGTSnhRV1fpWinElsepyPLFS8IJvw68D7fKtSi0sLXyuD7Aoshc1dXi8/KcAP8OSxiZHgEfZx+4HE8ZwRV7EN0OcgxRDoYX2CLnLYw6OUVKT41WC1wSX4LBrsGewKa8Dbm+tsGzzLKVs/K35L5jymTZH+3Z/DgMFBnZIPYiVCHAG3EUpAwQCvIKEhE8HNArBDxQSo1V+VyDYAa/bb04ug22OLNusZey27cBwxPSxOi7/zoT9RxHIXogNRlmEawLBAzUE88imjbnSN1VQ16ZYsS6jbdKslKuKK3Zr9i6IM0m5UQAoBNKHU0bLRR6DHUI9wseHMkyGEhgV0Bh1GN+tHyteahVplCqz7ix0G3tXgn7FzsYGxGPCUoHdg/BKUVDJFdqYYFlxLDnqZ2lfai+s7TKfO3aBxoWJBVWDFgCZQF2EGIwW0xsXsxkP2XlqGGjnaQ4sGbLwO7kCp0UIQsR/Bf4RgVEIxtE61nmYvRiy6RqoEuhm63pyZnwOQv4EoYJy/xG/NIS3TXHUjlhQGaAY8CiVaGDqH6+gOH+AWYRxAsh/I/16gDNIudGDl3RZLhibZ7hmzmgkLF50tz1DQcEBaf4iu8Q+qsbNEMkXa1mPGexYXqc255qrf/JaO5aBrcHufyL8MnyQwpgLiRQ+F9bZEBiG5vOmDGf5a8BzlvuLQMRAcD0Z+ko7BX+UCPGRb1aHWN1Y9NbmJaLmHujRbow3Ij3jAWQAkT5qu2h7yb/fx5JP9NWc2FzY7xf0liBlYWZYaQ7uoDW/PBa//X+RvRT5lDh9Ohx/V0dpTxkUcNdPGEXXm1Xa0+okEiUB51ZrYXEzd8n8yT8cvyw9VDoNuNI5jvzTgiFJLw9nFAgWUBfLl4+WjlVclJNkxWXp532qWK5ps7M5Dvy8vl3+sD1FekA43veDd+O5mX1ZggoHNMssUOJTxJX8VmgWKFWqFRiUudOVk08keySxZbknZmnObLfvR7O9tu15XzuafEY9CPs++hp47jeWdqV4W7lEOwr98EIKhjxKsA6qka0TA9UB1oyWiBa5FfGVVZSl0/vTHZMpEsdk8eU35fcmzChOKkXsTS6ZsL5yvTVHN9w4kbnAOgX6WbknuKk3afax9ng2ZzXLNmE3lrj/+4rAdkPSR3BLLE8WkWGSzJUB1dRV2RVuFQmU1NPH01vTCxIXEbjRI5Ip5QTl86aYZ8npPyucrr3xsrS5d1O5oLsCe+F7gPnFuA/3UzXstEOz6/P5dZx4GrwkwNBFSArzT+TTCFTlFYwWQ1aT1gdVS5Pa0ngRnVI0ZlJnkKjDq1yukTKVNlQ5TnuC/Ff7RXoet6W07XO0M9y1Frg2vS3DYgjBjixRStPclAwUCpOaEZmQotBckX4nSml0bLxw5rSK+MA6W3rrubD3CHZCNvQ4gr4LBJLKo87pkZDUOlO0UfEQQ47rDrDmxKji7H0wrPUFeEn5NfcxNIDzPXS3eGE+dYZ4zRZQhhHBUX3QBI6gzq2RFSni7YvyaDeqOW+3I/UmNC71d/pzAkcK5s8kUMuQJE4vTA9M0A+S6hSu1bOPdir2xPRisqQzmrfzvxCH9o5T0ORQbg7wjWaN01Jw7Xsy1PhaOcP3gnSctCp4C78LhxnNrc7QjgzLiop2DFsQlSyvMi92jnhGtmB0zjW+OUeCLom3TiBOuAxOCmGJ2g2fap9vnXU8t3K2FrNtcej0D7rVxNOLQY4gTYrLYInlTFNRGe3F8wM3E7cKtIpzRXQU+P9Bp0objZ8OXYyACdeI0AwyUMltf3DRdAv0O7GRsCowDnOQO1rFBMrOzRGMb0rnikGMCdAUE56wHLRS9PuzDvEEMLTyWXcz/oOGLIslTUqMwUt/CTUIgUuz0EnUfnFktTK1hbPq8crxCnI3NNB7N0FiRp3Ky8uOin1IR0bkhtiJYM2pUceubDGPc9izyzL/sWcwljDEc0K3eP1eg57IFMoZizsJ2okzRz8HX0gAS0YPqRL3lNSy5vTUtY30xXRz8mpxobH8M422E/rdwKyFFkhwyoELHsqlySEHcEWJxQzF4Md6Cl6OKJGKE+fVTnI98xTzZbL78gQxFPAbMEExJzGhs8r3Z3uQfwDD+AZmCLdJ+MuKywNJtIhZSCWG1EeQyBpIwEp7jMjQLBI8k5RVEDDZMf2yv7KSMkxxLfAi70XvEW62rxfwRLHEtEU3w7wcgHrDUMboSEnJ4EqlSnxJT4gux3wG8wZ/hoxHCMjuSrBNglA6EZnThNTulbrwvzGRModyRrItMVWwkLBQMCzw0DGn8um1k3ia+91/jUM4BccHTwikyToHtYaTBj3Eq4SWRdrIAEr1zOzQBZL9FLRWMFbx8bGxUjCBr0Vu623s7UvuPG8ose71qrpNgJ5Er4dZSSPIzkf5RhhE9QNChFTGvQnqjlzSSVVhlr1wf7EysDNulW3NbWNtWK7iczl4o/8IRMgISslySI5HJwVzBDMFYQjcDUGSItWOl1Qw0DCHbtJtROyObcZwS7TBO75CgsbkiEXHewTfAgTDqIe9jYkS6FYv13qvIi4IrM7rwuzh8Bp3PL9uxYKHfwXRQwMBmcMmyIXPbFRA10CYFy21rA0rp6yz8Jz3xsDhhOSFUANZwMCCLgeIz8rWNdgbmMht+uun66+usPW4fn3EHgX/A5VBEwFPRxFPUNWzmALYiOv3qeOq0i8aN0fABYRLA2d/JP4rwtTLb1OOF6qYeytOqiYqnO7595FBTwVEQ4L/yr6FwzRM7xRsV5BYDCn4aJFp/O4C96A/5IJmACQ8pL01xCROaNWwF+VXlSjNqGQqW3DyepmBUIKNv3K8tL7hB79RDhaLGE3XvWeTZ9dq/rI6fAtC1YNtf3r82r9ch8oQ0latWCiXSCdHp3UpSO/FuQvAW8Ebvfm69HwzQlhMxhSN19MYBtatZnNmrCkortZ3Sb1nPq/8OnkJOXI+Y4gE0SVVrtcG1m5UAWV35lgqGLEoeVa+hD8c/LA57zntvhrFnE5gVEvXjhgslrtVMWVpJkWp5C++dxq8Sr3OfI15Rvd5OHv86ERIzH0SblYZl1AW7pV+U4Yk9uWYqLitj/Rc+hs9Ub20ux84e7cE9+Q6jYAix8XPFxQAlvEXPxaRVYVUQdOy5Lhl2Whca/DxVjXiOTG7EjrW+Fe2JfTO9cQ3Xvw6QgsIpA7WU1KWJdcrl3zW0VVE08JTJ1MV5JRl5qf9KqLulHPy+Gz7HfvrO4g6Njg3dk21qbVBdwu6QX+3hTVK4Q9sUpLU7pYIFk7VvBRg03aSH5G+Ef0j0mTY5mRo2exwcFP0sHgeuqy72TxoerX4zrZZNKL0F7RVdXY4SrycQllH6cz30PXTl9W5FoSW9NYYVQFUDFNXErySnRPPZYKnI2lTrKLwBjO3Nlu4rDlUOTq4NHZfNMaz4/PTdTk3Jrr1/51FAYqRD6pSSBRN1OGUwhQfUuvR1NFWkP8SWiVOp6BqSW67ssg2ozk6ucL5HHc9dPozTfM8tCf3TPvqwo7JRQ6yUbUTJtOR0usReFAVj5yQWBNQZxEqke72c372YbeLt1d1YzM3crEzgzd3fM/EwwxKEJlTdpN7UglQNw5IjsfRduZNanQun/Pz9sL3ubUQs0EyqzQC+IbAu8jIDz+SDpMDkVVOUU0fjmOSkWjlLdMyxDX9tYdz4fGjsfY02/vmBXVM2ZB2UP3POQ00DItPgeeQ7GPyaPZmNhuzszDLMTT1jT65B4aOb1CD0CeNv4yZD43UjqykszV2pPYC8slxATKY+G2CGUrvzs0Pc4xUivSLrNEEKr0wAfS+tDRxne+s8JV24cDoyfzOMw31y2tJYI0rEypsxnKqNNazlnEEMMH0RrxERv6Nb49mjXLKf8pPjviUz67ycwizpfC6r3jw8/c8wT1Jk82SjKzJ2Ikiy98SPxby8iB1OLPZ8VwwkrOlOsoE/ssRzKXK30hISFRM0JM+12ezL7T48wew/LCEtHB7toW1C7dMvgrRCRVIa0tskJDVgDCXsy9zF/Epr1Ow5LULveEGTwsQy96KC8esxqvJLE6a1BGXBTK88wVxS6+JLzjwijXl/VSFHomQSwlKC8gUhwCI8QzyUTNUzFchsrgyaLDurwOumm/lM0d5ccAThcVJDcmbSBUFqMOjhC3HIkvAEIZUKpX08OkxSzBobuJtzu2uLsIxyHcxvR3Cxsc+ySRI4UdkBcGEy8N6hJ5IWMw5kCJTD1VnFhDxIjFWsGfvHG5Kbm0vEXE7M873/TyOwZTFvUhbCPOI98e8xceEUwNkgxMDt4WSCUYM7o++khHUclU8lZFxWHEt8CLu8y5ebd0tmS4vLtRwhbM8trC7Yv7tQrqEzAcGCGhHbIcfBcUEtoNKAxRC6kOJRT4Hj0o1DNqPpZHkk3cUVVTwVVZwRy/Pb6Jul24yrYSt3q5qblVvQbFT8xx2Gbkv/Dl/okHuhGjFhkYHBe+FC4Q0wxaCgsI4AdWCr8UZxyoJ2k17j4ERuJMsVC8Uy1Uf77svAS6vriCtSS11rWOtti53sF6zMfXrud1+X4Fzw70FiQYDxb6EKwNAwYrAT4EkAppFZwkejF2P71Hm0+mUyVV3FU4uwS3cbW/szC2F7iSvz/M6N1D8a0BgAsyECoPVAuNA7H/gPxhBq4SniUXNzBEnU4CVfBUrbm9s1WxyK8+sgK7usaG2p3zogTnDVsLBgW3+7P17fjHBEAZgDEYRe5QDlVfVKCzbrAsr4qyOb/+01HtRwJrDk4L8f/b9xb4CwEJGukzoka1UARU0FKFrvmqmq5tuSXSuO9oBq0NFgk+/c75wQLrG7I4w06aVlxW4VCWqi2rjLLGxKvj9voGBIUAyPag8EwBeyFAPt9Ns1P1UdenOqVBq065J9ZZ9T4C0/kD7qbog/rVHXA/M1CUUxxOMqXbpTazCM7r7RUETABm8YnqofYBGxE8glBJU+JMqKGro82uPciP53n+RPnC8YHtHfskHPw+xE+tUghL3kY3ooqufcy878kAXfuY7rTu4wJJJ/tGsFXWVntQ9UqIpP60ntN69f8CEfmS6BbnIf5rIw5FKFF0URpJQUAQnU2o18El5EPvMu1g3g7dEe2cET05/U+YUj9NDkS1Q62gAray02nuSvBA5wDdT9xf8eMWrjU/Sv5NA0jRPhc9L5tJp/y/Q9wM62/nANzY1JfgDfzCJBRBQlIgVbxQtUkCRn2ZCKTru27W3Ol67UvnKNyS2PndK/bHFcE3r0ieUSVNcEZ0QTxG5JnopJG2VdAJ3+Hmc9/l1HLM7c5M22b1rheRM+dEo0wFSi1CHTquNztAREkAnc2rur2I0mTcwts208PI2sZ1zRDavvJvDYIr2T7BTH5P6k2tRs8++DhkPPpBn07Xnr6sPbwCy53YcNqu0tbMPMi0xGTGB9Eh5pL3FxS8LQ4/BkhISN9JJUTxOnE54TPENg1BRE8gnr6rGLwgzRLY3t4j3Q/aV9H5yVPCvMJmxkHRSufT/t8SLSrGOYhEE0mkSHlEKjtoNwU1ATUtOvlFO1PInRqpTbhxxMfMBtFL0h3Ld8PqvEK96sCCy4Lau+wOBB8dyzH4QRhIN0rxQsQ+WzkIMwMxmDZUPr9N71qspfaz+MTW0MvWWNOkzQLE/8DnwNDDyszf3V/48hTuLJE8eUPfQ2hA3zmBMkwtDy+/Ot1Nnl3qqJy5E8aVz57Pess+xJm8Pru/w8vV0vC4DDAlCTM6NzA3PDBbJ3skcixXP9xUCGV/tBPAE8hJxA++pLjgt3rBlNeX9dcYpTFhO344vDBcJ90knjDXR2pe2635wK3JycQIvLy1wLrtzaruTxScMEg81jtmMUMnWCkfPJVUO2iQvAHGbr4/tGmvArZRy3jzUBr0Lv0v+yRtGjEhvTntVTFp3r7pwcS5pbEjtLjFM+xfF68t8C39In0ZjyIrP6lcY2uHvQ66PbAwrbm5UN2/CG8lpSwgIz8Y3R/VPZtbDGqtuy219avCrE3B5OypGCEqeCTQFRwamDIhVLtner5wutmwza+kwVLmuRO+KaghMBN1EMcoDU5IY1q3trPSqb6rHb/96C4UoiOGHnMPrw5pLMdQI2NSt96wv6hzq+LBj/D6F/Ej9xrbC98RkDJaUgFjPLTIrHWmiKq0wXXtkhCnHFERqAY6DiwsrU57YA9kqq9SqBer37zZ4xwMEhvME4oGEAcbHuVCAlvVY8+1qK6cq7y039DY+oQYHh3zDgMDWgQEHco+mlNLXWldpao+pwarorvg2jD7sA0nDDj9ffIZ/UEavzwPUtFaW1rGp6yku6e/s4fNz+xJBb8LoAUN+TH1WgKWHWg7JlDHWN5YzFNjpRalSaqEuVjSFe0sAZwGDwHG+Dfz6vnYC/YmnED6TnpWFFdOVL+pV6fUqRCzVcTf3ObyFAWeCFoAqPUR8J/vDvknDFgmPjq8R0FReFQKUldNxqYGp1OpOK/2vJ/N1+Ky9fYB0wWD/zL2J+/66jDsUPbrBz4eRzIwQeNKxE/KUAtPwEqeRu2lQaaEqQuwtbp5yFnac+vJ92sAUgKz/Uv0Au0+6Tbp6u/4/DoNPB6xMbs/ZUjdTaNPa099TARJnkVDqDKoDKqRrbWzNL57y1XaV+nN8an4mPkX9sjyS+z/5rHjKuXJ7Hf51wfpF1ApjzbLQd5H3EpaS2RIbUYGRRxCjKlYqoKtB7JLucjFbNKE4ObrYvMX9EbzPu9v66Lk/uIJ42vp7fYDCkcbJy7MORZEoEceSQFIy0MhPus56jchqA+rmbGpuufH19ZL5VjwcfOR8CbqDuMU4I3gT+Zt9CgGvhmpK5s4sUCUQihBuD49Oj053qgarTK1z8Fy0nvijuy28DLu/+X93WPbq+L88dYIxyIZNSZAX0S3REtAATkmNk+oqa0NuezKbt+/6hvuVuqS4qXdRuGV8ZQHwySfOMlBg0IqPdI1mDKmOBauX7tkz9zj4+vm6ubhZ9qw20vscgp2K4Q8fkImQNI5hDVoPCOw1cC01fDoqu/15fDaNtrR430BOSExNzA/sTzQNeoxDznGr0zBQ9si6gbjB9aJ0f7dIPiNG+M2SD3qOHAy9DCBOmWvecSA2wbiFttg0fbS4+RqCl4saz1XPLwyHyzKNKurU8GN2MHhHdqt0DDRHuTmCAAt2TmsOOUuoCqxNn6sXMI81zDa4dHdyG/NK+Y6C1grqTgLNVgtBCneOd+tMMIz1QvYEMxMxz3NDe5NFswwhjkQMUMmuid0OoGvIMXF08DUi8lIxWPPke61FZswTzluMpcngyfoOAdRRMC50QDQKcTSvqvE1txgBT0lwDV/NcwteyhyLw1Ew1erxhfRksxRwLe7KcG41e/2hxl2JmUqGiP0Gasb9y8ySbiyosFiyTLDGLh/toe9htEh9FkY9S2zMOUoOB9MGzon7TzhUoVfhsJVyHvEJrxWuH+49sYN4Lz/WBzMKNIp4iVvHzkggSfHOblQwl1Av5HKwsxJxD+/vbvCv5zI89/9+4QY1SniMwwzjSvZIFYaoxuCKdE3hkvsW+Vii7wAwT68fLXZrmSv+LAQt37HqN4p9SsN/R2oJNQh4R1OFRESUBIzF5Qk0zisS2RZjmOPaFG//L6UvyK7vbSAsmq0GrZcv6nKJ9y27yUGRBiuIMIlbCaJI30eqxiqGAEZsB/9K3k5/EhkVcpfwWf7ab24xrhwtYuw8qoiqI2o96kcsfK7ncnu2qHs1wPIEH4bayFSHnIWtwqNB4QFcwY+D+oZ0ihYOjVNm1sAZHNpuWsst3Ozfa19qWKleqPao0anKLDuuxPNdeaXADgUdiDHJ7oo/yIZGP0V0g5UDQMTViK8L3JBMk9aXotjDmcxqp+oUKRAo9ihJaRop5ivPsNY2hf5FA0WGi8gryJVHGIaShGvC4QMpx92OXROVVv1ZR1ry20OsNKsLaaooqSmzrNFxoLh9/xjERUajB2yFlQOawadDzQfZDZwUINhO2tibwewo6rzorKjNKwvu67bVPs7D6IZAhWyDPH8wPivCiAnwkjRXd5mwWfRnoCZoZpHo4+0xtO5+l4SvBg5ENEEJf7mC1Ex61CxY+dqgWq/oGacMKC8scHRnviSD4MUaAcn+WkDVSLlRWlgNWqXagqf/pplobG1gd0TBL8SCQ2n+1n2EAv+MyFWemOPZiydOZqJnle3QN/IA0QPHAf2+P76YhWKQxpcUmWFY6iZP5p4qNrNvvlrCiQEe/ES8P8P4jy7WX5iql83l/GXYai8zUX1awbt9j3oFezpFcdFOFt7Xy1XPpPjmP2tkdWS9afyfuV531H3eCkfUGpeKVyNVKWYiaWDxrHuE/7Z9efnXe3mDZ488VeZXcBX+VAWnJauJtgB+4D++O6M4b/vIRYWQBdVt1aCTAWUrpgZrZXQV/HK9gfsKOJF8m4ajkSUV49avFIxSsyYKqhBytvrSPin7ZPfC+MiAH0sq0oQVZBS/UY4RBqbFKx2y4HozvG551namN/I9S4bOUC2ToRRVkpJQ19I86BvtxbYDe4/9ebo491q4G/0zBrsPOBOJFMGTdJDqUD0mBOjG7d20FfhkOOC3W7RYc9V4C/7Hx8UOYFIWE0pRbs+wzzvQnKjQ7Nuy8vdLuPu4YXZXtGe0Sjd2fY0F+szb0NfSEFFYj5UONE4JkGPotOuEcFO0+7fy98m2/PR9MvBzFbZhu+7CSAjEDcDQFJBnD4rOGoxfzGEOc5FZamvtrHG/dQo3T3cdNYX0IfLocod0rThzfgxEW0p1Dc/Pl9Amz2hOVo1pTE2NcE8NEgCrhC6Vsp11rzchd7n3A7Xc9OC0GfUUt577O4DNhqkKlU3hjzPPu881zatMgUuOTAcNnBCZ6sjtWPCMs501izYdNQJ0VTKYsdWyVrQnd248TsIBh6wLO80TDczNggv4Ck6JVor7TTmQdqulLvKyPDWNd/b4fDapNI5zxfO79Ml4Xf2ZA7IIpUyrDnSOIo04iwqKScq1jATPhpL+LoCyUvU+9mw1fbO98lnyVrRpuAp+OkSvSalMx414zBMKtshHyPWLqw/QE6bwCbOsNVA0YnKvsSqxbHOsuVxAuseziyrMa0vEycnIXsmhTdHSpe+o87x1XzRcsqhxlzL2dnl9YAWUCuSMV4sDSPQHUQmODoXTZDEINBD0LjI+8Gcxg7YUvnrHOgtuy9pJ1EfNSTaMxJKksIE0BvQtcdAwrXFetoc/wIdqCmuJMwbwhv8KkJD/lPGzWHQ5Menw8vJa+HQBuwjSSu/I10ZBR4iMyJL98MTzGXGAr9TwSbUFvkkGzgm0CG6F/AbEDItS/BXocwexrm+jsHB2CoA8B2eIoMVpA2aGM02VE4pxIbE270ZvEjJHuqTET0kfh/jEfUVHi3rSLlXLsuAwyG7TMEq2kgB8hoJHS8PFQdfF2E4Hk8bV/K967cjuAfIl+x6EVsgphn/DswUci3XSMVVz8EmvCu2CLu+0QH4WBVRG/0RgQrlFTQz/0sUVy3A7rh/s3e4Fs0S8VwNqRNVCjcAdgqzJwJFzlSuWEG5LbN7sr/AXN4cAEgTng8PAkf/Kg1ZLspH2FSCWGa36bHbsVK8XNdw+k4RthUEDUcBngQSGn03D03bVypaOrc5sY2wkrgRzkfsUQeQEDEJ0fvo8+T7fxPdMEdJvVN8Vj5UZKsiqQGt5rc5zaHp8gLuC4AKCwI7+lH4XQhvI3k7qEyqVbJXnVSxqcWnPKhyrky8ptK567P9wQVWA3T5tfG+7rj2iQlSIZc5GEvDVA1ZiFj9VaeoBqYxqA+wJL0yzijmaPwMCQ8LVwmh/9PzW+5089T79w2jIgs5B0lqUlBXZ1gXVmxSdqI2oT6izaYQr169AND345b2BARmCi4I/ACX+lXzu+4Y83z95wsvIPs1u0V1UMxXjVvVWjlYbFTRn4CejZ/VodKmrq/ZvXjOw+Fl9WMAFQMqAvL8tfjz8TXxE/PB+PMDbhXLKo48hkmIVPFY3Vl7WHpWp1L2mmWaD5wunyCnrLKmwmrVtuiW+Cf+z/up+NruJulQ6DPotfDWAL8Xfyy1PlpN5lbKWf9ZkVeXU7NP7ZkqnKmhaqsSvKXSGuiE+Dn/pP2Z9AHrcORQ4krpGvmhDxortUPVUtlZAFvZV8FRykt0lNCXs54nrR/Efdps7WP0HfLn6Vzhfd895rT5JBlaNnNNJljlWktWBk0IR8WRzZe/pbm7e9Y06s/wi+044nLbkt2l8ywUhDa9TrFX9Ff4UbVNKJNymPKnmMNe4JnyXfO05S7WGddb6goSBDleUNhXklV6UOtP6ZXupM2+Nd538zP1VuSY1iHYlPQ7Hbw/y1HYUeRJhEG4jneYarEU14Lq6+Xo2uTT9OXKD/09MlZRV71S7EiNSU6WeKlLyPLb/Ns5zr/Jz9mpB6Y1jlGZU5FHMjyPSNSbxblB1IbgQdPCyUjUXfjPKLlIhFG+R5g9f0Eul2CyetKB2ozRIsTvy6HuBST2Q9ZK5z8ZOWo+xJYarzXLUdQuyyHAnctd8PAhC0J5S61D2Tb5PQiZ97OezwDW5csnwt3N3fX6KN1CekbxOGgx2jjplD+qJMXhzcHFGb8EyY7sfh+mP3RHXz74Mdk3Mk75p0bBZ9CQzzLEssFR1C0BczAVQ01FVjb2KRQ4SFFpq/DCvM7Gx3q9EMIo1jr9vyspQtREtTn8L+cxkEEnWtSvpcTPxr299bQiuPnNYvgbJFI77z9/OYMv9yuIOkJQz6WYuqHGG8XjvYq3zrvyynfsLRpANs8/MDpQLtYltioOPwlVwqg+u27IwMi8v/G4n7gIwVLYkf1/Gl4y+DdvM5AqcCSMJt4wfUTUV2WrcbxCxlfDuLrJtHm1oboVz37r0QuzJe41+jsnOZ40cS43KS4uzzkzTDVbjmSPvbnErsFwvqy4Y7TXs0S7ec9w5mQC8Ry2LUo29zboMUErYR7KHI4exy3/PvRPY13Hts++n8MiwRu96LYus/qxy7ScwObRmedB/4UTzyDrJYsoACPZHGoYnBkiHbApYzs3Sl9Ur10CtzW8GL/Qvdu5Z7NXsue2E7/pzKXe8vGhCAMXjSIrKDojVh1+F9AUABcgH6AwykLZUZZdqWJrxpTMDstbxvC9+bqquMK81spT3rzyHQkCHMImryd6I6sdLBilF5ofrCswOZZLwFYHXhq/Er90ulS1ubIEs/O3b8U42tDzhgo2G+odcxzXFm0PKAwDFPInED1nTARY81xFv7+4RLN/sbezk8GC2ob2LhJwIPYkfyIzFywRnhVcJL09C0+RWetdkcL6vVu4cLhzwCvUN/ITENweuBroDvYGNAcRHZA48027V8bBYL7kt6K2Sr4g00v14xLpISUd+hWgDZITcivlRaxThFllvTu2lLKPuyXREvVcEPIZDBIbBooH1RtJONhOEVe3vIS1wrLNu6vT+vnHFL0WYgaPAMwOPSyRR+1TUFSls3KvtrdGzg7zuw5mDr8ASf4tEtQ1I01GVIy4BbGEshnB8uCwAU8J0P0h9tkD7ij0RsBRyVATsfywsr1W3QP/YQUI+fTyxAMFKmZH71HUUOOxWbVHyTLsWAnCCxD9qfeXDIkzR0qxTxZM5a/3tJvN5fOzCiYEzfaQ+Y8aXzw4TVFO169ZrQy34tMc957/Y/RF6hX4wBuqPyNMdEqZrHSu/L1b3xj8vgK98nXtYP0MJLJBnEovRweriKzhul3YmfZ/+cfuvOjr9lsbhjrwRSNEFz0sqQSxRcfK55j3JfKR6HnsywQGKQpC5UjmRNs9SKpYtVrNW+tW+WfweeSq5Ov7uyEMP0VJD0hGQa0/LK8swJ/cR/T09ELqOd/E44H8MR5CO5dGxEVuPp86k6h5s4/IP+OF72/sz+Gz267hMPo8G/I130J+REI9yTYPON+ptbejzPviSe716wLk9Nsx4wP0PRBuL60/KUWCQSs6LzaZNyKoRLShxhbcBOit6ivkyNsW2ZPfx/MYEmEspD9HRphFSUGjO+U4AT5DqTq2XciF3e/sN+1F5mDeM9YL1gff5/ImDuYmpjd+QC5C6EBYOrg1xTRMOVuln7AWwA7Sy90X5iLhXdlt1DrQKNTh4Qr2vA+RJO83XkINRaZB3TwINsIzzDaZQU+nRbMCxN7V6N0U45/e99hN0a/NFM4O1vzpUwbkHoozMz+FQ7lBsz7aOOUyDTMmOvJFZKh6tQnHH9TA3ezdE9jH0E3Npc1V1oHoEAFUGq4vYTx/QlRBDTvtMjAtHi4iN/xG36emt17JsdcG3pHXydBGyiXKaNGf4rn98BpkMV9AbESjQao4yDKAMqQ5dkk+qBS6m8xn2ELbQNQyyUHEjsj81tXznRV1MLg+PUFCPMMzli6aNc1F6FjdtnbLL9aa1F7MxcRbw3PQQO1lE0cuajzGPNwzNyojLJ46M1AurfTBnc/hznrE0bzZwDbWu/vkISs6Qz6XMyEnxybHN09S561zwjDK6MKluiO6iMnh7RAYvzRyOSowySVdKTJBUl2PuVHL/8jmu3m2acGJ4zEQsC5ONoYrcyBAKKRCDV/ruY7Gx70Zstixkcgx98Egsi9UKOYXoB2LPFxeILjew2K+o7XCt8PUDQMlKfIyBCYAGl4lb0cbZQ69isBqtHOvgr9+6y4cnjKOLCcdoR2hPHZdmG4XvXGz5quTtvbc3hQyMbUyqyCNHCA4rVk5bf+6x7HFp6Os+M0EA/QjKCabFN0MjiS5UEdp0bTlrQamgqtaxwD78CJpKKYZdQ7aI3xKMGZBcOiyRqkkqO28mu0XGs4p8R45DlcVEzezWx9sv7HfqPehiKlCxvj52RwRIdAUgAiEFok8JV5QbPetWKbAoKqnnsNS8j4WhhvRENL+bwNGIytK+GA4aCmkjZ19nSerV8sO+FAUVRpLDlT//gVnIQVH5V/zaOJpEaHJm2ygu7MX2B7+dxS3FI0EpPosAuod9ECWWsFmzGiSocma7Zi7oKG1/Nju+dwMfw0XAtf3BfgoChso5kVmXO9ltmYgYiGbtJpmoE+wRs2T7fUGbhJCEVIGkfmi94kCNhv1Oy5TRWAWZaNjSl/bmU6YEJoNoRmx9ceg4tX4owTJAtf7m/J28UD3UAsRKJNC8VQbYeJk0WbBY9tf5pyOm8+eY6fJtajK/OKu9pICIAUK/4X0G+766EHu7fnRDUIlez3gTtZZiF26XbRaGVfvmJGYXZrinwWqC7oc0hbnIvfQBeEExv7x9qvrkeaA5UvyGQQrGvQ0kkf2VKxckl5UXr9Zc1bwU4+c9p3WoR6rq7n5y87jXPKF+u38v/lx8VLoCOaF5pLtu/0oE58qKUBtTPlUgVaiVQpR6kqMRleaepwvo1GtwL090a/l2vUE+zr6t/KA6xnlM+cQ9LIGrB63NtpGMVE9VOVTWlDVSgVL9KHjppCxCsIQ1f/pfvRn94jrR+dv4LzgjOyGA7EdrjcERrtNY0pIREo8bTp2oO+nZ7QlylXfTvBf9LHvk+Cc3BvgUPNBD3csNz89R6pG70OUPY0/RamQth/OdudY8/3whuYX3xbjXvWtFRQ1GkXySh1JekFyP4Cqerd6zlTmmfdw9c3nPuGx5t/4ShkrNKZBKkEWO5M2nTwdskDJCuV98OnrU95X2mLqkAvNKnY8SDsBNXwsYzaFsZ3F4NvF5KrcS9Zz3Cj0DRkuM8o7RzgGMnc1f7F8xaniZ+qR40HaK95J+LcdHjaJOqQw1SpFNJO2ys6P5LbmM94u2E/qMxC6L8o6MDbMKycwjkJoysDjIeor4K3ZP+eSC+QqrjcjNfcraysWPfjCe9cQ2wrT4c3G2Wr7UCB3LhQvCSf9K+o9Vsja3gvle9jD0xzh+wNdJRUyvCtnI3UlSTyHybTfN+Hc1WvR2d1a/g8gTi7GKTQd8SCIMwvArNIg1hnO8saSzsXo4gmxHi8jixrLGGYmZD2syg7W1dTbzqXLNNlr97UWTCbbII4Z+BhUKRQ/LMv31fvRT8iEx5XSBO4fDjkgmyJVGlEXvyRxOCtHUNeA2zPUEcyrzUjeqfpoF10iKiLiGYITZx6NMfZBadBt1iPUw8qEx5TRR+OT/rQWPx9EHwkXmhX1HLEpjzsoR9rT4NOTzRbLCct/04jnJAQlFxoe7BuIEnUHrgcFFX8p6jmqQ73LMstGyKTEIMQSy+rYR+0/BKoTjBeVEXUMqQnpC2QZVS2QO4lEp0i/0aTNE8jgxZjIzM5O3yL0zAjjFmsaKRqcFaoO8A25EsIg9DDGPYFFbkhl0WLORMmjx7jGo8nt02fk4PipCZ4U2xnZFy0ToQ46DOYQSBtRKXI3nj8/RPtFzcp2xm3CpMCHwnnG6dBo3rjxHgM9D1oSUw9DB58C1f24BGAPeR41LnY6ZkHuRN3Jb8jUxPjBHcNJx/XPF+AJ9PMCNg/zENEPrQb+AL8BiAY0EXUj1zJvPWVCkkRaxnPCOsA4wLHCmsn41wbtogIZEY8UFRCICTAEpgZaDWwcdy3VOg5D1kUqRTfG98BpwBLFns5J3x3zMQN3CrAH7gEe/ML5OQWaGlEuNDzpQp9Du0FFv629ZsDcyqzdcvWlBNcKFAT/+xT6XgJwFNAs/TuMQkxCgsBXvdi98sR41Svs/gAoB5wDe/gT9lkE5h06NKw/UEGvPLe6brslw5zVRe/MAZMEWvxg+Ar+kxbMMEo/OUPyQLO8P7w7xKTY0PGs/+/8c/Qm9IUHdibvO39Cc0F7POy8McWM2Un0TwFO+1TxuvTCCswp6zo2Pjo5ebibu/rKceW2+dP2QOxz7QUFkSM4OaE9Vze9t968kM4a68P4IfQo6cTtPQ3ILNk6rTonNJO4ncM43ej1EPnm7ivrLv9hI6w4HzziNnY0U79O1NDwGPo170Lp6fkHHNY1PDqJNO8vNLvJzEjniPEp6QriB/J5FqYx4jgyM3guX7q9y6nlCPJ26O/g2e0aEUEuhTekMZwr1rfKxsvgje495gTdzuPQA48m3zR+ML4oJy6pwTzaRe/g7YLie+Sj+0gfNDNLNXQtVinjNbXJxt7Z6Ovgndgi4B79mR4yLlgvGCgWJ+U1L8sU4aPpn+MA3DrhYfpKHYQvFDH/KDklbi2NweTUOOWH5BPc4tj25PH++B4iLhouDyVOIZYo7TnTz4zhhOQR3fvVhduA7k8OxCU7LssqSiKqIEcrvzo40Ajf7+JI3IvUtNR64Gb4DRPaJF0osCSBHQAetChZN1/Ma9tn4WPfbNe0097XruV+/b0VGSODJSghVxq+Fscc5itSOg7P8dnP3aLZQdT+0VrW1t+E8iMKBBvtIxkkOx9EGq4XZx5PK8o3v81F13bdOdzh1nnSzNEW1anev+/JBVsUoBzzHl0b7BULEVEStRmKJfUyfjxQ1KfaRNvU2J7UZNE/0ZjWdOHz8bsDSBOFHIceLxy8FgMSYxCyE4QdHCkbNL87W9U72j/a5Nbj04fR0NHa1eHdr+zV/aEN4RjhG8QbsBctEqQQRxIhGYojOy7vNtM77dif2r7YhdR80bXQINO62ZvmifX5BoMTUxlCGu4VRhEbDvMNYhQ8H2orGzUnOo/a2Npu2DnUl9FM0a/UU9wm6Uj7dwpEErETkxHcDPUIhguWFR0hLi2MNb45V9z62j7WJNN+0vPVH+CT8MkCBhBAFKQStAzeB50JtBK/Hxks4zMiNz/bctjG1JvTctY74IzwNgPiDxESrQ7DCKQGgAysGhspxDFGNf3ak9Zt04zTBdr35/r6dgl8DncLCQaeAz0NORyXKTkxjDOH2ADU79Iq2C7lx/nQCaoNEghZAlcDkw8UIfcsXjHB2fnVRNSh167ktfh8CEILIAU1AOQDYRLQI6UsAS/e1jHUi9XX3gzxyQNjCj4G/P/uA6QSCSQ8LEMurtcl1YHXUOJ29SIENAX3/cH5ZwUqGGcmZytRKonVv9XV3Q7wrgG2Bq8AKPtcAkIVlSTvKY0pq9bU1qjeHvDlAIkEgP68+vcBthRoI6snWyYn1pzXzOAG8oQA9ALn+5j55ARwGKAjLCaeIyzWI9hA4QHy9P3P/Bz3fPdWBZsYwCOxJWYjytfe2pLmw/fxAJz9sfbs94AFkBjUIfkiVCDT17La3uP68rb8dvvt9hb2mAA2EwQfBSJPIBwdcNnP3rbrTfdj+pX16/Fd9UwFKBYkHp4fhB3QGvTayOH77ib5yvvW+BH08fcyBeITch3MH/sdBRr82cLdiuVt8On41/ca89zwovfvA5oS7xvYHsocyxksGVHdgeOC7tD3LPsp9+LxkPER+J4E+hBKGSIcpBoKGOYW99vg3lblN+7x9T32jPI38O3vVfWWAZIO0RcPGzsbVhnnFfMVp91L4bPn0+/s9H/3SvV/8ifwIPL8960BGQxmFYAZIhq7F58VQRM1FHLeWuIA6C3uRPOU9Mjy6vC87u7uyvFr+EEBnQppEnEWuRYHFr8U1hF1EdAVYOHp5aTrW/IU9pD3x/en9erxU/DU8CH1avySBCsM1BJ9FdcVZBXxEw8R4Q+oEu4VQOKZ5vDrFvEp9LT1jvXG8hrx2O/p8FH1sfvKA4gL+REXFgYX3RYJFPYQUhBXEW0UShdz5NPoQ+7N8/L2R/a18wvxNO8679/xqvWU/awF1AuUEC4S1RLmEYYPPw/eDkQRrxVd5VXqfe888+v0SvPK8d7vBvAZ8iD2XfybBDcMdBAEEjgRzg2NC9sKUg5aE/AXf+nO7SbxAvOz8Ufvc+0X7S7xRviYAekKsA9UEaQPzwygCwQNIBLTF1nqNvCD8/b0F/J173LuVvAp+B0CygrWD1wQFA52C1oKSw7XEw7ohO1w8ALwQu6q7OfuH/Su/WUIVA4dELgNowo2CjsOkRXw6u7v8vHL7kXsRe1e8wn+zgiJDggOhwoZCW0NWxPSGVHwpfLK78vt3+6v9asAVAu/DUQM6gcJCW0P0BYe7SnwGO4R7FnsXvOr/mMISQucCCoF2gf7D5YXkO2J7uXrOevg7n35CAa9Cj0JCwYsB0EQpRdB757vVO067Xny2P7BCU4Mggk7B+cLYhTJGW/x3u7D7P3txvZjA8EKnQg9BB8GVQ/ZFpHuCe5964brEPLj/VIGnwWRAUEDzAubFCgY4+z96ufqofAH/bQGAAd8Az8EDQx/FJQYZu+A7AzsX/EP/S0GNQc+A0cCzwjhEhAX9O3663nrOfBv+2AFTgdXA1sCqgc8EXgWahd/7Cvrg+0X9RMAbwRmAV3+SADBCdQRSxV+7JrqJOv079D5zALnBMMCEAEbBk8PpBQfFmntq+v663PwxPnUACAC+P66/BsBtArZEUAU3hNL6/PqVe0d9Cj9vwCRAJ/9Tf3bA8IMihJrFMgTo+w47MTuevXX/bACTgJC/7P8jgC1CNUPDxNXE/8R6etL7C3v2/Rl+1b/4/4m/Cz79P03BAQMihBYEvMRsxAK7RbuwvEz+Gr+JAL0ASv/E/1T/RcCCwlRDvAQdxGfELntp+3o7h3ym/Z3/AUANQD//XX7Hvtv/d4BnwhADa8PHRAkDy0O3u2u7sDwuvSD+ZP9Qf8g/9D8tPu/+/j+AQTYCHsMLw61DmwOSw2hDALv5+9l8jz2xvq7/aT+y/28+3H63fqd/coBhgb2Ck0N9w3tDUMNaAz575DwDvKK9PL3IPyD/rv+lv1f/Fz7B/zZ/dMBGAa1CbwLjAxMDB4L3An3CTzxkPL89E34kfvb/V3+Rf3m+1H7Ivs7/XcBvQXeCLUKQAvKCvYJKgnrCIfyzvMm9pD5iPzC/UD9Wfxl+7z7df2mAL4E3QdpCQYKbwmBCL0HPQhL9P/13fgo/LD9rP2a/Ij7b/um/GcAmQRVB8sIDQljCHcHWQf59CD2svjH+3/9E/3/+/b6W/ud/YUBIQVEB6oHZwdCBjIG0vU59/r5kvy3/UD96/uT++r8OgBUBIYGPAe8Bu8FmAXn9l/4x/qP/NH8vPsV+9X7wv7BAlIF/gV4BZIE6gTc94L53/sw/dv8z/vb++/9WwF8BHQF+AQHBD4E0/io+un8p/3Y/PP7nvxM/6ICagR0BHsDOAODBI/6h/yN/Qv9dfwK/ZH/lwIEBPUDNAMzA4QEtft2/dD9//y6/AX+yADnAlwDpAIkAgsDfvsc/dP9X/0O/c/9NQBuAhADigL/Aa8C/gNS/Qr+nf1R/Rj+JgD/AXACCwKzAXICiQPo/Un+6v3G/aH+dADOAfQBcgFXAScCAgNS/nn+G/4j/gT/jgB4AVwB+wAFAcwBEP6s/qL+XP6D/l3/hAASAe0AlAC8AGUB2wHz/uf+xP7q/oT/WQC2AKEAbwCEAO8AOwFN/1H/O/9P/6T/IgBoAGQAPwA9AGwAmACh/7D/q/+w/8v/+P8dACIAGAAOAA8ADgA=`},{name:`dismember_left`,sampleRate:12e3,frames:6480,wav:`UklGRsQyAABXQVZFZm10IBAAAAABAAEA4C4AAMBdAAACABAAZGF0YaAyAACXAFEABgCR/wz/nP5g/jQDyQMSBAAEtQPdA8cETPyA/NX7jPqQ+R/5UvmGCGMJcwm4CIIHkwcoCcP30fj999n1OvSP89PzFvXRDlYPrw7uDGALHQx4DvkQrfS/8/bwju5z7Zrt8O5v8ZcVvhVnFO4RjBBMEfgT3xY98Kjv1Oxg6UPnL+aF5hroqur2G5UczxvWGQoXLhWWFYYYKhwAH9nqYuqY52vjaOCu3vzdvd5N4CTjDeYoJXolgCQfIiEfmxvEGV4aix2kIWEljChXKoPkm+Ny4NvcCdlI1jPUL9Mf09vTdtVE2N/ajd0f38IxvzE9MfIvbC1+KlAnBCWWI8gjNiXxJnMq0S25McU0ODfEOLw5y9lb2arX09Ry0SbOlcsLySXHw8Raw+XBRcGnwJ/AgMBEwQvCpsOrxH7Gk8i2yQ3Mdc00zhDPlM9kR3xHd0euR4lG6kXlRExCOkCGPXE8sDoSOIc22TTOMws2HzcHOY86QDvUPrpCoUVxSPNKLE7LUFtSUFUmVjxX/VcAWYzEI8KHv+m7WraUs1iwN62zqTenuqT0okyiy6EGoluiv6JTpauooqtwrhmzqLhBu5i74L4YvFpg3l/rXp5by1ayUpJNvUgURhNFfEOTRtpLq1KeWP5dnmADYrti9LhJszytIKZgoOCc8Jrgm7edW6BFpaqrqrLyuEq60WPKXoxXeU71RD0+yj7VQ2ZOH1kAYdBkV2aktmyvqKYWn+2bk5thneujk6xXtIi1TLCFXKpRBUhXRDFJb1NzX3Zm42j9ZketiaH/mnOYdZqQokOwPrxQvtVlhlyxUIpJ806eWWpj3Wh7aBOsQZ+emBqWv5lWo1SvDLcqsaNeZFC1RfJM4VksZIhpEmjnpq6aeJX0liOf6qyHuuS29GCcUfNImk4NXRRoDm1QahqmN5t9mNSamaeot1i/RrReX0FRLkzEU+Vinmrpa49mfZ2dlQqUcpidpOKwaLOmpp9TJUc2RoVUmmS0a01sj2XJnUyVvpO0mNql/LJmuO2uFaLjUu5PQlcaYnJqDW7ba3plopsJlFqTHJhuogWw/Lf3s7ekPZkpTBpE/0zhW9JlOmuMa0NnGF2RlhKTDpMil3GfxanrsAOy0qvEoGKYIk66SDpNqVbAYb9qfm7kb/NukmfZYPxW55aflQKWnZmpnoyleK21r6+vO6goo2abdZankkND9EEtRgpPWFdFXU1lymkxbHxtZ2yCahlmW2BLWeZT9ZRvlaqUwJZomMWbd6GNp++rwbHwspu3EbnzuVO337G+rCGn1KEsngyaBJYYlNtGqESrRWhIRUhGSfdNQlPzVVla8V/VYpJlrGl4a5FsEG1bbdds/2saawlpB2aKZWRj+mD8XXxapVYgVA9Q6U9MTDpI65Pdk9OU9pSVlByX35e/mTid058no7Cm4Kr0rxaxDLSHtMm0a7Q/tYuzp7G0sPqvnK/AraKpTahIpD+iM587nLKZy5bSlHNBz0AZRFFF9kiDTgxUhFplYCtkqWfQaQtr9WvqasNp3GbUY+5fEVrsUgVOrkgCRkeXr5gQm92di6LXphqrlrDEt6u6oLc8tA2tw6Zcon6eqJvzmoZIsE3HUjRb5F+3ZNVml2X7YKNcd1VBS3FEnEN0m5ufYqaFsWm5c75IwP62LKugoxKfBEubTRZYw16iYThjCWEEWkZPsEO/QKacKqMWrz25Mb0etyStO6QinwtHn08lWjFhZWNtYP9W2krdQv2fXKa8sDa9br50spCmEqE+oUNQvVxlYuhhF1zrT9xExUOHp3qz4rzxvcmwTqQDobtHIlX1XU5g+FxRUKpEtEWcq3+6hcSxv7+wr6RdohBK/lZsXX9d+lVWRp0/TkbIrrW7l7y+sVyl1aCmpEpRpltcXppYKkz2QfZEcq/7vNfCPLqUrs2lfqbnTsVZ5FyAWc1O5D9RObtF4bEvve67FrFnp6GjYKXwTExW6FnuVLVKLEBuPe9G07Y6wprGf7yVr+moH6jErQlVB1v3WqpVN0wKQ8k9qkREUGO/kcWgv+yzNau6p3upaLC4U9xY0VlzVu9P40XbPZo+y0WPTii+OsOVxIy7vLG4qsynG6dRqiCx71A1Va1WVVQXT15Fhz3oODU5BUCZR/BOf78UxgnGZsLTuvuy0K3fqvCpeKqzrBOys7flvt9W7FdGV8FUVFEdS35FFD+jO707lzwyQVJHLkwjUW1UEchHygbHvcQGwCO827cRtLqwZa2Rqz6rAKsNrEet7a9Usuy0hrnUvXRUF1arVmFWoVXBVJJTY1HGTtxJREfUQ0NApjwgO1U6djkaOQ89jT9DRI9HlkwnUF9S/1TpVq/LfswLy6TK5MUawpW+wboruHq0CbKor3KtlKybqzCrharZqY2q06vtrVOxwLRPuKu898A7xrxXAFncWN1X9FYcVYlSrU+JS9xHqEJwPp08/DlLOww9H0CURD5Ijkw8UB1TX1XrxcLGD8M8v1W6RrXKsXCuEatIqQipMqtori6y6LcYvO3AuFdqWOBWOFMzTkFIHkLmPbw7wj4ZQwZLYVEeVnRY58WhwSm6FrJ1rFGp36gQqqCumbY5v+jHulrqWJhTrEy6Q749ND1/RH5NEFVgWPrCTbuzsrGrXKhxqJSrcrJfvdHCM1loVD9KPz7NOTg/AUqeU3hZy8L7uQ2w76hzpkGpu7FuvDDCgljeUIpE1DoRPdlI41MXWrK/pbWjqrSkJqU3qzW2GsB2WwNVDkk+QJNCRk4OWMtcO774sBqniKOzpp+xc775w9RaW1CjRP9BykxGWddeEV5Fs+ym3KEspDmtXLmgvKhYvEztPi89Y0mTVtpdzF0asjimXaFho5Sr37gSwDC52lNlRfY/4kZQVNddW2DqXNSqHaKooOaj662Qunm/nbiLVQFJTkNZRzNVPV+YYglhsq51o4eeL56DoyatlrfFvCK2Z1fMTMJEcUSGTQNZJGD3YvlhT1sopLydMpxxnZuisqu9tfy6sbmGsVpW4kstRKNCakmWUndbn2FJZIpkJWF8XAGkyZ4unL+bZJ6Mo7CqerQ/uqK92LoftPuseqWTUHdJJkRMRF9GTEz5UzlbxWC6ZOJmFmgTZ/5laWIIX0daCqDzm/uYCpcCl3KX85hcmmKcOqDrpC+qYa8Is4q0Lre2t6C37bP9sECtoamSpl2jlZ/aUyVPeEyaScdHvkZrRZdGDElGS9pNo1FjVCBXill2W6ldr1/eYH5jJmQ0ZVdmVmcbabZpiWrRapVqemt6a8BrjWuvamhqxmjWZwFngmWnY1Bi6GCUXk5ci1vlWDBWoZnQmC+XCZZClaGUdZQGlLeThJNCk72T4ZPXlL2UQpVCl3aYjprdnOaeSKKnpJOo5apSrDSvXbDfscGzCbSHs6G077EzsMmtr6vJqF6kyaALnqtYVFUOURhON0ypS2BNvU+eUwtYLV2EY1xnUWonbJdtX21Hbd5qc2ipYzJepFjilmuUGpMrkiyTB5bumqOh3KdPruez27ZEs3OuaaRnnVtVtE8JTfpLVFDAWtpiQWoBbThtWmuMZ2pg6FeGlICSXJNOmF2ggaphsjO19682pyOdXZcoSy5LRFTPXlxonW2Hbo9rCGO8WGiV65Jel8effqxntWS38q58oIKY1U13To5ZCWVma45r2mYVXSxPoJKZlAyc6acStIO36KvfnU+VJEgwUhtfrGgFbP1nVl26T0iUTJalnYKr0rSOszOobpydlfhLYVVZYsxosWkjYaFSu0Q4lFOZwKdntdy3nqqYn3SXFUfiURpg0WfbaCFltluSTViXSJvLpQyyRLt+tbGlSJrNlSNHjlZcYitp/Gc3YtxXAk6DmUydsqdCtAa7tbXZp8OckJfAQ29OrVoTZSBoKGfxYVtWj0s4SLibFaPirGe2nbmUtWKrc6BBmg6Zu0fLUaFcA2N5ZeVkf2GxWGNO8EbZQ7Gb46DfqEexcbZNufmzGKw1pKWezJummribn0uuUsla42DtYyFlTWPBX25aBVQUTq5Ji0XLRryfgaPAqHivPbMvueK9575KvqK54bJLrFCo46Wvom2gDp/Fntmf8UqUUX5VSVpmXgtgJWNIY6BjiGMRYRNg1F1CWq1Xe1JtT7RMR0ixRxZFxkP/RBVEgUMKRiakxKVRqCSrra0YsX20U7kpvlHBFsMfxDXF8cbbxNjFdsKzvZG76rjatayw1K0hq9+oC6cppWqk5KMXo9ejTaOeok6ikqMLpW6l8ETWR1BKBUyYT1lSglRSVnJX3Fj1Wahae1s/W51bIVplWcpXXlbDVI5QkE1uS65GJkUiQZ4/YzysOxk/F0H6QkWsP7A7tAO4Fb1ewZTFf8byxa7DQ7/su1y4QbO1rp6qwKi1qNSnzKe3qWysr0tgT1tTaVb6V1JYLVgBVWNQWEsgRu1AcD0TPbM/k0R8sTq3mb0iwj7B471ytlCwd6vnqBWo4qdOq3pJUk9JU+tVoVQwUDtJ40GhOIo4K0Bns3m8AcfBybvEk7x5tBOuDqw2rj6zMFFFVoZX3FNISxlAUTYdNylCVrbQwDHE+r4ktgyu+qrnqzWyNVEJVSBVr0+XRRA9WTtZRCS5XcJMxYW8hLLBrDusoLItUQhVnVS5TtJCujZoOARFgbkmwUa9lLM2q8iqpa/fuvtUWFb+Tz1GUD0tQEtJ7L9ywzO9i7Fgqwqr3bBvUddVblQTTTdBWDuTQr5O7cT6wqu4wK0BqhesurPqUlxVdlGVR9g8Hj2+Rr1REcsTyFC8DbE4rP6tX7XHvv1VL1HRRx85YzQjPVdLKlSCxQS99rNfrDWrGK47t/fB51YwVQNOsUEPOg08F0cFUjPF/Mj8voqzJK0oqtmr27KdvJ7DFVcYVGFN4kEPPNE740EkSfRRRFVuwGS7X7MerFioM6fWqWOup7awvWLEBlmQVulRpkqFQjk+Xj/vRFdJUVCrVbdYocT3weC71LPyrFmpFacfpuKm3KherZSykrjVvjTCh8L0WBFXI1RMTp1I+kM9P709MDyFPTpBIEaKTI1RhVWzWGNah1w/XYrEI8P9vlO5FbW+sViuzan8pwan2qROo56jwaNbpL+lMag+qtKrmq+JtOq3qLtMvqm/csJowkDC4MEtXWJcb1tLWkRYOFWuUhhQMU2RSkpI8USuQQZA+T4zPSU9iD2rP5o/iELPRV9HoUrhTshPylLbVTpZtVqlW8ldBF9QYKxhp2GmYcq8WrtsuWC1kbILrsKrtqnopfWjtKF3n12fUJ6bnq+dwJ1fn6igK6MYphup7au2rzy2qbnfu92/Tr7JY49iCWFhXlRaDVeJUk1O+0pWSs9L7U8uUnZWCVtPX8NixmWUZuBl1be3sx2uUKe3o8SeI5wdnCud9aCYphStcbRGubm9/byQZOFfHVhfTwNJ8EYzSnRRQFpUYQlmqWg6aJmzrKktojObUZmImnufBqdLsLe6/7syZXJgdFbaTLZIOExRV5hfrmXKaApne6rCn5qZaJdemJyfiazatqO6VrPjXjxSJ0shTuBX5mHuaNlpS2Y0pKeacpZbl8+ec6uut5G7s7LAXeZQXUpgUQxdMWfPakto7qKRmQiVPZUAnKWpLrffuNGrAFqxTTlLxVZCZOBr7GzUZ8ihPpgLlYOZIKQNsjW3vrDwXvdQeEkeT55bsmdDbM5q/2LUmpuUBZRYmXKkArGLtVisDl2rTm1FBUwRWzxnkWzbawJmCZz5kz2Sq5TcnNunnrIss4iq9560UoZKO00eWJxkLGtabsRtE2hOXkSY4JR0lXWaraS9sJm2qLa7roGjUViCTdtJJUzHVA9gnWdBbA9trGq+ZWhc/FImkhyQwJDDkwOZR6GRqge0Q7fCtvuwsKiYn5eZGFDISvFJeUwaUgRZ1WD2ZqlrGm5+bxRv0G2LayRotmIiXaNWSpfelMKTipMnlE6VqZcLm1qekKHAprWqqa7SsHuz4LW2tfO0irOisByu2qqKp56ju5/5nA2b+JjoloJNvUr3SCdIMkcgR0NGMkaxSGdIPEqkTKpPpFKdVNlWBlmIWo5ci11LX/xfFmEHY1FkHmYbZlNn9mjCaWZq5WrcaihraGqOashq2GqeagBrMGuoaqtqSGvKa3ZrK2tEamNpxmj5aCZormZdZXhk5mOgY2Zi7GANX2xeWV3QW6lZKlZFU3BRuU4vmr+YSphPmBaYQ5gPmImYaZnCmXuaEptRnDGezaCAo16ntKncrJmwC7Spt5W5KLq1vKK7f7qpuCi1l7FYrfqpwaZAo9GfsE0VS1lJxUp1SjhN7lCOVItZsV1lYbZjl2Q7ZURkP2G1XTVYEVLRSnWc8poEm3icF6FApvOrVbJMt1e5qbcVsuuqmqNNn+9D1kI+SBNPz1ZmXcphaGNvYsheyld+TnRGFZ9noYqn6bCYuVW/bb9otsesnKRaoOFB50dsUo9bJmBhYaBe6FYDTA5BFZ+qokGqp7QPvOi6rrK0qHqjOUOISfFTgFzpYAVgp1nITmlEJaIVp02xQb6Rwyu9b7HNp7ei+kTRT1JaAV+hXSJXx0sTQdKk8aqatfHA68GguPes8qVBpctJplXcXL9dR1nUT/pDtT++qAGzs722wI+5f66RpmOksENeT+dXtltiWa5QVkSbPFSmkKygti/Bwr5Otdasv6bApcVFy1HvWe1adVh+ULFEmzxhPvqrgbVCv8XDp704tNyrsKdJqdRIb1IvWFZa1Vj7U9ZKAUJOOUQ+xq3JtMy+KcZaxPu8oLRyroqpCaljrIpMkVJiVkxYdFfkU21OSUU4P944VjxPQyeyn7ctvSDCysE4vbm3mrGrrNypUanMqsyrl68PTD9SzVR7V6xXCFb7Ua1OpkrxRJ88uDjlNvA4Dz3UQ8ZH0LQXu/TBScZ/xxbFTcTOvsi6qLdds46vLa0zq+mqNallqgmrGKz+q7quGrHcSlBNV1FrUoZUDFZEV1JX61eUV55XglYFVSpUgFGGUL5NK0puRohDhUBcP/k71DtVOu86ojrwO+s8pj+qQZ9DKkbkR+G0SLajuF67fLz9vmDBi8P9xV3FYccIxWnDfsMNwkPAt7z9uQi4/LV3tYWzqbEOr3Ctj6yFrKisAq2VrH6tFa3jrqavKLKss5W22k4KUSlS0lSBVl5YCViHVydWtVQkU+RQxE3yR4ZEFkFmP789hzwNPao/XkKCRyVMq1DEvLzB7MRWyN7G5sXGwZ+68rOHr3GrUKkMqPCpJav6rt6zZFL+VsBZLVtLWulWc1K3S5hGC0A8PYdAHkbZTXO6xMPuyWHHs8Xpu8Sw8ar+p/inKKrerja14FXrV/hYZVQSTalDeD8SQHtJDVIkwILDDcKvusCyOas+qLWom65at5NbuF3iWixTE0iEP84/VUaiUTi8z8AYu8+wsqhWpIalF6weuN1bJFt6VmdLBj+bPgBJIlUovpG/5LWUqeSjd6MxqTqz0lvUXJJXs00rQwdCV00AWe2/EL+zsJ+mKqLXpeWw1L3mYF5eUFVJS79FKU28ViVeVr9htbOplaJ+ogOpqbbUwl9g61ibSro/lkNzUPNbOV91tXKo+J/jnUyhXKvEtwS+rl4DVXFKcUXRSFRU211aYXm3BKtdoTScjJzaoiuuRbfFYNRa7VDbRDpBe0ZrU8ddq2JfY1Sx56Z1n3ec+Z3cot2r+7UJuAdh9FkUTyxFTkB0RxRR+FuwZLBmsWWms9Go4aFlnNebep0voXiqVrXJvVXD2L5mZeBeK1czTh5KNUpuTUdStlmyX7li42RSZkRkyaxLp5qhdJ19mk6Zfpn8mnWdPaItp5askrBvs7m3iLbhssuukl9XWqhVuFEgTnhIykVfRRJHMUrvTyVUc1boWU9feWOiZnFpPWr3alhsbW3sbZps12pMatVo+7Hir6Wt3ayxphalYKQon6qbwpncmD6XM5YnlzyXZ5fTmCWayZoImF+Y6JYrl++XW5gRmJaZSptvn1Ch/6QEpv6mIKiaqsKr2KsUreqsp64AtJq0zLXft9W4u7kStxy4GbaVseawCK+6rVGrbamnqdBkRmMYY4RhQV9FXmJcrVtcWGVW51UaVFJSPFHpTTpNokrZS5xLuEpZTKBNElIVVyZa0128YUpl4miDad1rX21vbYpuPm+6bZ9s5GltqimlYJ/4mv6WbpRAlCaT2ZPElVmXr5tZoD6lAKulsIyzRbNHsP6tPqeHYFtZ1lL+SQtGXEczSwZTkFvzYyhqN218bgduXGvLZh+hmZnEk+iRrpHrlCSaK6E1quKwIrQhs5mp0V7lVDlOzUv1TupWkGA4Z2Jsnm0kawBjrJvVlNySPpRimjOjBKzvseiyjKlLn7tQu0VmRZRNHVzaZrFrdWzJaFxdCpl1lfOXtp+Iq9C4l71EtbGpoFgtT9ZOxFYnYhpqX2zdaqpjlJ7/lyiYK55qqjy27LzYtJymI5uDSZVIS1PWYLNoI2u2Zv5dnZv5l0+adqEerp+73bvcsdKkoJpDRwBMrlnvYxhpT2pYZ6ZdD1J5mSeaY6E2rd+4R7sqsqmmTZ1HRoNGWE42WWxiv2WyZUtfslVwS3uY0pl/n+SoZ7V2vLe7KLKlpk2eM5lHQoJIclJfXT1kzmblZt9iN1wSVLBL6psmnAqgqKd2r7q3bbwZvEW3sq4ipw2h4529R65KTE/pVfBbOmGoZClmVmWEYzFfUVsIVQJOHEkaRpiehaBNowWoKKxWsmG5O70ywHK/SL78uty2MLJ8rQGpcqW3ohmh+58Mn1hCoEUgR3lKEU6HUTJW21dCW8Ndvl7nX1pgZGGvYYpgw2CfX6Bfsl4hXVRcfFqrWFxXMlW3U15Pw02/Ta1L70lgR45FvEPtQZ5C+kEiQA1BA0FvQJVAqaNepNykVaQopYyk56TIpomnqqeHqJiokKk2quKqHavNqyesR61Pro2vd7BhsaGyObWItsC38bgiuXq6CLwovAq+/7zvvFm+dL4qwK/APcE2wiDC4cOMw8bBHMCBvhu9Q7yXuhS55Lfptfe1h7WxtLCz6rEAsZuvJK1QrPiq9qnnqAqo3KeLp2ynMz62QFJD8kbcSfVMZk9JUmlViVepWDZZQVnpWHpYjldPVTtSf00uSXVE4z9KPW876DoqqtCsYbE6tii84ML0xrvIScmbxfu+J7lksletDapaqfipwj+kRatMlVG+VQBYrleWVS5RCEv1QyI+JzsvPqOvjrYkvoHFc8fZw4a7YbTLrTaqBKp7P+9Hyk/KVK1WFFQ8T3RG6D1JOf48l7G+ul7Eucm2xAK7oLHqrDus+0PCTMpTW1asVEVPfUWOPOM7L0OmuBTC6sawwn24sa+Dq0GsZUZ7UJxVOla/Uc5IVj/qPB1EXrmHwqzGor46tFmtVas7r5FNnVTxVhhUkksKQJE5WT84tfC/XMZbwPWzqKxoqiGt4ksWVDNWjlNnStI+XjiAPP9H7Lw+wlvAg7bVrumqtKt+shdTJlf4VmFRHkdcPXQ8+kRaT2XC2MbBwHW2f61Tqdapc64+tyBV8laCVGJNIETJPNo9SEV8TnLAcsZhxKO747LurASq2avdsFq401WfWJtYdVR1THxEujxOPJlCJUwcU3HBa8UHxWq/Vbc5r9mqEanjqNirYbFpuH9WgFl2WttYclRpUA5JIEOnPy5AWkKKRxxNlFMaV3jCJ8TEw/m/Ubv9tG2vM6rUpy6mpKVFpWmmqqjVq7uvibSLuNG8mVn+Wg9bQFrNWMxWwFTgUZdNoUuLR6RDB0LKQZVDaEOORCxIS0qUTVZRt1NHVpFXC1kLW8dbdb4qv1W/1b/uv6e+AL5xuhO4sLVgszWwMa26qauo/6Z7pYykHaNIorSiz6KSolShOKHDojijg6R1paCm8adUqVyrlayFrA6uF68SsZayjLSxt3G6+V95Yd1iRmN6Y+Ri0WA1YNBeGl0kWipXEVXWUA9NREuaR9dDWkMlQrs+xz6oQZhDzEcMTfRP31M7WHZcv14RYupjxWTXvNa/77t6t0SznK+ZqUqmc6JPn3Scops9m5Cb2px1n7eh0qWQqxixcraiuvJl7mOtYX9dh1lUUzFN90aOREBGQkycUnRbIWGEZDtm/LsOuJixZan7oRSdfJnXmQWdfaLGqZqyJrngZ7tlrGAJWbVOT0b1Q3VHZVGfXBlkJmjctRmuYKTmnLqZsZoun0Sn2rLCvHi/Wmh6YDdWikuAR7VQWl3GZkFqSbqdsTykaputmX2bAaNarme5trZFZMdYxkyxREtNpVnwZflqerTIp5iaKJWMlNuYCKbFsU61gmU4Wc9MjESuTxdfVmetaoKtwKFtmCuT45SRnHSovbVss7ljsFMBR6ZJXldMZChr02n8pWSbYJRxkkKXp6CtrvixPK3IXepKckLxSeZXd2VZafhqi6Q0mX6ThZUTmy+o1bdHv9e17GSDWkJTkFC/WpBkL21IcctvOrBooaWX7pV/mLCheqxsszW1kqzvX8BTQ0eORFtLUVRoYd1ni2qxatNlcZ2llXyRuZGMkxqaEqISqxWzxrVesWOoP17eU5NLg0j0SQFP/VU7YLlncGsibkBtf2vuZuJglJ2cl8iVpZRMlEyWiJmRnjGmCaugrwSwfrJetFCv96rPpuGfFlv9VstShU2FS/NNYU5nTg5RjVL5VjFb3V0LYSdksmdwadFrMWyJbaNtT2+ebt1tXGyparpp5We5ZWpkHGCvXHlZyJuPm8WZHZkdmHKYEpjWmK+XwJYNl1CXrJjKmOyWWZdIlraXp5jUmCiYWJcNl4CXspcRmeSYnplfmRuZ05hhmb2aSJv8mWea9pmimsacap6Znped3J1hnx6gjqBCoKCgqqENo8WjjaQ4p9KmwqcNqq6poKmNq4yuMK7FraSvwa8OsaqyG7TwssqzxLftuP28NcARv8bAbMFZwwG/S77Ovra7v7ioucK3NLfctDS0+q6nquqnD1h4UuBPkErOSOJGIUiKRyVIvkpbTP9NxlHqUylYOVzTXmRhvmKfZLdkYWQdY4Nh8F40XNBXT1Lsob2fNJ58nrmePqBHozSnC63Ysp+5pL6ewFG+J7dgr4CoIKPTReNCOEHTQq9J3VEhWddeRmJUYrFfdFy9VixPzqJZoZKhvKQnq22zVbu6vme+1bZ9rbWmEEWwQhVHtE0LV7FcF2D3YNldKleYTZGiQKJPpIqrhbXIvYLDpb7/tAKrYURBQlNGOlC6WThe2F8oXUNXeU7apbml6ajwscO8kcMWwZ+2iKxzpac8zkL5TTJX5VurW5lXtk7OQ2ukbaYfrM62JMLKxZW+dbLoqiBC00NnTCxVSVoWXLhZUlOwSDVA5qZkqsmyVL2+xErCKLkury2p+D1rQoFLOVUCWvdaYlj6Uf5Hcz3+pYen56s8s3q8BMHzvVS3PK95qTOnQz1sRbVNb1PiV5pYj1byUa5LNENQPIGopqvCsDK2cL46w/XBNr2at5Sx+aynqjeq6D4oRCxLflANVdpWIlfyVRVT606WScBEjD+FOhs61aqorO6wZ7U3ug6/+MJoxvDFJsNpvo+5G7YIsuSu26xdq5uq3qq4PppAhURKSIFKKU7vUI1TJlVnVmxXU1ctVxlXrlYGVWVTgVF3TilMbkkJRwFEwEC9P4A9VTzFPMg7hjrjOUU7Fz0JrDOt0q3zrQGviLCYsWazAbRUteu1/7eGug+8Vb2Zv96/0MHVwgvFU8XtxKfElsXyxAXFmMRgxlHFcMSOxc7EHcUtxSfFtMSXw4jBiMDevbu9/btQu926G7r4uLS5zbhSuZO4s7cEtwW1hLP+sUywEK8arv2sQKx4q/WqWar1qeKpOaqxqaqp8KkKqoWqXz5nQVVDrET5RxNKakv8TeRPv1H3UjpUvFXZVfJW9leDV3tXtFbsVY1UmFGUTlZLBEfcQ0o/HzwFOA44CjuzqYmrNa7CsT+2nLouwAfC48ODwjC+Obp4tPWvrKv6qPinsKZHqJ1DbkmjT5BUklgCWgNa31gcVXFPYEhEQZk8KDyMQNurv7KEuaLAusIAwU+6JLK+q8mnC6ZfqBNL61JfWJVazlnTVcJOaEWyPJI6skGXrNa19r/AwcC7mrFNqRal0KSgp0lRvliMXGtbglXiSn5AzjwARbut2LeVvue9PrUnq/ak1KN3pzBUmlwZXxhcmFMHR4U/2UMRrJ+4B8DzvZGyYadDorui/Kj/WHNez17hWOZNRUNEQo9MtrOhvYC/arShqB6iqKFIp0hXOV7dXrhYYE4IQ/M+e0ibr1q7EsB3uV+tbaQDonqlGq54XbBgKF75VURJG0DwQdhOZLQYvo697bTBqKyi8KAoo1iqjVsTYPFfC1rRUDBFM0AlSO5RIltXvbq92reIrbukaKCbnxWkyauFtdtgtGCdXTVXpU6hRptCs0UkT3lYzrhav7S+4re7rienTqLIn0if7KKFp/CvULlXYm9i/2AQXXVYzlDBSmlF00MeR/hO0VVjXEtgZcFiw4HCKb4DuKauq6e2orSfyJ2AnUKeYaBVo4Wo/66StCS5f77jY2RkS2NEYjBh6F4rXOBYSlRWUBZM9UmlRqBD40JkQ9JFhEjPSQdMkE+ZU+hW1lkTXIJd+14XYCe+ksD1vzrAI74Rvjm+wLzOvOe5GreKtgGzBbKNr/Ktfqz/qeOoLKjWpf2kfaMqozShQaCrn+CeCp7inYadj569noifFqCCoLOg+KAJos+jZ6T/pPCl56f1qcirpq3Nr/Gw5bENswi2s7hvuDW6zLwLYLVgeGBQYMBfJV/0X7VgFV8gXh9dKl0dXDRb+FfFVUdRBk9kTClJTUWSQ/k/FkGvP4pAUUKoQvJEgUh+TKVQY1SLV2Fa1FwxwEnE6MOywkXAJbuztrey3a7JqiqnaqWopKukVKZBqbSsCrBmtVK7ncD2xHNfk17xW8ZXBVR1TYFIikJTP9BAkUTqSo9RUlZ3W0BdxseTxMG9n7Wtrhur5aiHqLaqUK+nuI7A5cSGXDJabVUSTv1GwUCKPLBCtEvSUzpZ3sKTwvG4krCHqRumJ6b3qMCtn7iiv3lYflSHTrxDbD1IPShEJUt5UhBY8sTwvuu1aq0wqWapIK4at4G/EMZmVZtP00VZPQA61kBbS5JTTFacwzW5Ka94qc+oDKyosje7uMJSU49NQ0Z+O8M5SUAPSqdRb1THxG29uLKWri2tkLCduBjE6MzqyQhQiUfTPWo7Qz77RkxOC1K3UnTCK7o9tI+wT7J6t27AMss7z4fLYE4qRt4+DDq8PApFikwHUbZSFFEfxCO7WLQhsUOxZ7MfuWjAu8h7zNjHn0opRjY+kDdENk85xj4LRddKKE7ATgpOeEvuvzm6u7WDs3yzuLMitlG5uL1uwqbFzcdgx2LDKERmP5Q6ezWJMq4x5zLkM904iD1mQvRFiUh8SrZKBUtWSyFKj0jyxuzE+cFPvgC9DLteuRW5Orn0uSm6frv0vPK+mcDgwu3EgMYAyCXKo8tRzfLN+s7t0M3SN9OE0jbST9OF0mnSJ9KCz0jNp8zcQnxCkT+RPrM8FztUOoI5xjhOOMA2eTeDNtI1lzYrNmg16DTaNAU0WzSwNeY2CDZWNb40oDNLMwQ0rTM6Mw80DzO5NFA1iTURNRQ0+TSnNKAzijOqM/Uz5jSRNBY1+zOUM7MymjPVMvoxzjGCMWMwCi8fLSouDC9FLkwu2yx8Ldot8C1nL/QsLiv6K2MszSwwLCstYS2NK+srGSrjKOUnjyiUKcsnyCf1KZ4p9ypiLGUsQizFLaou8jDtMEAyljR6NSY3LTi9OIU6WTpkO308bDy4PIE8wDxNPD070TpNOtDSvdCUzZvL8MmKyKTHSceixsbHesiAygzNm88g0srV9dix24Tbi9rd1/nUTTSmMM8sEyk2JuMkciZrKeEsQDFwNH82WzewN5M2OzSZzsXKJcl7yK3IKcqVzPrQxdTu2CDbw9da1BAw5SuBKHwnyiptLhE0zzcZOTY56TaZMzjP+MsYy3LL5M3A0gfY69s+23XXVy+/Ka4lKyZzK/YvnjRiNSU0VzEIzsTKa8l+ygzOUtJI2APa19dT0tso9iMnIt8nOC+iMzQ1HTQdMIPOqssTy0fNg9GR2Dfdydtm1pLQ2Cb7I38lZitpMG0zWTO7MAorEczDyo7Lz86a1I7ZZNtq2D/S4c2dImwiLCeVLMAxADRxM1wxeSyUzjvN9c3D0JDVqdvO3wTeydeu0SrOPSOWJOgoni28Md0zpjTcMycxEC1tKM7N983nz+DThdkV3jPgpt9m28/WPdJ8z1wl1SSFJqAqEC7/MAAzADQPM7MxMC9BLKgoAM7azefNB8/B0fPUZthN2zneq98M3gjbN9ih1NrRwc81ztwiSSJrIsgkRSepKlstwi8FMgkz7DOTNEY09DNCMy0yLzEyL+wtXSw2KvQo5iasJJnN/Mw3zfDME80lzvnO/c8q0RPS29Nk1UXXbtjY2HrZzNrU28zdm93s3CHchdzB2yzcT9vB2iPZ/Njs2G7ZVNgE2OLWRda71ZXVwtQX1HHTLtOi0vnRPdHL0CvQic8az37OEc7nzcTNx81+IrMityL3IkMjTCMDI18jWCSpJHUlIyZCJkcnRCjxKAsqOCpUKzssHC3fLawuyi+8MNMwXDHOMb8xEzJwMm8ypTLEMlMzXzMiMyIz4TLAMmEyYjGcMO0ubS2ZK94pjSg6JhkkZiKxIHTNvs1+zkDPFdC40WrTXNUH2GLaV9y73ajecN6T3Sjc+NmU1wjV/NLv0NDPaM+LIxMk0yUCKOoqky3xL3ox7jEeMrcxmTDALhks7SiSJXIjgM+30O3SYdbX2SrcQt1d3fba1tZC03nQBc/iIEck2SgOLQEwqzGWMd0v1ivMJ80i+B88z2/RcNV92k3eHd8E21PWWtI20C3Q2yanK6ovxDH4McAvtCv/JZMiotDK0w/Zbt4Q4BbdRNep0pDQNdEnKEwtdTAcMZUvUiuBJb4hV9H41CnbqN/y31TaztQ+0e/QeCUwKzsvVzC2LgIqHiTcIfcjxNYm3Rzhp99z2WjUmtG20VwmMiwmL0QvpCyaJx4i9yBIJQPZB9/44P3dINjI00DS3NJWJqQrNi6bLv4rPidcIekeVCLB14Ldx+Cm30bbPdZY04HS29OSJvAqfi1uLTsrlCcTI9gfTCBIJO7amd894qHhyN1B2VLWtNS61HfWJie5KmMspiyRK8MoSyXeIRQf2R6VIQkm2d3c4bvkb+XZ4nDfwdum2JjWJdbL1iDYV9pcJxwpeSomKyIrCSoAKPclSyLtH0cezhyeHCAeqR89IhwkM95T4Bziq+NE5EvjWeJ64A7fFt2L21ra4tho2BfYO9id2P3Y0dl12lPb+Nzu3Uff3iWAJvUmMyebJ7YnwSfWJ8YndycXJ3wmEiaSJQklbiTDI/siNSLoIdAhmCHqIDsgrh/gHvodzR3CHHQbpBo7GjEazxkKGZgZZhl4GRMaQRphGjsaaRqnGlkaOxp+Glka8Bl7GY8Znxn7GVYatBq4GswaJBuSHLwc5xxRHQwdJx0GHWgdmB0THYsdzB3YHWUegB4YHzkfcR+RH6IfyR8W5dPkkOWk5UDmduZ25yfoJ+l36ZTq4epe6y7s2uyH7C7s5evE6/rqPOp+6VDohueY5s7lheS444jjxuIi4rDhd+Fc4ULhb+GR4anhLeLy4nHjCuTQ5MflCecVHZUdFR5LHm8eWR4JHr0d5xzAG34auxjtFsoVxBShFPYUCBbAF9gZWhtp62btyu4u77rtp+vT6cLnW+Y05fDkKeXU5QHnLun46qMbQRxjHGgbshmJF1AVGhMnEm0SwBP3FZMYgOyX7RLuh+yC6pboQufL5uHmIOgT6jrs6hkYGnYZ3heuFRITVxH2EfkTnxaOGM7vjvD67oTsE+rW6J/oaOlS6/rtwxjsGCEYXha3E4IRUxFqEukU7RZL8Jbwr+5x7DvqEukf6fTpzusk7hcXOxcUFhUUpxFkEN0QBBPTFCkWBvEd8J/uAuyC6v/pf+op7C/uRvD/FU8VWRRZEjQQig6YDgQRKhOKFAIV0fCV76ztl+wJ7Lnrbuzj7TvwF/I88kIUzBLyEBoPFw2nDH0NFQ9zES0TEhQ7FPvyzPHt78nuQu4D7iXu7u7z71LxAfNq9A30RhOREncR4Q9fDkENsQyxDP4N7A4lEHgRTRLTEgETzxLT9Knz6vL18fTwavCm7z3vDe/m7iLveO/g73HwCfEc8ufy1vN99AL1DPUj9SMRghAiENQPRg+sDgwOjg0eDf0M9gylDK0M6AyyDJcMvAyZDKwMDw0uDRsN+wwrDUANVQ17DZkN4A3lDRkONA5/DrsO3g7fDuAOuQ7UDnwOmg5vDlsOFQ4xDjgOcw6BDrMOtA6jDrAO2Q7BDrIORg4zDkIOBQ4rDu4N6Q2xDaEN0w2eDWcNdw1SDUgN/AwXDQINyQztDPEM6QzoDMgMrQzWDJkMgwxtDFMMMAw/DDcMZAwADAIMAgzwCyoMNgwODDYMGQwrDEMMWAxKDPUL4Au2C80LqwvRC98L9gsrDEgMZgyMDF8MlgybDKcMngyFDHoMbQxMDHQMOQwmDPYLR/gZ+NT3i/cb96f2ofZI9g/20fV+9WD1VPWL9Zb1k/Wx9e71VPbS9lr32fdC+L/4FvlJ+Xj5avmK+UD5VgrWCSEJnghQCNYHsQdjB5cHNwjcCIsJBApOCm4KWgpCCpD5xfgg+KP3RfcR9w/3Pfem9x341/ho+Zn5dfkg+WEIjgfQBsIGwQYwB/QHswgRCS4JDwmkCPf4cvgv+DD4WfjZ+Kf5lPr4+tP6IwhqB5gGRAZyBhMHywdNCGgIPgjCB6X5MPka+VX50fmv+pj7/vt++zsHZgaXBXoFOQb+BnYHmwdlB+AGSPoE+hP6dPou+w/8Zfwc/E77vQUTBdIEXAX6BXQGmQZgBuAFvfp1+oL6xvpe+wP8ePxS/Ln7+gRyBHYE7ASBBeoFCAbrBaEFFPy3+6n72Ps8/Nb8Pv1W/f78ZvwjBJgDmgPyA18EwQT1BPIEywRfBKX8ePx9/KP88fxe/c/9CP72/a/9Q/1QAwYDBQMeA0wDowPVA+ED4QO9A4IDMwPiAp/9nf22/d39F/5a/qL+xP7J/rj+kP53/l7+S/4UAgQCDQInAj8CWwJiAmkCbQJkAlUCQgIuAg4C7QHOAbEBQP9J/1X/ZP91/4r/nv+0/8r/4f/3/wcAFgAjACsANQA8AEMASgBRAFwAZQBwAHwAiQA=`},{name:`dismember_right`,sampleRate:12e3,frames:6960,wav:`UklGRoQ2AABXQVZFZm10IBAAAAABAAEA4C4AAMBdAAACABAAZGF0YWA2AADs/0MAogD3ADoBXQFHAcAA1/+x/pL9uPwN/I77GvvQ+rH78fwh/vL+Uv9i/5H/tf+FADUCUQTzBoMJ6QoTC9MKzQkICJsELAC9+/v3Q/WI84vyRfL18Wvzz/bn+dT7x/xV/Wf9rv1//hIBwAQDCn0PwxOvFVEVZxTrEYAN5wdRAAv4QPIl7qzrb+rt6czp/OhA7U/y5PVX98T4gfgs+V/6aP1jAeIHxg9RGOYdNSIbIs4gSR6IGfsRhwiq/Zr0He3F54bkjOJW4kHizOGd4Xbnlexd8czzJvXG8yL0BPVh+Gv82wQZDpIXoSAZKfQu6DDlL8wtRSn2IYwZgQ7xAlz2Luy34/HfMN2l2yXattkl2KzWH9rA4XPlrOeD6D7oF+gk6c7qYu2Z8CL4QQGiDVYbtiaZMK04wz5FQ8RBrT8kPG02by+sI+QXuglZ+xrwBeeC37TZEtYr1WvT09MS02fTEtKC0C7OGtOY2BjdUt5G4K3eBt9q3Kndp94i4OTiSOgc8F774QV+FLMihzABPQRHrE4sVetY5VzoW21Z5FWTUIhKGkM5OB8uLCAcEu8BzvSs52vd7dQ80OvM2ccExmPF58M1xtzFO8WXw4fBKsLnvt2897qSun6+gcPdxbjJEcv5y7jK4MmGzRjOos6X0ODPR9S413HdouJ55j3snPbp/vkIJg+/GVckxi+lOc1Ef0tOUotYLl5TYn9lJWj5acVqIGnmZ+9kfmG8XaBZ51G0SxxGnDx1My8rCiHbF04K8wJz/GvxHe585aDgvtyF1aHTGNJUzFvKjsc9xq7CqMFFxV3FNMZWybLKIshSxgHHHscUxIvCtL3FuDS1wbgXuzy7Jruouq25Ybsguym/pcBmwVjEjcVzxtPJ6shxxo/F88Xcy6vNANCg2JPbNuSa82376gWyDXEaTiXtLcY5J0XfSthQ4FfIXO5gd2X8aN5qtWtabIJta2shaJJjsF1LV0FRAkgfQwo7fi+PJGodNxV2CVb+/vZg76fniuIG4Njb7NlA2PPUSNX/1LHV1tTq0orPmMvGywbM88Ypvze5ILQetAW2trrKu129QL/Ewt3DVMOOwrrH68g0zkrPxNYQ4BnqA/flCCMX0CWmMRBEXFDgWNJgZWbtahRt7m7+bnVovV83VuBK8zqcKpgYyQvE+djsBOWX3DfVHdPx0EXTRtKi1sTWvdKrzu3LF8ZjvVOzkrV0tjG3+bgHu66577q+vQDI0tdI5db4QQirHfszkETuU/5aLmDGZTZoimp3YsRU80MVMAUecAjn9nTsHuLi3PvdWOPA5BrlKear4tjZW8riuia3nrfztUu2bre9vJXExNMY5nb9pxVyL6lEUla0XjplDGkba09hR1E5PREnlw2l+7ztOOjd5jjpsOqK6QThGta0xx6496xUqmirnK7/sU+86svj4/z8sh7IOqtO0VllYQFlGmfHW7JKpDCUFu0CtfaQ8MnwKu2z7Rztr+Wv1Fu6rqbEppGlR6hTrqC10cXi5IEMBS44Q71TH1zUYK9kQVt9RFMr/BTLB8n/1fuW91X0AO/56YLYI8OwrSCpiavUrtq1hsab3WsADCBGOfNJoVN9WmNbZFnuRWYswRQiB9MB1PyB/TgA1/cQ6njZwsSArV2q561YskG5Ns0U6B4GjyNuPfJKXFGyVE9YGUxpOQglABXODT4KOw3dCacBK/Kf2QjD0q70pOKmJqsMtFDDE9/d/2Qc2TNdQilKOE7dUPJO3TsiKV4Z2hDyCigKQgdPAvf0xN/Iyf20KKabp5qs3bbHxR/g+ABGHp84CEhOTwtSpE+pTglH4TS1JM8VnA6mD0sQihBoB6v6IOYPz8K7xKmjpWeoHK/BvQHQ8+fgBPgdmTNNPXFBxkMKQ71C7zwjL4sdThSsFTUXhBb3EYEN4wFw7drbS8UGs+mmXaWiqUKvJLy2zt3lyQBqFsUpCTcsPcZA7UAzPwRBaT35MP4oBSBBHdsZLhniFsgQ1Ahy+7/poteIxYW3sazepuGoBa4lt6LC8NJ56ZP/iRMlI08v1TXVOZY6UzojOD45wjqhM08siSYhIUkeNR1wH9wfxByaFtkLlf6F7bbcxs1OvsayPKvspQSo1qqIsMK5lsUR1JTljvsZD6oeKSvBMYI2tzfAOfc4MTdkN3g44jm6Njct2ClvJ+8m8CiZKkcsmyz+KFAofiEoFn4KgvlM6jTYUsyiwgq3cq+/qtOmjKc4qhyuNLLeuBrEQdI84MzxMv+vDIwX5yHpJogpjStELj0t7C2/LegurS3CL/oxsjUSOSE4GzJzLscsIiwMLIkryyj1KA4qCCwLKYQl+SDSGy8VAg4KA8b5re3L4mvW2ctswwS7xrSCsJyslKnXpyWpZawKrwe1MLutwgXJQdOX2x7kn+59+F0BPAl2EYYZBh3uIo8oxSmeLAUvMzH+L4gvETHVL/AsEi5MLlcwujKfNRU3oDN3MYExmjDAMOouwy6+MCYzNjGZM1IzATM2MHstsymmI6wcnhdjC4MBlfZA7cDh5dYVzR/DtLoztgmxuawAqRqnzKVeppqpTq3XsSS4X77qx4fQodkW5QvvJ/ozA3MLaRP0GuEfPSN0JeIolysFLB4tuSyyKrYq2ysgLtstjzJ+Ncs5y0CgPuU8QTkuN/A4yjWsNWkzLTOwMZouoy3TKdQhRRqEEacIe/vn7CXfLtLCxYe7GLRCrSeoTaRXoUehOaXXqvSyMLs0xd/Rl97V7hT8GwcqEboZ7CC6JkcppSr3KVEqKivPLIgtvi94NNM7tEAbP688OjucO6k8FTwXPMg4rDLoKDseUxAT/JXnHNU/xDu2E6uYpDugup34oBmo4bI7wEvSneQy9hQF5Q5iFvodACEeIyMkJifpKVouCDjfQbNJCEg8RttF9UUORE5AfzhkLSIbiwJJ69fSXbw4rdWjZZ0emuydu6k1uK3M9eZ5/AkOlxpeIEQi9SC/IbAkeipANzpFnk74TDxMikzGSvZDDDYeIfwD7+SiyFy0laZKnnOa/Jpkp528ndY98tsG9BMyGk4Z+xlUHuwiZi8XPrxQv1BCUAJPlUnAPT8sDg6c7fXMorQlpiaey5qAoB60Rc7s6boCYw6iEqMVEBf5GiIlMDcTTSlWDleLVaROWUGEKaoHguFuwAKsAqDDmgiZ4aoTxYngl/c3BS4Lgw3aEAwc5S71RahcP18LXtRYZExcNuQQpuY3xKau16F7na6dObVE0j7uR/7/BO8EqgdKD7od+zdcUrNdBV3mWJlLQTCsCNXesLvQqLWfmpzVnXy1WNZJ7YT5L//PACoIRxbVLMpKrGEfZEVhrVY6Q7cdEvAky5GxdaW9obGgOrKgzdXkCPIn84zz9/lqBeEdzz+3XD5lamEPWLlDbB9K9IfOcrVBqbCmjqdlsnbQh+XR79XzVPJk9akCWBrRPORZkWgTZiZb30aYJP353tQdvdyw+6pUqkWuqsho327qxev868Tuu/wtE2ozk1S7aUNqX2R5Vjs7bBNa7l3OabrfsAGv7K9ctJ7KINr13pLhS+G76Kf3ORB2MrNSYmcja5ZkfFb6PKMZvPIw0u+9ObXHseywQ7EQvabN89Yy2MLZhtpK4wnzrg0vLyBPtGK6a3hlWFktRDclrf9o3XDG57vptSa0ObTZsYy8bMt21vfYvNdX2u3gw/BmCUAlykJAWmFoDWziZc9a1UaSKW4Fkul4z5jBL7zGuYm41Ldksja8c8NDyb/MYMwNzvzV9eCN9BUP/S/cSb5b5Wamap1kZFpWSiky7BEW9t7fotFZyMTCYsI7w4TB6L+QumDG3cpmzC3PR8wPzBPRs9fW4ov03xA4LS1DCVUpYUFo32XeXgtUbkFqK2sSoPlj5HHXA84KzLbI/sgmx+bFS8V+v6a+L8Omxt/Ip8ckx+jH0sjd0GzbjOpc/BUVPCz1Qd1QWFwTYvZlvmMDXqtU/UheOvcmqBFO/bfubeLD2gDWGtO4zkTQHc/ezUPMNMghvpi9G8Nxx4HG8MjvyAjIRsmN0K7YluIL7r8ADw9CIsQ14kPZT/9WBFwAYBhi7V56WetQf0dtOYor9Bx5C0D6PfHn6Vni3NwK2UjVStTA07vWYtKCzsbM5ch+wrXF/MakyaTJUcxeyzzKhMkCy9nNmtdA4fvsAvpgCbEaJSzyPO9IdU9MVQhZWFuIXRRcOVfzTm1FIDn6KwcdJg2/AHvzLudW4P/Ykteg1bHWLtdd123XN9iP17/UFM54xd2/BsX/xw/F+sKYwdDABsOcxyTLa89v00zgf+0H/84OqB6CLlw7uEVtTTBTXVf5WUdZ9lRjTSZEZznOLWsh1xPZB9oAM/Zf9Dnyae8e7gftvenR4//bTtjxz73HzcD8wSHAzMHTwq7Et8fpyWjOQdMu4JPxUgFUFCMkMDHSP1hJUVHwVGFXelb5UKhIKj6CLYAckwgM+HTwqeyV5Y3kPeTi4pXkIuOk3jDQ58WwvVPA9b5YwHy//sLJypTZaOYQ92cKuyIRNUtD5UwbUdtTKFY4VJtKDj0OKtkW7wKy9vvrFeRe47Tiw+AB3QnaBtPXxjC7NbrZuQi6Y7tBwqLHWdVg6gUDGR0lMxdBDEsLUBZTXVHqRvo2uyLXD2wFDvye9uTzMfTb72rnPdvjzdi9wrcguNW6Tr2NwynSQeZGAAEbrzF0QWhKOU+wUb1HXDhTIk0RFQWV/Db3FvZ38orsZd6Gz2C8qLRhtjq4Rr0eyLbdOPgeFLsu1EHwSqZOj1DDRYAwYRrLCoT8sfSn9YH3pu6R3evKZ7jTsECyg7Z2vSDM/eXCB60kNToaRZ1KfkwOQBMr+BmzDLUGYAcQCCkE6vYW4srFELDQsAyza7ouy9nmhgmOKRc990bXSSlNOEHdKNgUlQysC6gOMgzDAF3pe83otOGs76zYsqPCI9/PCIAoRD2ySCtL90wqQoErOxavCHsK7A2qCnv7jeKPxaKsn6cMrRO3Bcpb6RIPpi8UP4RHPUhbRkYw6RtED5AQ3QyMC/0Aqe2A0Fm0IaSRp4quKsEe48MIFCkUPRlJwkknSVNDES+IHyka2BpxGDQOZv1332e9YaY0okmo+LSazwHzFxYSLdE8lUEFQktF3TfZKLogohrRGz4RTwH45pzFNatnmyOdEKcvuGHZEv/GHqoylz0GQpRCUkXtOZAtjSq7KbAptCMlDxnzaM8ms76ffZxjpHW1ttHZ+x8bADLXP9hEU0U3SKpJojtDMYouajD5J1UbdgeY6MjJSa1SnIWXAJ/Hr/DLEe8AEO4oeDViPk1AOkBoQp1DcTnPM+cx3DIELCgi3wvd7oXPi7DYn0qWTZjwo8y3Ydho+BUXuyz5Nqg6gjwhPb8+AkMUPwQ3IjURNcMzkC82IoEOO/Wk1eS5KKaHmWGU+JvHqonAnt0E/nYabyv8NjM8uD32PQRBo0ayTpxI5ERJQ9tDRELPPV01/yKNClnuZdBItvOk85kDk1OWpaArsX3GE+Fg/awQ7h8hKhErNC+DL88wTTNVOfJE2khSRn5DH0BXP6g7DDcKLXwe4wrs8cnbCMe2szinfZ0nl6SSfplcpHqxEsTg2KruygG/ELUcoCFUJH8oPiaoKO0pvS7bNf9Ay05pS8NIt0oES9dKkEk3Rt8+ITSjJWgXFwGP59bRzLzPrk2kLJzpljeSApOSmrGlA7WGxerVkuu+/sEOuRlDIo0kaijUKfIpIiezIyom9CruMq09F0X9T69Su1LtU/9Tk1NqUuJO00uYRXI8DDA4IvwQ5v316fLXe8dhuJStAKbsnpWZ9JUmk+qTDprNo9qtArplyebY++ms+O4EcQ4vFWIbwx18HaofTR++H4QgWSPIJu8qnTKFPT1GTU8RU7lSSFLGUVxQgk5BTIxK+kQCPOszCimMG/8OOf6P6//YkctvvRayJ6h4oQ+e4ppNmYCXipsxpKKtDbyrzMrZp+rE9qMEWg7eFvcaYxwAHfIceButG3AbNB5BIt0ocDHYOqBEXVCUVEpUcVKyUHdPVU3PSHlEATz/MCch6xMDAmvvrNsoy+i8vLEtqkCliKFanpCc951Xp8uytr+pz9/dyurr+GsD7gndDOsNrg+6DEQQuxF3FhkcQCU6MtBA4U34UwxTcFF3UDNOpkj8Qog3fyfGEdj73uZR0k7CqLWbq5Om3aNmoiCiW62eunjKStzR66T2mQCDBCcEGgSBA+wE5wqNEnEfZi+qQcdRuFMgVDlTDE+TSp1ADy83GN/9DOL4y9q8oLH8qTyn9KYXqNO3H8uZ4PnvjPwyBAsG9gO7B+8I7BGGHLIrSj5BT6dVE1ZxUp1MKkCyLrUXI/li3s7IqrrDsfWtQ62MrHq7RdBa4ojvUvaS+VL4sPkpAIsMGiB0NC1JnlYNVhNUEk0hQAQpbQlx6rPOB74ttr+xQrCar0TBgtOC41PvYvO+8UTxbvdFBYsYIjRYSrBW4FTeTsRD+C4kD5HrRc/avmi1PrKqs5G0zcft2pjqa/As8lr08/sLBnEcbjUATHRXNVVQTOw8wyEcAJbegcrpvnq6nLkxu3bIANmD5JPrC+vU7Wv0LwVMH4g77VAEV1lRKUUkL3oP6e1+1G7Fe8C7vre8DMSH0wPdzeHj4d7l6/HnBmwmc0KmVGlY6VDMQqgoHAPj5OPPpsblw0PB+79jy+jV+9cn2lnereZS+SoXBjYrTFdW/1ARQTkkwgFs4S7QK8goxRvFP8JpyabUy9c92nPeheg5ANAc8Dr5TVhY8lENREUpvQZi6DjZV9CGz43Q88t+zN3VYtrm2xLcqegFAOcejzykT8VYLlMQRDgqZAsZ7n3bM9KKzmXN38nJwsvLTM7+zWTPP9r17lQNAS7dRO9RNVPbRc8xUxVl9cngftiG2JHaa9UOzbrFxcj+yaDLadBf3fDxOxXcNNpIpVOTVRJKxzPYF/3/juub4gDhFd7l3HvT0cRbxn3GhMf5yXXVNenqBXUmr0DgTcpUg1RHRxIwKRa4/M7qGuMF5dvijuMw2znNRcL7wqXBgMNkyTzZG+21DH8oD0A3TTNVn1aJSEI0ahuUBLPzqOtP7Fvq4Oh65oLaB8qVvfW9h74gviTD787N4VD+Zh2jOD1IslCgVHRWP0q/NvUdBwXj72zp0Ogo6d3qAedv3tLTv8XSuja6KLrHvNjAx8tI3WLxQwn4Ivg4yENUTLxRDFJoSho7iSkmE+QB4fbo89/tDPGI7kTr2OAl2MjJm7tAskWzGLRut0a+vcj71orqqwQKHKAyM0L+SxJRp1X0VWxWlEgfN8gjpRF8Ajf5CPbx+Yb5Q/lz957zYu2g4m3R6bxHr/KrE617rwa19rnNw6LUn+z4AzkftzHAPS1Gck3cUx9VJ1bRVEpKIz48MfggHxZWCLID9P7PAvAEuwKbAi/+2PQa6Ffbxsz6uYytNabtpROmpqc+rTi18b1+y+PZKPbpDK4kJzqrRgtLc1GfVERZ41bWVs1P5j5TLyUkjBcgDtwCKgCM/JL9Af7A/XL6kPlv8YPkVdl0ycG5Sa27oe+hQaKJpTSqba5muC/DO9HC41D77RCfIowyZT4eRd9Lb1BzUx9UDlO7Sx0/WjQtK/4iYBziGzkZHRyuIsEhCB0OFh8KQf3/55bTRL5ZrAWh1Jj7md+bHZ4hpVKvGb6Z1WvtzweYHzY1g0R4T4FUyleJVVZSE1JdUgNKCjtUKCIewhVJF+cazRq1GJEUQRG0CQX64+s71by9ZKq0nD2VAJaAmZahH6w3vOjRx+zFC7Yk2jjiRi9Og06eTjBQjlI0U1xJ0TkxLZAlOyEBIygg3x3qGWQTvQjQ9JrZo8G3rAKeuZMelR2boKast1fR6fEaCVwgkTcSR6ZIbUlTST9L7kweRhQ4hS0bKTEqpCikJAEeeRIk+BrfK8M2qZCZ55A5lVKdqq3wyhTuxQ++LXw/6ElrUeFTqFKTU/tO6kDYN9sxuzC5LLAk+BSnAurl7sGkpnWYJpLYmZOoYsKF4IcBliKgMUo4DTzPOuU6+UQMP/w2DzLWNWg0lSnLFlb7ytH6reuYuI5hkUicw7QF2aP8uBn4LSs4CTj8Ol1DlklPPf83qjgcNWAp/BGH75jIxaoCmQKSCJ4KtnfbRQIHHYkwxDEQMn8ypz08SA1Aqj+3P+A3MShaB3fbA7Qdnh2TIZlqsMDRH/tsHIgsZjPxMTk37EQrTX5Jq0Y1QdYxuxH26DK/IKSXl/CY3LFD1nH9mxdNJZUnAymbL6tBtUqYSXpIiUAsKYwBFdc0s8KfOpjKotTDjPBXD6setR8zINgihzgGUmxQokzeRiczAgnd2N+1mqH8mWujxsKJ6UcEKhBXE6oW/iPZPCtVRlXyUN9FLyxP/YvO8K0LoR2c+a0l0jL0qgYZDUgMRBbrKlpIoFZ6VXlPnz3lFa7i4rqBplCgfqJkv5Xg5voLBvgJyQ04HvQ5+1Y/W45Xk0p+MD4D6NWcuBmrbqe0s8XVUPLr/UT/WwMNDLcjk0JDWnJYfFGxQHIdS+xSxw+0jqx6q8O72teR6h7zDfJb9BoCOx2NP95YL1ecTps6yhav6Y3GJ7bWrvitnbj/1T/o2u408Azyof2/GIQ5WVS9WVFTdESfI5L7JNcAwDS3trSbtOXFjNwz7MLu5e0E86kACBuNPONUvlqlVHFFJCf0/rnbBsgRvWG6YrsfvWrR0OIl6o/pBerx84MJfygcRAZYJ1zlVfNHBy37BYzi68uGwcq9zL22vNPEUNRy3ULfZuAx5jX0SQ3OKi9GDFd8XJhWPkk3MZ4QPu1k1FjHfMIrwhzCvsBxyaTTXtgW2VzZ2tsx5vz4ZBMjMdVI+lUHV1JOtT4/J4EKTu6m2QfN4cnjyD/KAMngwn/NPNMp1HnUv9Sx2iLlxPedEU8sMELWUKZYeFU/TFw8FSg/DR71weSZ2YXUKtJV0brQwMumxH/MLNAF0aHQidJY1k3gze9DBisfpzXrRRpRCVeSVk1OwkDXLcoW+gBu7xPiDt0c2R3Zitli1+XSgsuAx+bKP8s6yZjKossT0R7aJ+gr/ZoTZigkPCNH5U+2VANU+EsMPjcurxtrBij2gei+4DPcxdqW3cXc59nj1iDOcMTrxMLH2MYSxlXGS8nUzTHYL+ed+DAM0CEtM8ZBl0tKUXZVFVWcTWdCaDPtIVYQHwDE8+rpeOQs49njouPN41Pgsdrn0wbJrsGFwgrBYsJuwlTEasjqzmvaresAAAQWoyk1OrdE/UxZUatUg1ZUT2pEkjRhI1sVlQQo++TyC+zk6OPqQOpk6B/lDOGe1v7KvL6dvJa7Lr3IvkjAyMMMzHjYJumP/HkTpibPNxxEMUyEUQdVKldIUndH+TgUJ6QVEAf4+sPxvOzF6zPqQOmB5pjivNkGz33CN7avtuO3+Li3uy3Avcd81H/nH/26FEArXTy3SKFOy1O3VTNWH0xfPdEpyhTkBNr9pfcX9aD0TfRK83rwKOkp2R3I6bj1tNq0GbVht068mcah2XjxFAznJPo3hkRwTIVRjVVPUlJCAy90G5EMYANO/8j9Q/rD+C/13+lC2hfGubTSrMisn6/UtcrAfNF/7HMM1ykFP0BMbVILVepUR05KOwslJhG/BhECYgI0Bc8BGvk86BnULL0QqsKp96o3see99dRB9coWJDApQ8NM1FFjUzBRIzv1IgUSfAs0B1sFnAIM9y/mvM5at3ikg6KJphOy9cS84z0JTCptP/5IuU/cUKxPcTbyII4ROwuQDHAKrgJG8OjZeLwrpKucW6EFqwLCqeMjEXYtUESfT7BRm1BcSfkyNSEkF88Z0RdgDn38X91avO+jLZ81phC13dNp/TwkGz4YShJM3kpxTDY17CH9GN4ZGRhAChb2iNGUsU2ccJkbo/a2rt27DJYsoD9+Q95DEEX4PDIqiyCjH00epBUB/hDZCLa0nsiaX6bVvWvohBaoNL9D8kZSRbxHKT20L78q2SZUIXMKtuHruJydpY++lbelEMW+8uUZHzG4N1Y5SkENRX46nTlDN7kwKh2m+UnJc6UGlLOWoapf0D/8ICLrMbo1djYgPctH4ju0Om88RTNAH/z3jMkLp4WVmpcfrOrQaf4qHfwqKCv6K+00gkcBP/NAdkDSNr0fiPbCxrSlRpYWmsmwZNkwB0QhxizzKy8ubTb0SypH6kZ2QoY3Zx288d3A1aOelCaVk62j1I//JRmXJPgo/ihEMhRFw0lBR8JHUkEsLqME6NTMrvaaZJMZoM2/4ukgBSgYtBx9H9cjnzHKSKpNsksRTktD/ih//g7TKbGAn8mX1J/Tuvrda/8XEdMUNRRbGSUlsT7IV69ZC1k2VJhDfCQt9bzKLK8Zn2aYk6CgtyfbZPlyBawMdgsVDF0ZxDKQUSxYklUbU3dGtS6MDGHh+r/UqM6cl5t8pTjCpuGr+cQGVwk5C0IQgBaxKhtE0l3UXnVdEVgvSw8uYgl73xi+GqkcoKidypwvsc3Rpuxl/FMDAQQpB7YI6BQgKfs/VlU8X+Jg6VxtUZY+rSHx+zzXdb7xrsqkTqPZpS+z7MoK47H1JQKHCFAKCwmeClkTwSSrO/lSMmPqYqpfkVlgTnM8IyNOAezjVst+uIyvXqo9py2m07FvxNvU3OFR7vPxqfWK+pT8dwKVEIMi7DmiTDNdZ2BpXBVYNU58PzcoZgsB76zS2b/rsRKqk6cwps2nbac6si/Cfs5O3KDlJedv6qnopept8Ub8JgxMICY0ikc9Vx9gLF9ZXMNWR0y+PpYulRgK/DfoFtRQxOK8zLqUtCixgrAfszm5c8UG0RDZVNt445jkNuYI5Xzp5PMBAdQNzCEENGhFHlJFXJtho16CV4RRqkmfOx0qjxUq/fvpMdl70F/H8L5vu1G6C7v+u5+7Rb7Rx0zPgNec2t7cPdqF2S/YCNkd4Brrq/d+CHAa+C8WQWpOPFh1XalaXVYOUlpJczvmKjYY2AUI9Q7k1doazjPIA8XmwXe//7/Svqi+ZcWZ0FHVcNre4J/kKeTS44jm5u3586gEHRT7JAM2hUOjT2FYAVloVrVPmEVlOLckaBDM+jTostk+zvvHXMX7w7PElcMjw4S/WcSZzFjSYNak2MLYCdw73jTnuPO2Ao0VSiqpO81L01NjWqZZt1T3S4U/Kis7FQf7w+kM2ljRM8tJyxvLY87Ty2zIaMfbzmnU/9ay1ZfVb9dr2z7p+f2LFaYqY0D2TcxWelrKVQ1Mzz1aKIkP2fWT40PWktDuznHP8s6mzW3In8lj0HHSBtMe1GPYTt8k7SoBgBwLNftGpFLKWGVTb0jPNIIaDv9y5l3aINBAzQLQiM/JzIDG1sdfzdDMZM430pPZLOr6A5kgjTqySr5TnFZBTVU7aSAHBEbuP+Fq2pvbqtkx1oHNUcaryi3KpcpXzxDZ0+4RC7crp0KAT41VFFDAP8YmIgr286fnl+H24KDeuNYkyHHB9cL+wrrFbNCh5QcGASk4QulONlSQSiozhBNo+A3pcOIL4DHetdfYyR++3L8cwVfGRNMk7WUTITWcSVVSI1VTQpIllAZE8/zr/OeS5ufee86Qu3m7ab0ixHrSoPPoGZs3eEi8T7ZLoDLjD9T3n+t87DjuRuqg2IrBXblrup/A9dIy9JoeDzzATFRTtVBxOdMaZgFJ9mf0zfQ56enR1LcHt1+7O8fZ4HsKni/4RUxQq1IVQvsh8Ani/qz8Gf7z9Cnc7bxrsqm0hb1a1Of8Sye+QshMlk9bRLwlAQxLAlkBA/8U8rbYs7vUrEWwwbzb2D8DsipkQ7JNv074Qh8lMQ7NBm0HkgX09EDWQ7fPqzewUMAY3hwM7TFTRoxOIlDjQ2wocRWtD80OhQeh8wvQTrAjpsmsdrzA3tkKUDEURXlL2EutQVclihXlEmgUZg3/927VmrSUpFyqz7tv2ycJky3/P1dGuUYCR9YulBq/FuQVuRAd/pjdxbkyovahIa4ZyKbxah5TOdBEHUd4R2Y/FCkMHRQclRlHEIj4jNXYsq2e+aCeriHMNvh/H/I3VkJtRTNG6kfvNXkq+ye6Jzse/wVk4qy8waMXmWeh97NE1nMCBCZ9OJs/hz9vQU1FKzViLt0sNCuNIqUMj+r1xE6p45mqnFqr1cZa77sWOi67OHI7tTo2QFdFXDkwNLAzSjJ2KjATb/ESyfaqHZtWl4GhnrZe2PT/vB7NLyk4rzcHN+o9A0ihPCo5VTlcN9Atdxnn+AvT2bNnn2WUzZXNo1a7ht4LAH4aximDMdAyojLPNXhCqUMIQWpBGUB/O0kxHhwe/NbWB7jeo+2X3pSnoE609dAx80sPUyHYKBosMiwjLt80CkEqS41H4UcVSHhDlTu/KgUQbO8lyb+vWJ5ElVySw5w2rmrIeeZ7APYRTx/9I+wjaCTLKTc0MULaTNhLikluSj1HmEAMMjkZI/vt2yXAuayBnoGYepVVonW2NtLb8H0JrRpfJFglNiY0JuMpnisPN0hF3E3kS95MYUydRmY86i8dFsv4BttdwB+qApx8lROSNZOJoYm1PdH36EIA2xCSGpEapxscGokfRiQrM8ZCclSjUdtSrFHuTdZFeTn1JmkLGepV0JK4sqiQn3yYnZUKmkyr+8Gx2Xrx1wNXESsWZRaVFVUYARwjKXE6Y0i5WMpZHlqQV6lS2UpHO/witQdZ6Z7NI7mUq8+iPJ4km4CjjLhUzPbi9vLVAGgGKge1Bl4JPw8MHBkqPEC2UkdYR1hnWDhUu0lKOZQgqgEy44nHDrOrpYeeb5vumhyrIMCZ1rXrb/pHBBQHaAehB4YNcRhAKflCllfsW/FaSFjIT+5CES2IDcXpJcr+s26pIKI8oZ6fgrECzaPivPbXAlEEIAMjAcwKXxzLLLdFKFx0X4de5FmjT486tRjK9WzS8rc+qj6j5aFZoWm2Qsyj3x3sZPRx9J32cvs7CusfrT9dWXRd31oUVVFFxSvUBxXhHcWrtSCvW6wxrF7AnduJ7+P7b/yv+FT9uAq9HxVAbVcaX/BaiVHjPqwdDPOUzye52q7zrKqrmbYS0PDhgezW857z6PkiC3Io0EdHXY9fPFijRsIo0P4O2KbCZ7aCs8S0Xb+t2GznwO5L7F7tU/oGFJ02B1TzXr1aOUznMiAJD+UFzrbAh7x8ulnDqdkJ5Mnn0eRs60QGUSiMSRVd+1dxS08rQgJE3onEI71XvaC//chT1rjget5l4BLz2BOWOTNUy11NVHM5phDL5MHJFb+wvJy+/sAe0rvZ8dr/3rvvoRIXOvhSBFm0TOItBARk3wXNr8m7yInHF9Gh2MTaCtub5UUGuC2gT0JdjlQvPVAU2OyY10POk87HyvjIMdVA1qzYBuVABT0vlk5wXBdV9zqjEzLvKtw/1yzWRM78x2LMKs6E0hPnaQxrNTxRT1ziTmUxJgkN6ojZG9m52CLLBcm9y/XMRtcN9doiwEWcVsBYQkT/IGT+5ubK3kzePtVfwRfCzMT2ydLcVQIAL/hJV1VPTRA0fAsF70fgb+JQ3TvSmcJHxd3ELdEq60cWuDi2Ta1TxkIyIooCde3f6SXph+XL0QTDZcPlwz7Rd+4lGRM9H0pjT3s6FhdI/vjuuey/7FDg5cv+vG++WMKkzgHvghj4N6lHZUx4OwsZWP0A8bHsPO+P52TT/7xHvhXC9c0z6PoPODDXQxVLaEmuL+0S2/xY98XzcPIw53LQLLx4vJq9EMx850ITdjUARHdKMEolNc4cKQp8A2YDF//D83fciMRuvNW/J8iT2fb3DiDPOaFEKkjGR1sxTRo6Cm4ECQA+/LbxO951xNq2ZbhnwffSaPPaFBYxRj9yRvJGU0G3KsMYEQzcBg4FdwAw8nne9sYFtY20V7kexbLbZPoWGiUuGjosPqNAzzliKUcZJBNHEUoThg4B/0PpXs8Quo6yGLW1u3rKvOR5B6gjJjP5OiU+lz3kPCcqExuAETUPQg9dDmEIpvof4fDJ0LfRsAOzV7p8yHbf9PnhFoMrezZ0PcY+5T/KO8wrOCATGdkTfRNxEU8KIvu/667TLsHhtOuyrLZ+vEPLnuJo+2cUICXEL/wzGjasOKo6JTbXKV4guBw4G58c6RpcFJIHiPXp3m7L47rbsIixKLfAwS7S6uhnAeUWxicaMiw25Dh9OCg36Tj9M/AqoyUwIy8h+R7qHTcZCg4P/H7nCtLkv3q0T603sAy2XME206Ho1gAYFhgmYC8mNLI1gzUCNkI4mDxvMSsqFyjFJ+Ym9STAIA4XKwfn9nreWMqMuk+xFavgq/KxLLuMyafds/IvBrMXqSIqKAkqYSqKKhQtqjCGNJYr8Ca/JB8iJyRxIjAd+hNPAzPwV9nax+m61rBxq8atXrX3wXHRJOe5/tcQER9IJvArxCwMLZ0uwzEAN9Q4gjHQMNwvtC3dK3ooQyA/Ek0AeuqM0iDBnLYXr6CtWrSiv4jOWOVr/jISvB0BI7AlNiipJ9kqoi2tM8wypi6NL2Uwwi4JKvIgbRIt/QLm3M4rvjiyXqwZqwSzMcBV1J7pXgCSD4QYlxtTHbMcdR4/I7QuFTVzMzA11DXfNNEuLiOqD6X1f9phxZO3BLBirdG2lMd73vL2MQobF2sdjB5lHuohsijhNLg+CT3sO0o7nTaCLMYZiP/44EXIfbhesWOtdbcFyXniyvlzDOsVABt/G9AcwiRAMVFBij+4PlA98TanKDwRAfJw1AfA6bVEsem14skA4rL6swqgE34U0hT6GMIkITR/QUtC8UCbO9YtahVP8z3UEMD+tUWxTLg3zSnqAADfDFgRXhN9FSohBTMtRTREz0EKO7Qrzg+s7MPO9731ta2ytsHT2XzvjPy1AQECZAasEjYqOkCXQ9xBTjkRKCMKVuWNyve9ZrkLutDPzusC/KgBQAEUBe4NUSMJPEZFe0J0OnInXAeq4VzKRr9ZvF6/a9jM8L/8Y/4WAOYGKhlcMXlGMkQiPXAt3BAH7EfQxMIVwJDAidcU7zn6YvsB/cMEjxg1MmZGvEVbPm0s2QrM5d/OCcWMwg3E1tq87c30HPW69qsDJh7LOSxGrUG/M2AXiPKS1vfIIcUlxVvPaeGO68nsNe9t+8QVozMhRkBFPTvXI70A1OCs0O7LMMpdzWveCedV6IHpqPY/DuUr20AqQ7w5ZCSKAQrkuNLozkDPS8253RvlYuUv6G7x9QmaJ2U8TULQONAiMAA54/vU69Dz0dzQPtwM5SfnAunj8ccImSXHOm9EhzyZKkIMdO6R3HPWbtUw1IDTmNy33Z3eP+aQ+fMWSDHMPmU/azPwHND+AOjw3NPautm01GPXJNt42yTerulxAlwfyjRLPxg8/i0aFTD7uemr4srhpOHW2ljaNN1K3bjgc+vaAgAfPzJuPHs9rTL6HusElPKT6LTldOUq32jVqNjX2HrayuD37/8INiK0MiY6QznlK5sWXf9c8RbqSOmy6ILj49kE2H3YlNmo3ZnoMfzEFgUr3jT6OEY18SYsE/T/mvSO7oTtFe6U6WfeG9jV2HPaTd1/5kD4pw4AI8EuSDTQNfIs/B3oCzn7fPMi8EDwNu/F6ojgldUW1qzW+diS37Ps4f70FM4kFi1jMd8ySSmsGu4KXv9X96T1VPbJ9PXxquhj3OrVF9bB1kHa/+J18UYC3RXWI5wqmi64L/krgh9zEWQFKP7b+o/74Pra+iD2R+3e4LbXH9et1wHaKeF87PD6/gvpGjAkUCnMKzwt+SlhHyMU0wfsAFD+ev87ALb8tPdp8CbmM9yB1LTU5dRR183cMuYy9CMD4BEWHbIiDyekKBMplSXzGhQQ/we1AiwCNgFhAcUAR/yy9SPu0+Op2tjV8tUf137aAeE86oH20AQIEaIaOyHUIz0lUiWSJRQfNhb6DQwGRAJjAqoAvADo/wr7FPcW78flRN2W1srX99ix29DgEufP8tcA+w2xF/8dBSKGI5Ak+CMOIwYbXxM7DbkHXgX6BCcEgATjAaP8jfQ36s7iSdub1VrWGtjn2/3hsukj9oUBQQ5OGNAdwSDAILYgcSGYIQQbZRODDZQJfgYWCH4IqwX1AHP6ZvFc6MHfWNlj1arW9dhS3Qvk9+0c+o8F2xBaGI8bQB3dHQMeCR8vG7gVERHLDQAMyAy7DNoK1Aba/ePyqedb3x/ZP9di2YncpeLi7U37dwdJELIWLRpnG8EciBuSG1IWVxEsDQ0NZQ0cDLIIGAVo+3vv5OTM3ufYrdiB3DLjeu3C/P8I/xLfFvkarhsHGzcbNhwmF38TpxGTEcUQCg5lB7/99PKO5Qjes9gc2RrdjuQ+8pwAuAv5EhcW9BbgF9wXrxt8FlkSeRGxEk0RmgyUAeX1M+fo3S3Z09iA3b7mivOJAtkNthNpFfYVsBZAGrca4BgWGPMVYBVMD2oCDfIz5FfcZNip2+7kiPI7AqoNfRToFCgUxxVIGUca0xdxGI4XZxEIB8j3Yefz3SDZ19kr4a3sGvs1B04MRg1aDakQHhj6GNIZuxnDFI0Kjfsh627gwttE29fjffEIAgILhAzoDE4O7RGVGjsaYxpoFhcOvgDV7XbgH9tX2fvgrO/d/0QIXAqsCVQMSRPZHP8bQRu3FQ0Livkr6Qrfx9vy3QXrVfm0BCkIwwjMCzkUgB0LH/UdsRoAEcv/M+yi4ALdT98v7fr7owWhBxkIWwyjF20hBiKKIHgaIwuK9r7mhuAt3ujlEvTe/xwDhgLlBAgOohtlIJ0frRlsCs71xecY4bjf9uRD8yj8j/6e/4UCfgurGXAgOR4NGG0IsPR95jHhSeAa5ufycvsH/VX+eQN/D7AcdyFgHkAVHgP17lDk6ODb4I/nz/Lr9+v4IfqMAwESUx80IYgcYRDw/ebsGOWs41bj2er78j/2rfeD+ggHPBZbIAUgzBm8C7f4uurh5RrkIOSp7Nfy6/Ms9Xf7SwhcGLkhyh+IGL0IKvdn7EvoFelk59vuM/PF81P2Yv2KDIca1iHGH2wYMgl69+PtAOsD6qjoC+148LLx1/In+sQHxxaIH40fJhnYCi/6HPAR7Qjt+es07A/wn/HR8yz5RgUNFKgdryH5HckUwQT99q7wUu+U7v/rZ+we7nvuyfD29wcEbxK+G4QfBxuOEUwDzfgg8xrzZvMU8K/siO6T7oDwJ/Tf/scMvBdLHRAdHBbuCfb8oPaj9Lv04/JZ7mvrWeys7Eru4/Ov/rULQBYkGx8cdRb0C38BYflH9Sn1E/RL8Tjsxurw6sHr1u/598sDzA+bF6AadhvCFYAMcgJ//L/5c/kS+AD2VvBm7Bfs6OvA7Xzz2f03Cm8S1hYEGfUYohP+CngCWPwo+jn6W/q393ryqOsQ6xfrHewu77j1sv+SCpkR0hUkF04WsRBBCE4BQ/3J+l/7Gvu5+aL1we+C7JTsJe0m8Kn03fsOBXIMjhEbFDoVIRWcD94I7wJa/6D+fv4O/vX8lPkP9NTuz+zx7PbtiPB79fj8jgWmDLQQ3hKeE7ETWRAnC7AFZALRAIcABwCs/+H9ivnY8ybvwu3Q7eLubfGj9dX7ugM7CukN5Q/hEAMRShBaDOkHGAQcAogBQQECAYj/3Pst93Pyq+7u7WXulu898qD2SP0+BIQJ8gyGDh8Pfw94D+0LIggABZ4DDwOoAsYCBwHo/R76WPVH8dzuTe9x8DPy1fUU+yMBSgYKCkEMBg1MDWIN2QylCVYGMAQjA5ECjQKrAZD/Gfzo9zf0ffF+8CfxbfLO9Jz44v38Ak0HgAlyCvwKEws3C8YJHQeMBaEEsgSnBLADGgLp/jv7Offj8+XxPfJP81/1s/iH/WEC6gUaCB8JaAlZCaAJyAjFBisFnASKBD0EPAP0AFz9Qvng9eHzK/Mc9J31ovjs/GYB7ATKBpMHyQewBzsIqAc9Bo8FKgUKBVgEugKq/8n7FPiY9WH06fRh9uL4q/zJALYDLQXKBcsFAgafBhkGDAWaBJMELgS7AjsAm/wI+dT2rPX89Vf3CPrk/WgBrwMEBSEFFgV1BW0GQQXNBLAEhgQ1A9AAcP0G+gX4+vZf9wr5yfsm/+EBTwPrA8QDAwT2BEEF+gTXBJ8EiwNCAfP9tvrE+Oz3gPh/+oX9jABhAh8DPQNUAx4EWgUBBecEtQS9A58Ba/5o+6j5+vir+bv7jv4HAUMCbQKNAgoDMgS9BLMEoATnAwkCJv87/Ir61vlP+kD8zv6+ALMB3gHsAY0C8gNTBDYE3wPCAoMApv3C++r6sfov/GH+LgAgASYBRwHhATQD7QPRA3MDPgLo/4D9F/yI+477Dv0C/z4AxwCoAOwA4AFRA3MDOAN6AuoAg/4E/Vr8OfwL/bL+2/9GAD8AkABuAawCCAPWAi4C5QDy/p39Ff0A/ar96f7U/xMAEABzAFQBYgKNAk0CpQFCANP++P23/br9cf5N/8D/zv/r/2cASwH8AeEBiQHKAKT/yf5y/mX+kv43/6P/vP/H/woAoQAxAVMBJwG+AAYAWf8J//v+/v5N/5z/sf+z/9L/JACAAK4AlwBlAAwAuf+R/4r/kf+l/8n/1//a/+L/8v8AAAIA+P8=`},{name:`jaw_dislocation`,sampleRate:12e3,frames:8640,wav:`UklGRqRDAABXQVZFZm10IBAAAAABAAEA4C4AAMBdAAACABAAZGF0YYBDAAAGADQATwBaAFMASQA2ABQA4v+y/0j/7P49/qP90Pzt+wP7M/p++en4Xvj196b3iffH90L4XPkW+1z9MQBuA0YGrwhGCqgLmQxHDdEN5A3GDU8NkQzJC9EK9QmXCD0HWAZIBWgEiwOlAhABPf8u/cT6+/c69WTyge/a7PXqDukB6Bznheay5i/nX+gS68XusvT4+qMCwQnxDrgTmBfxGQscshxfHTYd5Bz2G3IaiBgEFjETMRFUD9cNVwy5C8YKCAnlBowDIQDo+kH1X++z6C7jIN642uXXsdVa1KfTitO51KPWPtpn32zmF/Fb/FAIURRpHekj/ig/LAAvly/3L+8vhS6wLAArOyghJdYggB4UHQQayBgOF3EUQRIODz4L5wQ5/HT0ierr35bXrM+FyUjF88GfvzC+bL3rvT2/18L7x33QjNtU53D2YQi+F/YjdCxiNNs4TTyiPylBsUBHP18+/jwMOpc37jSYMMcuTS2mLEEqcij9JBgh/hvfFwcNygDC9QDqd9xg0NPD0Lo6tBaurqq4pqWkKqRipBGmW6mTrpq3d8MU1NDogvzlEYQlszNoPxNJlk1AUS9RSVHXUElPXU3qSTlGPUI4P1w/Pz9/Pb09FjyJOmI28jU3M+YtpyY1Hk8RFwbe9rromteLyW280bJpqvKkrp9PnfWbW5wfnZeecqEKp4Kt+7eQw47WzeZv+g0MsBp7KFo0fD78RJJH6UsPTJlOpk5sTs1NVU1HTLBKLUkdSK5GeEZIRXpFJ0TXRuxGTkWBQxw/pjpOMQEpUhvzB3v1d+RF1i/HcbqgsbOmx6CYnJyYiZYklCOUF5Q0lcqXyJsqoC6o4bGJvjjMd96y704C1Q+oIKYsyjIAPZBCokUqSAZJsEqKSpRL0UphSEBJUEmBSq1MV00RTnRNPVExU8JRRlJ1UBhP3E2tSp9GGj3WNf8tBCJkGCMHYPVm5E3TvspIvO+wEanPnhmanJa0k8qSrI91j1qQ7o9wkvCUeph2nYyjnK3Dtaq/kM0z2m7kAPQKAskQWhj2IggrqDLvOsRBAULQQA8+IUMORa9GX0bHReBExEaISitNVU63TUxOrk5fUdtSElN8U59U9FHmUF1MIUsvREA+9DbJLgcgtRToCmz7+u3s4/HUW8iiuQexz6ceoLmbkZaBksiQLY9Pj7iOa4+qkKuRqJStl3mb8598pTGtZra/vYXNZ9e75Y71/gDiDlYd0COaLTkxMjeLOSA6XT/xQE0/RkP6RAFH2EaCSspNZ0s9SilLG0wOTq1OmVHwUZNQClX2VlxYsFcCVYpT9FEtT2hNEErxRIw8KDRaLO0ipRjbC/3/afA54kXYjcvLwOm1vaybpSGgTp1CnBKayZhfmPqY7ZgcmlGb2Zv8nVOgRaRrqaeuLbU6vDvGkNEF2ozkDu5J+BcAQwrkENwVnBoZIf0gTyggKF4t+TCNM5Q1Azh3NyA8FDxFP74+oUEqRYFERUX8SelOD1ENUftT/VQYVolYsVh4V8hUJ1PwUrNRsExGR8E+Nzm9L+cksxPcA47ym+ds2svR7sW8vFuzTK6LqFCkvZ+AnT+bPpoPmQ+bHJzLn0ujnKULqkiwSLSYvGTB6srfzm3VX+HU6MTvtPrD/5oHyQhbEkAXVxtrIEkimyicKt0sBTO4NK82XzYcOCc5yzi0PB5BeEOBRJxG3UlZSyZQpFDhUcxP20+zT9ZNz0sySf1E3kH1OtsyNybHGj4RDQRH92DmmNbAzFLCI7zEtGuugasTp0elUaOmoSGibKJ1o3mloqYcqW2tXbQzurTA28rY1LbegelW9Tf8nwN6C4ARfhPjFSIZ2hkqICoieCPeJ1Aq7CyJMHgvFDQoNFU5bD7qQXFH0UvFTl5SUFSyVJdUTVTMUkROzUg+Q688gDMKJ6oXpwXF8vDkQ9coyl2/M7c7s76vC6w/qziqFqpqqsyqsqyKriWxYreXuwzCz8l707LbaeJZ7Lr0JPXQ+wABdwWUBzUNCxKUFHoY9SBrJX4oqy1mM6850juwQDhF60cQSwFNok4ZTzhOmk/GTctKvUXZPms4sCw8HPUNX/xR65DaHc2GwZa49LKMsF2t9apgqrSqfK2/r7azgrjXvKnEZc3k1FTdpeO67UbxxPbm/Cf/YwN8CNoNURMDFkMgqSahLBMyoThRPjdEjkiSTeVOZFAVUW9Ru1AuTlpJA0IjN9wqlBnkBTnzU97zzn7DV7vps8Sw/q5tr5uw0rEZtKe32bw1xJDJfdGk1vTbbeVy7ADzAfd5+rz+3AGUBx0N7hIDG4UiuCpVNCY8lEIXSTNMFk/4TzJQB1DGTaRK+0MBOW0snBqFB1jy0t3RzonBn7nus3GwFa5vrXCufq9EsgS2d7olwKXH+8/t1/ffNupk7bTzyfgl/eYBygmWEdMYXSKqLuE3d0J/SpRPVlKUVL5VJFVLU3NPJUfnPJ4uKRurA9bsH9hryGa82rbnsI2uAK/osJWyRrZTu5HCB8fF0CnWLdur3h/k1ufj60PvJff4/sUK2xbOJPgwZDzLRUNOVlTRV2NZWFmDV4JTME68REw0Oh7PBhvvIdr+ydO+VrUJsNOuXq7ZriOyKLaau7jBycdQ0IXW8NyJ4KXhJef16Ynx1/urBagTYSLaL1k/N0jzUVZXcVqiW4NaslfeUx5L9T5aLEIVXv6T5sTSk8OXuCeyLq9Urp+vKbHxtJi5qL6swijHLsuU0GDSTNZ72jzg9ulZ+HoGrhUfJRU23kTLT0RYoVxwX2Bg41/iW7dUdkcrNQgdigFN6PrRcsEct8ywBa/arkqwxLINtXK5jb7cwTbHtcrLzHLOR9T62QXjtO3U/qwP3R8ONONEhVF2WrJgx2P/ZLBkeWKoXDtT/EIkL1ASN/Vo29jHHLufsvGthatMq4Cu17F4tHG4jruiu4e947+RwKrDock30iTcl+xvAokZ/C+aREdSD1yFY6ZnZGlBaZdmUWFnWHBJejX5GyYA5uQR0YzC87lLs/Wve7A3sFKzELbuuvO9ir+9wT7DqcOxxnPJZM7a0w7i7PRoCgokszniS4VZ+2IGadlqY2owaEdjZVrATBA4ECCXBE7sftcJyP297LdQtLOy5rK/s3e1rLdYugq6ILlLuYG6AbxCvibCicrS1ALkOPx2Fhku70ExU9te1Gbxao5td2y9aKlivFhsSsA3dB7ZBNno7NbAx6K/6buZt6W1urUbtci2AbY/t2+2wrONsw+y0LCSs2e4zL6UynvYQewMBNkdejiOTDVbW2Vpan1tqG7ibVBrUGU8XBlPcjtBJU4PKPeF4WHSLMdhwLm6G7pduwW7RrpeuRK3v7XUsaKw+a+Trs+tI7HPtmy/8s2Y4C/1Og3cJVc/sFJDX9tmfmutblJwl24Ba5JlF13iUYlDnjBWGev/ju5o4KvTs8oQwuq+Frwluv6567ijtyS0VLBFrt+rWKnEqJWpQqv6rWa2dcJa07jlg/8tGaMufEOTVUpfMmbaamZtNW0rbDhqFmWeXIJSbkNJMVUeyAqK+K3oCN1K0wvLGccJxOXBkr93vtG8irk9t7i0AbGTrYGq86dHpwenQao2r8626MEn0b7l1/5EFkcvJkQHUgddnmPBZ9RpV2rbaWBnSmHsWpdRu0a3ONsmFxbWBIP0NepK4ZfXwdDyzZvLkcigxk7FPcNKvQ27D7iCtJix1q5PrPSqF6rjq+quLLNZuBzA3s3B233wWwbDHToylEFKT3pZJWDQZDdm1meAZ5Bl5mLrXs5Y+E+jRVw6Eyw0HfYOyQKe97/w1uhk4tjdJ9dN1MrRtc+wzHjJdsbfwlO/qL0guJ6zVbDSq7KoVqmwqcyrIa6Vtd++iMuq2oDubP/LFrkpIjv2RnhQD1dfXAxfuGB+YDZgCF40XOJYbVJ7SktCtzfNLcMhGRiTCuT+YvrC9L7wzO+i6vLlauSF4Abe7tk41WbP1MguxPbA5bqhuVq2HbIGsfCuEK+OsFGwI7M6tgK8WMST0JXhIe+NAs4TdSUYM8g+UUmZTr1SDFiMWTRctVyKWxxbI1n4VrZS2EvpQ1Y6fjDCKIkcFA8jBJf75vXi8HvrN+cY45XfbODp3Uja69Zn1EzREsvvx53DR76DuiG4FbVHs2uxa7EusfGxh7S8t6O8JsImynvV1OHK788AXw7yHDIreThLQvRHrU1oUOFTS1U/VrNWMlZVU9VSIVDGTLRGfkLAPA4xiyl+ICQVBw9KCtQDuPwJ+BD56fFY8arthOdw4Nbcetrp1KvPist1xzvD475PvMe4lbUksxWxmq9Irm6vnbFhtaK5dL8HyrDTnuLS9R4IeRj1JWsxnzuCQo9JSE7oULpRR1KyUjdTM1OhUNxOLkuGR5hEYkCCOegxTynOI2EbLBRNDEMDAP/b/Kj6GvRU8KzrNegJ40HeI9oa1G3MfsjpxTHCI72nuc+2M7N1sJGvRa+HrSuuurCNtBi6hMCKyy/ZHOnv+/YLzhwmK/81F0AcSEdMyk6MUIxSQ1NYUkNS80+gTIZJ8kZ+Qi8+BzeWL+slwh9fGxkV3Q0SDZ8G8gHq/nH55PV38FjrDeRl263Umc5UyaXEPL4muRK09LBprkqshaxdq7ysVa9OshK5A8GryjPb+OjS+zULhxm5KP01uD9RR61Kkk7FUaBTHVVFUy9Rnk1bSu9GoEHAO1w07SoIJxofrhx6FTYQSAujBPv/g/0F94ryuelW47DYGc9byZfC/bp0tb+vG60qqlqp4KmmqQ2rZ66zs/O9DMeg1nTqzfx5EWEjWjKsPhFHZ04gU8xU11QfVftSsFC8TDFGBUCHN5YwRCtkJG0gzBvJFiEUFRByC5cHvAIr93vtgeXW2n/TdMnmwFC3TLAZreuq2Kc4p82lVKeQqt6woLlNxdLU1uUs+rMQlSXxNI4/3kicTnZSyFSkVP1S0lBbTmBKBEWWPSA3rjHWLEUokyE8GpkUvgtTCe79RvNh6VnettLjxZu6TbLSqPykCqHsnqOetZ/somKpe7N/wRTTnO3qB0QeBzHhQsxLTVJaVmZYblgnVsZSDlCzSCFEYjuqOEYw2ygZJHggaRkqFloQwgu6/tPzdOL20jrDxba7qhGiiJw+mq2ZRpusnYSjOKw3u+rQ0OzJCP8gnDVER5dQ21bGWUBaZFnGVQxTRk+GSPZCpTxDOQg2Sy/IK4YiVxotE9IEmvWp4NrMxbvorX+jmpyNlzmWD5cYma2dO6aStPzL/eV+BXEhdzalR8xRh1fhV1hXD1e6Uj9NBkfeQRJA+jucOsA2ojCDLHAlvBcGCUj08eB/yrq3taoon6KYVJWylCqV45jwoJyueMD23cb87x1fMSNDcU6vU45Wo1atUkxP/Ed/RIU++jpFOxM6ujbkMvUtOCYiFxoBE+cDz3C4dqi4nXCXUZQ4kxiWTJyzpki3S9DP62oHCSKgOJBF302WT4NPu06KTDNJiEQoQf5AZj7GPNk6WjLgKssflA1W9qzam8FQrtCga5k0lNmRc5Lflrifn664w+DgTwGVH0I2BUUWTQlRIVBkUDhNEEpSR6JEG0a6RBREfkQhPokzYyPzDGrwtdHauX+nzJuolR+TMpMnls+eF6zEwBjeXfxjFiwstjt0RxxLQk3WS8lJ0khuShJL8Uy+S1hLo0h1Q4I4GSSPCVzrm8qwsjCi0JdDkmaQ0JFclkefRq+txn/koALOHpgwVT5KRcRI9kjgR0RGu0fFSEZMLE0+Tg1O7ko0Q+411CANBGjjHsVTr1Gg65h+lZ6VfJh5nxetkMGy2/D40xDVJgQ0RzxYQYlBS0UERfhGjUl7TFJQQFPGU+BQaUfhOkckJAj55LDE0q0XoDuZdZamlUaZpqDmrQvBr9qJ9KgJEh3/LUA2ITpJPj1CLUOaR6FN01GpU+JUXFUSUoxJdjwaJBQEWuLsxHSx+KMnnECZcJknnR+k16+9wD3Xie5OAtMTziCdKHwv1TJFOBc7HUHcR6dNrFBqVK1UaVLoSu09jCf9CaPn98pmsx+m854DnD2cWZ/3pWSxM8IM11Dr/P1KDeYYBB6bJRApqy4gNUE9LERHTFdR9VbwVtxV1k9tRdgzmhqA+a7Zjb8lr8Skv6Bnn5SgC6VSrG+3PcdA2PjrcvvHBskPKhi6IDUn9C3/NUY/BkfXTrRUGlflVgpV7k6qRCowFxW39bLXpsDfsOSncaPsoSykJaiArvG3OsVU1eDkffRlAY4JUA9eFbkbhiGdK+E2+kCzSKFPIFXCVzZXXlVZT5RD6jHqGLH6Lt5Sxhi4e67iqRSo1ah+rO+yB7sAx2zUwuG87Nj3dv9DB0IMxxOkGyYkIC6CN9xAWUnCThxT3VOlUiBOK0cGOpQmpwtF8fjWscQquD6y1a36rJ2uM7KQtgq+u8dl0ZDbwedW8Xv2Uf0/BKYLOxQcHzUqMjKMPHFGMk21UQ1UyVNSUZNMEkZpOdgkdgrc73TXK8ctvam18rASsKCx7bSWuSXAyMbRzj3W394s5vbsxvCh9ej6vAPKC0cY0COdL4E5mEPQSipQ51EAU/FQrE2+Rbk6bSvjFvn7Eucp0oDE/rovtpmza7JCs562wLnwvrfEP8u10jLZi95D40jn0+wi9Af8fQauETkdtipvNkVBVUlmT0tT3lQ6VX1U7lEMTD1D6jOFH6oGWu0Q2FzH/7vgtNqxE7Dkr6KyP7X8uRLAkMX/zO/QhNiN3kDih+Up6Sru9vNb/rgK/hc8JLYwoz6dRs9MzlHKVNZVPFVNU9RO70f4PkEyOR/GBkDvhNvgy0LDEbzLtoKyKbPVtFm3QruQvxrDl8aSy6rRe9Kl03jX7tpj3wfj9unM8nj8agwMGwoncjN2QAhJPE+dUrpVXlY0VoNU2VDMSSFAVjLpIAILvPYm4h7TFcjnvoO6NLhhtoW3PLcgupW7jbzUv7bDJsjRznLR9tUB2YDeCuRP6TPvCPZW/b4HdxQhJ9QzkUECTPVS5lbSWsBcC12bWn1Yt1SMTPxAfzPNH34JVfQY4gfUPMZgv9i6tLd8tna2Abh4uZe73r5GwYLEH8YoyMnJbMrAyonKu8tZzVLRM9ol48fwtgHgE3YlSTbjRDRPLFUEWn5dtl78X/9faVwQWnRS8UtdPiUq+xS5/sfnYNriy13D8bruuL222LWdtU23Z7nGt2C5ibrIuq6+478Qv1G+tb0Wv5fEvcczzBTSsN5Y7rX/aRULK7k4s0aNUola0l41YgZjIGOIYbxeI1rVUydJnjsKKskW7gB18Gjfd9P/xZC9QLrztou4lLjyttS5brpzvCS+vrv1vF+8w7v9u6S8Wrx6vRvFb86N15vkefhVDe0fATOGRL1NKVfZXdVic2ViZqpl+2R4YdJeG1k9ThhACy9WG2UI+fSl6GLZJs0MyAXDl77Kvvm+pb+vvWC8nbsduAe2RLb1s+qybbIhsq+zG7jUvszJtNTJ5Yv51A9pJ2w4UEn+Vb5dAWNSZvtmc2WlY9th4FumUnlGTjigJUEToQZ38angNNKzysbJusIbwGS+WLmCu5O6qLviu2e0lLS9sIevPrD/rfqv7rI0uDTDX9JJ5yYAShvGM05H/lQ0YD1oa2qQbGRso2s0aR9lxV2OUzlCQzOnH+0MKvbv54Lbq9dRzo/NxspvyYHHTMXgwsi/rrl8tq2u56v9qiuqNqujrEGyt7xAyYPfQPZpEuYrrD+QUnxfJ2YTa15ti27Ha+ZnN2MRWVdIBjhFJUoSg/3o73fiXNmP0gHR+Mtey0TFIL10t7uyxqv3pwqlRqLBnjmhaKePrQ64ycoS4O/3FhVsM+lJm1hdZFJp6WsHbIlrXWmRYdlVhkj4Ms4hKw1g/x/uwN4X2LTSW81JyvvHtsLVuzK2xq9Oq4GmHKNwoH+fJKN7qlS0f8W42fjzFBX2M0JLsFrfYkloy2mUadBnk2IhW15OSj2hLW0dzhBlBXv8O/Ly61rneOFd1trMUsMyucKtwqSXntKcp5tQnsmjK66jvcvXO/t5G8k2f0zgWghjOWbQZlhlKWHvWidR6UTfNG0k4BZDDh0CBPl+8ZvnZ9wD0UfHarlyrJejO50imv6YjpoUn4am4rUzzO/uvROzL4pGe1ZcX9hil2OzY0teFVdfTmtBhDTIJ34eHhe/DewG3f3C9BrptNhnyvS5+qxgpZagsJ7Inv+jBK06u8bU3PREFwY0EkdCVRRcE2CFXw5cL1XhSzdBEzYSK8AefBMHDAYESf6A8pDkVdSawd6zQqmzoYidppsvnIefTqjsuQ/UJ/X+Fxw0cEgjU1RasltUWYlUr06xR3A+JzaBL60krh66F6wP3APS9Bji7szhuw2wBqY/oZCfWKFZpdavWML93RIBGiX6OxhLc1L5VX9X31REUB9I/j5KNvMvgyjrI64bThP2Bur5Eeat0YC9x7BJqLGijaDToUylqq5xv8TaPvcAFFEsUD1sRu1KVUxOSqNEVkDfOzg4pjWEMXEsXSPsGiULZPl8477MkrkUrs2nGKWxpIeoI6/3uu7SLPJ3D5Ep/DlnQ7xHvkqOSUtEjT+NPEg40DSFMrovcSjjIvIWEATc6ifSnL+rssSqe6f8ptWoHq+/vEfSZvDwDZ4nPDZgPxpFikbQROlBmD3QOV83CzjFNvUzby9uJu4X4AOU7DXTNL++saGrqaeip9CqgLGcvd/T8+4KCtoeli8ROdg8Qz8qP5A9ITzzOxg8yTxOPPw6WTVjLCgaDAGf5QHOabv9r0mqLqhZqCys+rQPxIjYOvWEEL8jfjChNxU83Tw5PGs7sDrxOic8jj0IPiw7tjS/KS8XoPy14KjImbekrt6p9qhyqpSvjLj6yOPg/vp0EQ4jFS0DM202zDelNx84zTj2Oso8Nj7XPkU72DYaKvgUV/tD35PIJ7mor8KrLaroq0mxq7qlyfHdFvaZClUaoyTwKysv0jH+NPY3PzooP8lCLkUtRqJFckDCNOsgCwnx6SrPR7zrsKiqM6lZqnyurbQZwdbRKOgt/UQPqhnCIaEmHizFLf4xXDcoPLVB70Z8SUxKsUekQm82DCIQCDDovc0AvLKxA6zRqVKra64qtUrAQM9G4S3y8wKbEOYYniDpJoAtJDIVOPE+skSKSMFLikyLSlpEWjrZJikNfu7D0lC+WrGqqsunhqfYqk2wyrkIyCnY8evn+8gJtBO7GLUeASPVKBIwdjcWP49G60yCUa9SUFFsTUpFOzVYH9gBt+FxyKa3oK5gqaandqlVrGaxwbsOyCjY4+fr9u3/1wgiEekZkSG0KuExbjqJQ75LLVE8VfNWZVVmUfJJtjs5JScIVOjJzV+69a9yqcmmvabwqEuuXrYOwJ3NNdjI4sfssvSx++4CEAjJEOAZFSZ3NBZA4ErhUWNWZFlzWeZWfVFURT0zHxtq/eLgcMl/uCmvPqlIp+CnK6q9r4q1dr/3yL7RYNwi5YztzfK9+C8D2gkyFjUkhjG6PLFH4FEMWTpcQl5xXY5Zx1E/SHg2nx2z/Z/iUsqRuiyvyKh7pQ+mmqhFrJKxG7mcw9bNMdgc4brmiOlN8+v30QCJBzoT6SB3Loc7KknyUb9aOV6JYEhhl18dW2VRTkICLQ4S9/hr3fPFx7V/q62m9qP8o5SkmabUqwOzBboYwPvDCcpqz+PWjt7J5N3tY/d3BXIbgysVOt1ITFQQXJRiZWYIaNJnlmb5Yr9b8E9JPfUjrQk67ubT6MC2s2aqIaVCpFOkP6UnqXStL7Jbtpu7Y8HMxQXLz8+f0kzZ/uAd6bf2fQUaGGgkmzGNQIxOe1ntYZ1lJWYiZhxllmJKWxBP4zuvJGUJHfCV1RfCtbOyqxKnnKRUo8qkkKZKq86w8bOOuLa5k7xNvwHBG8QayHPNdNUr3n3qqvuWDO8f9DN4QiRROF3kY5ppSmzzbA1sWGkQZfZbp04lPAgkFAZ+7aHXMMhYvAuyKqy6q9OqT63Jroyxg7P+tmi96r+FwnjDKMQeyRTLgc5D1L3cn+lq8xUFHBcyKIg7F0u7Vt1ei2XBaTBsSW19bSxrx2a2Xt1TTkJgMekYKf9Y57rSXcMauBOymaxHqruqh6wfrOOsBLHftPu1OriXuaO7u7xmvEG+1bvDwJfIlda14mzwLwVrHDo2u0vpWXViaWjsbARv/G5wbh5qFmXqWmNMUjsxJWYOGvdC4rnShMROvBy287IVsjOxh7BDskyyhrPqtKW4/Lv6ueS1+LaAtd24i7zZwrHJCdIE4cH7dw+oJRs5qEq8VUNfWGXaaZBqVWw7a3Jo3GMvXLpO3z8oKboV9/qz63jbe89+yF3EDMB+vpu82r5avZK7uLj+tuG0s7PysVGxy6+jr0KvhrNquc/GhNBs5qn9DhRZK0hEA1FYWg5gw2VvZ3VnMGZwYvFcJFZhTiBBoi59GVwFRfdE6KDa69GCygXIesXjxwzHKMTSw0DErcKCv8S5MLbAsu2yELSrt3G7LMTA0xzpkQLOGqYymES6USJby19VY/hkSmRPY2de4FhCTwVFezi2Jc8Sogb/74/kNNsb16zQHM1Hy73JssPJxSfDrL/bvU+8drf4tUu2sLmhvA7HHtOt4Sb0UxA1JaA6Y0nXVEdaWV/1YUxinmDxXYtWKU0AQBYxLBxfBsD1rOm23y/ZfdWf0PrOx807y9zEO7+lun22drOasMeueK8XsV+3b7+7zZ7hCPkAFTst9D6VTP1Uf1rqW5BbNln/VIxM10ONOBgnchRkAzv5Qe5258jhBNrz1AzWfNIZ0PXJt8WTwJu7RLnvtvazdrezvO7EYtSc588CixszNONF4VDDVmdajVqGWXdWuFGxSAQ/Py5JIIASMAcx/vX2wu5M53zgFdx5073K6sGjujC0GrDRryWwdbKNt67BDdW+66oI5SVaOA5FxE7vU/lV41SJUpFMHUWrO08x+iInFuMJi/0x9ovwkuda4A/Ysc/ixqu9N7gss1CwI68fsZi0LL21yg7i3PynGzI0gkSGTLFSNFQmUxxPZUidP041tygSHjURFQcpAd/8zPei7vHjuNd+ypbBQrnnsreuS63QrduxjLpQygzhsP40Gp0zm0JgS6dPA1LRUIdMvEUfPjgx0yeJHZMTaQe7/0X2Le8z6G7cxs1sw1W5CLQCr1iun68zs068EsvZ4Q4AixxmNvBC3klYTeNOvE2BSTBCGjl+LTkmMh4HFioO1QYm/PrxtuOi1U7HNrtts5+tuasLrVqy4LwXz7boZwuMKFM9/Uh7TuhOyUzWRxNCjDgwMLco4iCgG4MW6BA/CYj/uO8s3tPLFr9etUKwkK6Jr9SyD777z+XusA4wKoE9v0dsTTVQrE4oSgxDajxyNHIs4yeOIEIZGRGoBZf1e+IO0PC/JbTWrcep+am/rXi2RMb54GIBTCDCNiRFpEqUTWpMqUgtQnU72jJiLpUqPygiIjYaNQ79/GjoKNXVv5uxs6mdptWmxanCsnbEsN0jAQki7TdHRWdLgk1lTJxGDENUO3c1LjDiK9ooRSLxFnQHb+25103DD7R9qRCkBKP1pBys5rrN0uXyshQ6LyY/z0jXSyVMWUpMRq9BSD8SPRA9+ziMMxwqXBpCBL7n2MvXti2p0KJUoOeg3qWjsMDE3+FhBqol+jlqRiZMC02SSsNJ/0bKQ0FBTD8DPDY4IjBdJMINNPBx0Wy55qo/o/6e/57RomirPL6b2Y77rhgIMP89wUUlSQNJYEe/RtpEQEXJRQVGFkP4OSYrGBVm9pbVdLqTp3ydi5kFmUqdTaT6ssbMm+0LD7soCDYQQbVCB0UJRkpGBEUDRi5H2EjoRe1BBjZlH8EAid+hwbGsvKBlm+mYUJvTomGw9MZI5EcEEB+1LrY6NT9OQNZBokK3QzdHSUmiTV5N9knRQdsvBRjn9kTSXLdEpGCbrZe5l4Cch6XwtejND+usCTUe1C5wOE48yD0TPqJBPEVYSU9QR1KcU2dPGkdVNkca6fec0zu0UqO3mVaWLJaxmrqkVbS3y47oWgVaG24pbjIoOWM89D1HQpNIjkzyUORVP1dJU65JITvnIeD9BNe5uGKkVpmulC2Uf5eDneGoqLxs1mnwMgpTHBQq6y+DNfY62z9RRfNMsFIEWatbCV30WelRnUK7J8AFn99dvjmoY5t+lqmVJJg5nx2pjLrfzyro5f5REfccEyanKysyozkCQhFLPFRFWslf22J+Y1teZFNpP/Qh9PpZ1HC3l6MMmFeUYpN6lsicoaemtr/IIN7O8voADQ+OGKQfgiUZL405lkLnTJxXhF0eYfph+19XV1JHKy+bDTDl9cGJqlmdmJYalGOVKJmkoKGsfL1u0SXkNvXLBW4PtBgUICgpiTOOPD1I31MHW5phUmXeZuxkyl6VVPE/ex8M+oDWCLp7p2acEJiTlouYX51rpvqwor/Yz6zfqu0N+iYEnAy5E9EdcCjiNMdCvE+uWGdgeGQGZ/hmyWMmW/VLHjFXD6jq08o5tKukDZ11mfCYsZxxoqiqcraiw/vP/dxy6FXxtPj0/yEJ9xOUHt0tRDmuRlxUZFy9YoBlgGV0YwBeU1PKQM8jqgLN3qvD76+ApXafyp5qoLeitKcDsUC7mMda0KDZX+Ah56DvPfZF/ecGEhJiId4xoz/jSyJVTlx7YC9iwWFwXWFVxkfDMrAXh/rw3VfDLrOFqjClaaMepBCnjqoQsKa4gMKnyinVx9045WbtwPQE/VIGJg+YG0IpXTfQREhPwVYwXIJfSWARX25ZH1HaQrQtZBUs91rdVsl/ueKvIqpsqKqoQ6rfrhq0S7v6wYXKQdGc2Gjeo+M654TtQPUcAgUQACG2LZY8sUh2UqRZkF1rX/Ne/lwvWMRPRUPPLlMTJPRZ3mvKAbwotUmv0axqreaw8bVSubi/Bsh0zRLU9tdc3kzfw+Ql6lry7fjkBIQTCiXmNeRERE8rV2hahV2iXfxcWllNUoVEUDG+F90Aj+Rszg6+g7Rcr6itSq5jr0WxqLXivDPDu8g4zLnRKtTL1gjbcd815e/taPjjBmEXlyfON3pE+054VXVYYloQWoFXkVL9SdI8QCnvEpn8A+Xm0n3Hk8DhvBa5orhUuOe6e75Kw+XF4sm5y2zSWNLT09vXi97E5dbw1/2AD34eGTLyQjZMklNxV4dZeVl9V89TzkwaQSkvnhmW/wzqadj6y9TCCb7vuI+6prplvp/AbMKwxETGmMckzCzKtsz/zkXTkNxN5xb3ngilGaotPDobRk5Pt1NgViFXTVXuUZ1LbkIqMscapQLg69/WtMsAxFK+GroVuvW5TrxXvdnBQsVPxyTJcsvGzKjQftLb1oLek+li+Y8MCyFSMzxCLUpiUjBVpFVUVFBQ/kqZPoYtiRknAYjqDtp1zu/I3sLdwsTC98RPxSzKj8gmyvnIdsh/xr/G+cpE0AvYruZT9T0KCSBfNSlDoUuYUUxU+lWLVI9RREsRQHov+hv4BF3tPNww0jbJ98UNxU3G9sZcx8/Itchqx4zGD8VsxevFMsuA1u3hr/GMCF4iQTj9RnxRgVaKWB1auli1VO5MJUHsLbkWLQDv7HbekNb2z+3Ke8Z0xUbFZ8aXxITA7L2xvPS+lcKIyKLSzd+d82gMLSZbOTFGAFAPVqZY9VcLVatOzkKaNc8iAg0M9mnkxNa+zbLLKsv4ybDHYcWBw0i+LL1qu6u69rxXwUXOfN7l9RMWPjBFRKJSFll4XB5co1lVVFdJpDhoJHcJEPIk42DZzNLCzlbLrcnIx4fDWMAZu4+3ybXitkm7VMJj0YXo7wUZJmc9/E4fVwRbe1wYW2ZUA0r2ODglfQut+MDq5uKR2UvW49DQy4LDBrwwtT2xIq1jrEixK7pxyovmhgq7LT5FeVMaXOJdOF5QWqVR0ELhLMIWKgPj8WXke9xb12TTz896zEvD67sxs3awc675rnC0y8A51nr4Xx3RPFJPFlokX3BgCl+MVpRLTTmNJE0P//2E7zDoOd+f2RjPn8cgvXq0lazUp2+mW6lxslXFK+KwC4Yx0UiOWAhfxmH5YM1abk+bPhMo+xSjA7H5Ou555sHdxNHBw463DK95qaKjIKTTptO06s8l9nQk/UHiVVliumVoZgJgHVSRRWctnRwDC3L41u1g5M3aD83Tv1W0hqiNoJ6dyp3aokWv9cvC89wiJUUvWMphs2SbY8NdWFNhQjwuhBvmDvsBrvuv7+/mF9WRwzaxOqWXnhWdoZ6vp924ANgTBtQylUy4WvZg92CSW+ZQ9UTpMVkgIhd2DIr/gfc866DZmcW+s5WlZZwlmaKbY6OZtYDWkgX5Mt5OjV4aY8BidV1OVtxLxTnhK4QgXhPwCpD+vOzG05+6aqiRmqqUIZNilUCeSLMk2a0J0DTcTNtY+F1BXIFVuEgeOCUqVR/eGrES+AfM+sXke81KtYujkpjrlA2XTJ5ArwLTlv67LStNOlzvYWRgKFxAVKZI5j5KMzkq9yNUGD8HJvGC1Fy5S6TumCCTp5JUmBCnb8HE7XscBkAYUxZcX106WTlTEErrQOk3bzBVKhghWRIH+9fbw7zrpWyZFJTTki+Xp6NXvFjkpRVZOpVOGlfQWXtY31JgTaRGZEL6PFo2LTC2IcMIOeswyEeus5uolFWTfJbkocO43NnyBeosFUiMVSBZtFhQU/hOWUycSiVHYkSpOSUu9xWY9qrPzrGfnliV3ZEClcOclq8fzif5MyJqPOZMdVLKUVpPQkwFSlZILEbYRaVA5TdYJEgHxN9ev0GpmJzGlueWHZ1Zq57EpeghD/Ar3z4VSaVL40lGSVVHQUmrSrBKw0cLQtIz/Bve9/bUurQ3oqeZxJbUmEmhaLJqzjvwlxG8Ksc67kNpRrJGqUUNRwdKO08hURBO0EboNu8bOPcJ0BuzrqE9mpOYQZsKpOOzds1W7w4Omye4NKY7Tj35PQ1AlkOjRnFLRE0iTQFI3jwGJpwDB9wBvHOoCJ8Fm4Gc1qFkrWHBwd0g/IYVhCc9M5A4ZjzgPupC+kcZTpVRbVK1UKdJ3Dg1Huj40NLTtqWopaFDn6GgpadDtTjKbuciAiwVPSJ6K5sxmjWKOP09g0MAShBPYlFjUJ5L6T6IKMEGhOH7wn+vHKZColGi1KZTsLG/ftRM6vj+tRBjHKoksSkBLww1VTxkRC5LB1CHUsxPcEjZOXggd/1M2gXBpa5fpmejWKT7p9WwLr9X0hbkyfnqBoYSzBmYIZEnhS1YNT4/DUeTTThQIlBTTOpCDTAlFBbyvdAiuyKvAqm2ptanCq2ntnXEtdU/5ir2XgXoDroYJx+jJa0tqjcCQxVLzE8JU9BS3E/ASJo5aCIGAXjffMYttqKtTKrpqbOsc7IVvMfJ/Ncn59v0ev7EBswP0RbqHtsn0DSnP0tHdE7eUQpSJVCCSfA8qCbMCJLqm88Zvt2yAK6grJyu3LIeuuDCqc2v2Djll+/e+Oj9PAXBDp8ZiSXJMvc+XUhoThpSZ1I6UFdLk0BzLo8TEfLJ1kzC3bflscSuEa80smu3Dr8oyBPUOt2q5mvxyffH/ecFPRFaHQosrzh+QhFKw0/AUsFTFVEfTCBBSjAjGEf8/N+cylK9gbe/sxm0F7bYu0jBTMnMz4HWYdye4pvmluwS8B38xwpaHcMt5TrNRrpOQVPRVZpUy1GMSwI/1yzOEJDzldlPxwm9n7bTs0K0s7YNvHvBnMmy0L7X2d104Bbl6en48IL5PQSyE9QixTA0QWVKtVDjUzhV7lTnUHNJHj2IJ7MOcvOJ2dvHNbxitgi0MrWkt4y76L/ixQPMnNDB01fXa9qm3gLly+8t/FwNRh+wMplAakuVUVhVKVdKV5xUTE5+Ql4x5hZx+y3jt86FwS64i7RAs66zEbdDvKXA/8UvygfPW9F+1SvaseCu6MP1vgUoFwwsLj/hSxBVCFqKXBNdwFtHWLdPeD/aK1wSCvj44f/O9cICvE63yrdLuD65fLwovW6/2sDLwLvCO8WYyUrQYdii5oD77RB5KGo8k0vLU7ZZQlwsXb9ZCFUxS0I+Gyf+DRHx29njyLy/m7lKt8G2gLldvNu/r8E+w1zEjcLWwB7CrsToy1LUrOOy+O8ROSsjQ+JQWFnYXlNiWWPvYONcIlO/Q68tTxYe+u3fP8xCxD682bnTuai8Er2IvQbAkb8SvZ69Pb/ywMrCK8qK1rvp/QZcId077EwdWP1f8WNZZX9krGCIWONHNTVHHR4FIusJ16rJJr+nvMy9m759vhy8R77tvCO6c7iUunq4ib2bxw/a8O8YDEApVUP4UcherGR1Z+dmXmPpXmhTLkQFMUMXufzt53LWmMyUxLvCyb/Fvui89LpEts2yx69drsau77A5tt/DsdhV+X8eMT0QUdVetmf1a7FsWmtGZu9d4lEOP6AmEgmD8njiRdj9zvXFu79/vWW7L7d/scmqRqWIowKnMa2MudbIHOi0ChAwvEu8XddkHmnDaMVnjWGlVspFUDCxFbUEqvJV5cTZg9O8zknM9Mbxwai4/rGrrD6qmqgOq0SymsDz1ob54CA2QiRXnGSxanpsp2qOZhheZkyXNXkZpv4P7f/i6dqO2DLPz8jKwgi/aLZVrO2lbqJXoNakGbE3wsfflwerMGlKllzSZXNqv2pQZ1pdYE+yPaQpWhZnBQ359+yz5nniG9krzz/BdbQ1qtiixZ+en4ilL7KaxrDqIxRmOXZSFmEWaFlpkWdBYH5V20KELn0ZsAbu+BnsN+Ug3IHSpcRhuNasj6GmmjGYpZcYnyqrHcW96gIWSzyJVTZgK2aTZupgC1rGSlg3zSO0EdgJuv6U9L/snd6K0PK+27DDpOWb6JdpmByeo6wPxx7vYRv3PupWkGJyZu5lTWDkVmlJsjkGK14bChK4BGv3AOv51nbDLbHpomWZ05PxkpuWIp+8slXXlQJPKQpGKVgbYD5gm1rLUhdENDcJL6AlHhlwEZMFe/yI5/DTk73HqQyg+Zw4nSilRbPX0Afy8Bv7PgJTM1ttXCNZrFFRR5hAvTdfLYolmBsuEYMDAe0H0hG6hKexnLaWxZaBm/Kk6Lvt4t8MTTFNSDRVy1c3VZhSSEk3P2c4uzIWL5ckURmECaTszNG0upGnoZ2ymH6Zgp5Uq9rEr+tCFAM1cEdjUetTe1N6UNhKvkTMQIM88jnEMbgg7gjB5yzJQbKlojScNZrMnUWofLwW3r0HmiigPv1KR0/oTMlIK0OfP0E8KT6AO+MzTCKgDQTwI9AbtrGmiZ6KnFmfG6kIuszZqv46IKk3WEGZRYNEjEOMQfxAREDeP7M7gjdOK2wWvfMB0tK1aagbocyfJ6Ibq7q9xdvM/sgcCTAoO24/tEIZQjJDdUP3QsRCYkJUO+MrjQ2+6TvG2rGmp9OikKHEpjKyycdC6AcJfSDKLQ02bTp7OXs6vjx3QDdDI0W/Qn86HiihCrLkrcUysEWnLKRjpiytkrrR0unx0wziIcoskDF3NLs2/zprP+JDSUnTSQpHYztIIykCetqPvLOtmqZVpTeoc7BiwLTXD/SaDMMdfCcVLFUvYDT5OVlBskYaSiFJQkOANcMZTPUz0965iK1iqAeo86s6tejGq92E9U0I3xW8Hy0nzCocMzI6AEM1SZBMNky5RP4zGhpj9OHRP7qVruWquqqrr0q6JcrO3unzOgRCDv4TixywJeouoDlAQ9RJmUwbTLVFlzU+GZfzvdBuuq6vmKz5rPmxX7vnyaTbDvEi/0UKuxFiHP8l/zCzPNtGPUzZT7RPS0q4PFIkOADH2lrBiLMgrmysr6/rtsTBwc+n4B3wSv2BCNkTlxxGJrAzDEGjSetOaFAGTi9FADOjFSTwCs/xu+SxGa4+reywd7mDxrTUhuTF7zT4LwHnDN0YcyUNM55BLkvlUBhTGVGXSd45ox5W+jfXDsF3tEOwpK+yshW6bsRY0FHcquR47EfzFfzoCPQYhyn+OcVG504XU5lSO04bRNUvzhE67vXQQ7+atkGzgLRQuFW+7sYR0JLXN97A41fsUvUYA1EVpSk+PLhJm1FbVf9UnFGiSBg3JBqu9yTYbcQOuby0VrRPt1i8D8MVylHQFdc13THjmuxB+KAK4R/VMo5DXE0RU2lVR1RRT41DUC0sDynuh9Ikwsm5NbZUtm24pb3Cw33Jq85K047X3t3r5APxHwQiGRAvpEEKTVBTq1XbVQRT6EoiO1IiRQMe5nzRO8RmvbS6droUvnDCnsY4yrjMrc5+0qHW4t4O653+AhSdK3A/Ek3UU8tXaVjyVbxObkLBLBEQsvBu2K7HVb9xu8O69rs9v8jB3cQRx+jIp8ljy1rQEdrS6Gj/cRfsL8JCHU9mVpBZqlkZV+dPoERAL3EUNvhb4CfQ4sbswWbAf8FVw4nE8MTExLbDvMKGwkTF5Mlm1UXmsv56GJUya0XLUS1XHFqGWWdXNVCuRPcwihlV/GnoatfxzWHHrMXAxVXGtMWzxQvDdsCnvkm+tcCxxH/O3Nsd8toNLSnpPxJOHFZPWvJbNVuNV+VP10PMMDwYJwAM7QvcytFqzHLJGcfwxavEC8FCvnK7u7i9t9+4n7zqxDbSheeeACQcMDbARupRNFjEWp9a/VeOUm1IzDkKJ5sSYfxs64Hgv9c20SXP28whym3EGcIFvla6D7kMujO7ib/EyCLZKe8TCu0lNj3ETDlVyFriXIxc4FmNU69JAzykKP4VPwIW8ADlWNvd2KzVf9G5zB/JMcTxwPy6DLhRtZy0Mrbiu6rFTdUq7I8J0yWmO+RLrVPfWKJa61iVVXpPBEWLNssidhKDAcfzbuon4j/aBdbk0jLNX8f0v4e5wLTAsVyxcLFYtK+6/MUd1ynxnBJ0LK8/Mk3WU+lXq1ioV89T50uaQIQzMyG9E0QDGPkr8D7qXOOQ4LnbwNW1yifFB7wjtpGxrK9RroWvNbViwX3R8Oq2Cn8oRz1jSn9S+FVxVolVzlHTS2RAljI1JpIVqgeC+n7yEe285PzerNf/zTfH3MC1u/yzga9hr2Kw+rTAvVnMT98s+W4ZPDNDQmxMyVF3U3xSF1CoS3RELDrILFcdiA7qBBX+l/f77pbmG97j1U3OJsdZv/u3trNssbKwtLMPuj3FA9V669YJoiMSORVFFksgTf5MoEtjSU1BhTi/LbUkIRumEVcKDga3+hX0ZOsV4JHVqsp2wgi7mLTzsjG06LbxvKXIE9qU8mESLCv4OnJDbEmbS4pKWEiERDE9djShK2Qj+xY+DjAKnwSN/FbyCOu03+jRscluw5C837q5u6W+KMRB0MvkRf3tFfUq2DfHPlJC9kNNQ8xAYzv8NIAuPifgIh0efhh7EX4KQwLp9PLl89cxynzCS7yQuaC4pbqLwNvMleCv+T4S0Sc9NdM9aUH/QIU+pji9NGMuFSd1IbkaTxZKEmMKZAV/9sXnI9nXzBjEOL5fu+C7J7wnwWXKMdz08uALmh8OLJwzhzcdOUE4cjMAMAYrBCZaJfEfrht3Eu8KwwGM8JbgJNIRxmPAfb3ovHe+ycLty/XbTPXjDmsjNjA8NzI5zDhgNm4yDi0BKBEktCBBHhEa9BOxC7D/pfAJ4jDTI8nDwqjAKsEDxPnJgtTW5Mn8IxMMJNAtrTDNMakxljAWL/gr9yodKgAnMSacIREXWgg79dHhs9JqyJfEp8JDw5XHLM8O3ZLzwwh1HLcm5iwTMCwv/i2mKwMp8CeaJ/AmRCQ+H0kYOwrM+VvmYtfLzFPHVMYPx7fJ4dEx3o/wagRVFpYgziRPJZcmgiUdJvklICaYJp8mvyPLHRkQe//c6ZfZt8/KydTHTMhby5nTdOG/9JsHaRbCHnMjlSWrJgUmnSSXJQ8nVybSJfIiWhs9Dkf63eWN12/PS8wby7HMHdIj3GHs6v9RDyYaER7yITkiVyP/JC8nICqmKxQrYyl0I0wYEAMo7bvb49DHzHvL/swF0Y/ZGOn++csJjxS5GPUcRB70H1AiYCQlJ7InGCdFJM4b4Qz1+O3kvdaszzHNa82K0MzWXeG07wX+CgpGETEXQhqgHGEfiSNmJgYpgClkJ4ggcRNM/S7ontj30UvOc8570IzVu94+7u77EAnKEMIVMBjCGyYfRiPwJS4poimaJvkgphLk/L/orNrb0+TQnNCJ0yHZNOT28tf+oAfyDMMQ0RQIGHYduyLBJn8peCpZJuIdoA/x+mbn79lT1BfSwdLk14PfPOgy8078fQPVBoALZhERF7UeCiZbKtQrriolJt8aqwYl8cbfatYA09TSidX12kDjtu3998r/HgXuCLoPRxaTHgcmsiqWLCIsTyglHzcNBPdT49DYntMg0zvVktmE4Ybqw/K3+hgAugZsDIkTrhzQJOgpaiyALJApdiHbEIL6WecH23vVj9T/1QzaiuCP6MnwEPbe++UBXgiXEJsaWSLrKGQsZy3DKgYkBRa7AJjr+N7n19fVPtaS2cDepOVd66XvivRX+hEBqgw4Fw4hCig0LEQtdivCJCEXOQHB7MXe+deo1UbWl9nS3obkdOnK7s3zqfhM/0AJShRnIGIo6SzYLWMsKCcJHKEJEfVJ5Mrb3NfR18LZEd6Y4xfoGus57nfxLfjfAvIPKR2SJggsNy7QLUAroCOEFM3/V+yC4J/b+tkY2x7dUeET5TDoIOv/7fbyw/vJCAQYTyMqKrgtXS7kLFwndBsrCG7zP+Sv3GvZndn92t7dC+FP5CfmCOhW61jxYvrBCPQXyCNDKhEttC1OK7olxxk6Bwzz9eUH33zcTNyg3UffYeEn4yjlKudn6r3wEPtcCsoZ9SQNK9Et6y2gK9gldxr+CPv2XOmK4aLdttze3ZXfkeFj4oHiAeTT5o/suvbZBS8VdSETKcgswC20LOoozCA0Ep4AbvGg53Ti9d8h3xvgIOBu4F7gTOBy4djjqOra9oAGHhhKI0Yp9ys+LHIqdSWyG88MuPtL7yLoNeRN4iDiqOLo4iziGuH53yXhu+Ow6sz2vAZcFrAiLSnzK/4rmCrNJlgfNBNMBav3Re6i6YznQOcP5y3mquWq5BzjZ+Jy49XnaO/i/CINVhomI8MnzSkMKjMoIiTUG7AP5AGg9ibvEOtq6Kfm6uR549DhXeC83pfe3N+D43nsgfqKCokY7CHpJicpOCm/J/MiURvqEEkFLvsL8/jtreuL6onqAeli5qPjO+FY4FvghuIp5+Hvof1RDeoZjCHaJVwnMidaJekhmxstE4sJMwD592vzA/DS7Rfrqeim5djiROEM4M7fCOKp5rHuOfvgCTEX7h4zIwUl3SSEI7AgRxs7FKcLYQPh/GD4dvU68x3xKO5n6j7my+KG4N/fGeAM4p3mFfDs/NIL0BfMHhYilCO3IyUiXh+nGgUUZAxKBa3/Afsm99j0DPFY7pvqz+bF44HhduCD4E7iZeYG7WH4DwaVEZEZLx46IFkgMh89HZwZvhSpD/wJlwRZAD39uvoF+Bz00O8/62vnruQw44/i0OIx5DLoWu8t+q0GVhEbGBMc+R2AHqId2xt5GHEUkg9DCgkGtwK0/3D9T/pu96nyCO4s6vfm0eS7437jwuRw5/Xs0fURARQMGxTzGEgbEBwGHL8aVhgJFV4RfA3UCfUG+QMYAUf+LftE91vyk+0i6qrnQObI5VPmJOii68PxbvvaBMkNxBPcFkAYiRj9F4YWJxRcEbUNzwmVBo4ENQLM/9v8LvkU9Xjxk+2L6mXoa+dW5zvoIOrJ7XbzYfy8BW8NhBLyFOkV3BXZFBIT7xCbDrEL+ggHB+sEdwNeAsP/3vvh95Tzf++J7Mjqs+lS6QHqzuuJ7hb0MvzTBG0LUxAtEz0UexT1E6wSoxBGDk0M+wn6B7MG1AT7AmEANP0R+T71XPE+7kfsc+t66zfswe3N8Ir1Zf0rBYcLUA8gEfAR7hFFER8QPg6VDNgKIwk5CG0HHAbJA+8ACf3k+Ab0+/Dz7mDt7ewd7RLu3u8589n5GQAUBicLJw7UD9wPvw/cDoUNkwzZCpoJ+QcEB+cGOgXnAqX/TfuB98bzWPGv79ru3u6N7+/wzvMH+Bf+pAQcCYsLGw2aDYMNugzzCwwLgAknCdAI0AfpBnsFzAOhAKf8xPgG9X3y//Bk8IbwAPF/8vj0U/nP/ucDTQihCqoLVwxIDBMMZgvRCmcK2Qm9Cf0IjQc5BRQCUf67+Qf2nfMW8nXxavHe8dby4/T2+PL9xgJvBtoISQrFCtMKjAqfCYcJbAlXCQoJbgiRB9EFqQI5/9b6Kve49IHz6fLi8oXz6/RJ9wD7tP8dBBcHYQgtCTgJKgm9CKsIoAhMCFAIkgjyB9AGTwRkAYD8uPhd9rT0NvQr9Gv0pfVv9yP7JP8HA8IFEAcACHoIWghkCBEIIQg/CFsICQgIB8AFIANT/5j7bfhr9qL1UvWP9S72kfcv+sf9vwCtA2UFXgbHBvAGIgcnBxMHnwfDB3MH5QaiBSoDrf8G/F75bvei9mj2dvY095X4rvqw/VUAuAKAA1kEGQUSBY4FBAY4BpUGywa+BsUFGARWAaH9i/qZ+Lv3aPeL9zT4cPm8+5j+KwEEAx8E8wQ1BVkFcQXTBSMGjgbpBrwG1wU4BLUBWv4x+4P5xfiM+Lz4Y/mw+nj8xf6fAEcC6QIxA4oDDwSXBC4FuAUZBuMFbgVZBDwCJ/9P/I36svlo+Zn5I/oa+4z8Xv7s/yEB8wGTAuMCewMdBMcEJwVvBV8FxgSjA58ByP6S/Cr7nfqD+r76Wvtn/MP9Nv9JABkBpAEhApkCQgPjA1gEkwSeBGcEzANTAj0ACf59/Mn7ovvH+zT83Pz5/fL+wv9QAN8ARAGvATECxQInA3ADewNBA64ChgG//yn+LP3G/Lf83fw1/b79c/42/7D/JQBsAMcATQHhAU4CkgKsAqgCbwL8ARMBwv+g/gv+zP3U/f/9R/6h/gD/Xf+u//f/WwCWAPQAUQGQAaQBnQGAAT0BtgAJAFH/+f7c/uH+9/4f/0v/fP+u/9b/9f8NADEAWgB7AI0AjQCCAHEAVwAzAAsA7//n/+3/+f8=`},{name:`shoulder_left`,sampleRate:12e3,frames:7440,wav:`UklGRkQ6AABXQVZFZm10IBAAAAABAAEA4C4AAMBdAAACABAAZGF0YSA6AADg/wsAOQByAMIAMQGzATsCFP0K/Rf9Kv1O/Ur9I/3q/J/8Q/yn+xL7sPpU+gr6APor+r36efvD/HP+OADiAcoDMQVRBi0HqAfsB9sHQAfkBkAGBgYoBiwGMgdkCE4KkwzYDu4Qve4E8PfxdfPy9GL1svUY9Sr09/Ig8q/w4O8o73rvRvDo8d30kPhI/FIBbwUhCd8Lmw4JEGAQ8g+PD84NYQyeC8AKKQoWCycNaxBjFJkYnhzHHxEj3OKt5f/nbOlW6fPoOej25hvl+OOX4nPhKOKT47vluOkX70r1c/tRAjkK6Q83FO4XBhrkGY4ZHxj3FS0TuhAfEO4P3xB5FXsa8CCvJ+MtOTMWN9fYYN3S31jhouGo4Bjfddu21z7W19RB1N/V49fA2q3fV+cu89f7WwVhD8wVvxqWHwgifyIUHwQc9xh6Ff0SoBFNFMIXVBxCJVcvRTYcPrREjErWx+XL388j0nXS6tGczjzNtsi+xV7EpcTQxVvIMc4d2CDhHPHC/84L+hWqH0Il3CcRKNAmTSMzHsgbvBm3FJcUHhu9ImErQjTCQGhKDlOkWrBgaWQPwJLDmsaPxlfEE8LsvFy5Y7fvsYOxkrO3ts685sT/0oHi7fFJBKsSLBzHJeErfi7eLWMoaCJuHq0aMhYjEYYSIBbiHZ4ptjZuQklO0Fh6Ya1m/Glbvi7CI8LhwAa+hLj+sVevva3urb6t6rICuVDATczz3BzrEvuxB1oYuB9mJRQp5SZyJU0hMRyJGKMQ9g5TDWAOYBaxHmkrhjdTQu5P5VfUYLxme2ppbHO2d7hmuGK1BLXqr96rn6lyqJ+oM6pFrmW1Jrr4xMXX2uqN/SMN+RtXJvspTC3bLZAmJB/EF9gRnAp9CVYIaQ4SGY8kbjNkQqFPo1rqYOdnNWxybRxv9LHFruera6jypWGi6J9un0Gfe6GXpeiqH7PHvj7MQdu+7M38tApXEiUVpxlOG68dqRyCF1gSlgpJDLkNZA6RESsW6R9uKrw2bUSyTgpbAGJ+aM5sPG9Ice2zfrR4tFiuOKsupnSjraCjoQukwKWZq7q1ZsHB0PfehPBE/h8OPB0IIhAkdiR9JIgk0B9gGLwTEg9zDcwLOw4QEZQXbyAyMNs7+Ee7UE1btmPJZzNrqW5ZbhqusKvhq16pIKU2pvKk76Q5pSCmt6vesQS6P8bNzofcSee7+OQFZQ0wEwUbORkIGYMXTxS4Ed0N8QrWCEMIsQ3pFEEbbiPGKo85vkalUWZZbF+yZKZnb2vLbAxsTmsZqYqmyaRAol6if5+lnoqgnaN7qiuxgrrCw8bP7OG78bj8uAb7DL4MTw+2EtUP+QtnCgoF/AC0+9P8cfy2ANECdQZPDL4daCmiNtNCtUtDUPhYmV+FYgxieGJHYgNi56THpS2kEKNOoh6iRaRNpk6p3KqurVi1+739yjbYV+Dj7K75MwnIEmcUoBIgFPoUxxLJDUAHFf+69832sfvr+vT8NAFkDswb4SmXOeND9ksoVMVZwl/kYI5ijGEqYcVf8aYSo+ShyJ9rnrKdzp57oOyjIKoUtMK6ksdz1IXgie5P+80CFQkqDosRmhOkEHkNrwiAAUj9Xvgp9B/zLfOr+2//4AoWFwslGDUgQTRLmFH1Uxta11uhXV5eZV06XV1cZVs1reCqjKpXqM+pXqmzqFSsrLFttx2+yMpg2CHeG+qw+ngCfgfKD6QUfw9SC48M0wfz/4P9jfk59X/zjPY9/gwBHAuZF7gg6CmvM+I6vUFpSbhPk1JSUxlU/lTJVJFU71PfUWqpMKhTqBqnJqdJqAirpa05sMi2hr35wt7L69c64ArqcvMi/Dv/8AGGBMwEXv4M/7z4oPY38y/x0PKv9r/5vACaBTsRKRq2JAkvbjdqP/FHQE0eUFBSIFNNUyBUuFOtUDdNr6xYrMCrbatkqxCsCK+1s8i48b1OxdnNgdki6DfyIfi1/j8Fzwc/CeMHKAKJ/Kb36fIt8Eftme9c8aP0ifqNAGoLMxZqIRAqJDL4OgNCwUdcTJhNtk6sTjlQSE8NTRFLUkmyq/SpralQqYupkKrhrKqwaLWwvLrIutL/3RzqA/Mb+WD/qQJlBBcCeAHc/RX4I/Qc83vvwu347ib10/f7AI8LvhhvIu4tQzhRPwtD30hMTGdOKlCcUMdPBE7jTEJLx0iHRTGrOaqXqxCtCK/FsWO22ryyxU/QU9rs4fLnwfAy9bj44frA+Ir1jvGx8Grv4+m16STpGunf7KLy/fpbAlQLzxgRItkq7DMDOx9AmEU+SJ5Jn0hLSL5GaEMnQqY/CzzNpjqnwqjHquOtebJDt1a/U8fqzFLVx90U5vvte/TX+Mj3pPes9gf13+9i7Svo6+TT5Bvn9+wG9IX7HQmTEzUj/zBQOr9BOEazSj1N202UTkBNgkqhRxREfkLxP9k+s6QdpVinvKqRrq62n74xx0/QvtzK5unsdPHA9074gPaO9LHzyu6J6cjnVeal5fHmUOyP9RICLQ3wGqQm+jE3PCJDOEjkS1dNZk45T3ROuEvUSONGhURVQv1BZ6LMo8OmbKwgsn259sPIzgzageTo7S/zHPcC+PD35fJP73bqxOQv3ovds90l4nHnVPG6/YILIRxNLHU2G0CbRv5L802mTqtOEEx5SVpH6UMpQ49BLEPunQaf1KG1pq6s1bWhvtDJKdbR4J3sf/LC9Ff1p/GF7qLqv+Qv4LfdX99Y46rqKfS4/iAMaxx8K3M4pEKYSXFNOFBsUtZRVU8pTI9IO0REQaNBD0QjmR6cip8Ups2tBLi5wofNpNdq4ezm3+zP7HHo0uTF4Cndh9g41+vXRt3X4xvwZf4vDywhgTDuO95F9Ev2T7RSelPmUXtNrklaRtdCwUFuRM9GpJbRmUSfk6f3sZC+7spt1QzfUOdb6lvpHeUp37XYqtJwzo3NXc5L1XzfCuyR++oN5yDQMYg/HEnhT4BSsVS/VKFT4k8DTNxHeUXnQ6ZHo0w8lxGc+aNor4u7KclL1gHggeaB6D/n8eEC3OjWH9LZ0PLRHtX229rm4vcZCOgahi3NOw5GgEwdUVlUqVJvUBVMKEaPQT8+zD/rQd5If5OPmZ2i260eu97J1tXo37LhJOCn3ILXK9FkzWLJUMhNywrU7OH78rYEwhiZKr46dUflTqZTz1TmU3xRP0zCRfpBFkDrPutBl0kCkmOYyKKEr2m81snG2A7g0OBb3ULYJ9P+zrLKX8i7xyzNlNiY6G/8ew0OHjcvnDu8RcpK6Uu8SthHdkQUQAQ8hjvuPOFBsUk+VHGZ8KNlsFK/+8oA1W7brN4o3XPXfs+tyNXDEcMBxirNpdd55vb62hEZJws3ZEJASZpLskypSuxGTUDnO2k4+jkMP0pJ9FLJmX6iba9nvGTJ8dID2cfXitPgzVrIwMCTvGW8ccB8yCDYE+2kBB0Y3CybPVBGx0kSS2tJe0VEPCU3gzOAMes2K0AATIVW7Z5Lq0S6Ocal0dzW8tYX1MzPGsv2xUrCb8OEx3TQv+GN8RoIHhv6LRk7RESVSeRKdUYmQ748YjWOMi80qjbXPeVJmVUeoo+scrs7x6LOFNVR1m3SB84dypTEeMLLwmbIedM74nv4xA4wIVI0tj85RL1G5kV6Qfo5aTPNL7QrUSxBNtdByE5EWgqryLeZwvPOydVe1N3PFMiRwZG9i7r5u0K/wsl42+jvcAZXGe4nfjLxOH85jjaxMd8tgigFJwUrUTGxOY1HkVJvXK2yDr/nyFvNjs93zkTJqMQjwMK94rxCwHXKjdWe5tz7zg9pIqww5TmrPgw+2TuAN6gxhC4tLMMsGDAQO79GE1QHsOG6YMYYz5DTitX1z6/KLMUIvxi9P7/zxBbQeN1+8RUGwxaIJ3QwWTTfMhwuqiluJrgghB76Hfsn7TI1P95KslbwtavBt8hrzJ3MwMm8xy3E8r7iv3O/w8Pj0aPeJfIABMQVQSO2Jzkuui8VK2woOCHNHrkeOSPyLWM4a0SGT45WcL4BxefL2M6Wzg/KOMeNxLrBQr4CwmfHiNEJ4NjzMATTEXEfeyh7LPsvXyzkJQwcbhrYGQkd6ykpN6o+yknPU47AHseNzwPVbs8syB7ESsEzv+u+6MIiyObRC+Ev80sHgBc3IWonHSmVKB4h/RxvGZ0WmBQFG3IkADFnPeRJvVE2V9TFUMpGyiLIXMWLwHG/7bwxv77CM80A2FzqEPqvDDcYCCT1JoMmYiKUHlEUEg+LDX4QRxhWJMsywz/lSvBSE1jfxoPJ7sc9xWfC0b3ju1S9QL4Qw1DMvdlO61j8nQwyGZwh4ihEKSElOyBVHDcXRxalGMQeWChsNotCC01OU8BXy8jey63JVciDxPnArLu1vE7AEsjJ0SPiP/abBgsXwiavKggsRyhaJVMdgxY6FaoSwBJNG38nyjQlQgBNPVKBVQDDQscax7XE1sESvfK70byJwGTHFtHw3o3um/7DDtIYgCAFIt4heyDfGSkUNBLXEN4V9hv0J4kz/kAkS91RJFYBWenDbMMewsW+0bnutzq5uLwTwW3LqtfW5fL4LAnJFTkdXCGsIdsZ8BTLD1cIXQjdC6kTGh0ALb45O0cyURNXallNWwfAyL1quUq2BrPWsrCzvri/venI+9fc6IT9mRBzGKYh7iLtI8MeVxjSEvcMngnCDO8TqyDiMDhBS03iVMZaBl6eX3nCor0LuKy0ibF0sZyyYbbSvsPIHtp97N76TwzvFeQcbyCvHAgbzxT5D20JiQXMCnwUIyCtLk8/qUtNU+5apV8dYYe75Lc1tQKxeq01rd2sUa+MtJ+/JMwi233oo/ZyBN8PPBNYE+4RzQ0YCoIKcg2NEb8XHCNsMsg9/klkVFJZdF4cYIJhnrXjsuGxcq+trC2vBbBqth+/q8p22Lvkn/k+BvkMOxVFFbkRPw0hCCgEZPxTAD0JmBOCIM0sST0uS/NTy1zbYF1iQWIUrjmtUamCpmOnpKbnqoKwtbicxCjVrudD+mYImBIaFpwZlRguFB4L4QKi/kb+swLECOkT6yDkMGdB+U9bWKldI2HhYvJiEKnIpgKk06EMoeKhDqZlrKO108P21QnkMfIIA8gLOA8KD5IJIwLz9tD2sfen9F38dAifGLgqwz1HTNdW5F2nYxdnCWb0ZKmjOKBFnwKdzZtunfGfTaehsa+/UtEF4mTzIQGSCfwQiRJOEP4MLgQu/aH6R/3sAPkJBBYDJV00o0TKVOFcsWI4aJpolmjSZjSiYZ2amdCZRJy8nYCjj6w7tqPFwNtC7d37EwMiChsLJQtLCWMB9fo/9hP0lvkSAXAQDR3jLOI+Xk1PWNtgOGXgZwdo5mf7ZpqeYZsqmo2ZuJofnBCiY6qCtN7GmtdR50n1t//uCe4IAgZWBIT7u/UW8Bnuu/AH934FnRS7JuQ3UkexVFBd42FoZeRluWTPYShfUpfBlRGWN5eymWWet6bss2HC2dL15sP3fQPkDFwQxRHLCKUAyPdn8X3pzez/8ef8uAqCIYE0OkXOUWlddGLqZdVn12ZkYyRgI1xvldWTKpMklBKXIpx0pEOw373yzMrZiOu79dz5+/oa92zzG+505x3lX+Dg443offZHBkIW9SlKPFJJO1V4W9ZgxWGLYgdhxl59W+FX+pV2lqqXWZoHngCnb7BqvbXNzN0g7ID1Mv17AIn5nvQt7sXqNed/4xnl+etQ9/QHwxhcKm87AUnUU8db7V5+YFRgcF+XXJRXJ1TJltqWUJivmxuhM6nVtIXClc8+353t8/YJ+7z+L/oc9KruaOkw5JbhIeTK6jLwdADbEQ0k7TVWRMlN8lSkWOZar1ltWBVVI1E+TiRNjZgBmhqd0aElqCi0zcAW0BLemO0G+Vb/dwHnAVz7FPYq7+Ho/+S55Ojn9fBH/DUJ4xqUK446r0UxTN9RCVVnVXFVPlKCT4BMe0r+nMGdpZ8mpMOplrKQvc7JDtZR4KTpiPJS9Aj1ke/A5izhwd7y3ijfh+BD6RL1Bwf5GkcrVzj3QUZLUlDcUEJRPFCDTWxKekYVRaxDGZ+Gol+mI6uUszW+c8qE1pviruqA7wHvYu8L7GvnKONN3wHf3+A85QHvBv2lDFIdwC79OtRDQUmITIFOS02iSt5GpUG8PoQ8/j2woQal8agjsBu6TsZw08LgtOsw8MPywvJx7mrpauLX3ZraEdoq4WvozfThBPgU9iXcMdU83UTdSOxJVEo7SJpFLUJGPzc9ijwUPfylzqlTr2e4u8JIztrXUuIp6rvrQelo5efdVNlm1onV3NV32SzijOzf+ooN2x1sK2Y2ED1EQT1DJkSVQto9cDkONnI03TX7Oeemfqszsli7esfa1HjhFuo+8EfxbO1u6EXjL9zs1+rVrtpg4FXsxvzrDdweyywkORpAQkPBRD5ESkKAP6c7pTnrN0k76j59q2KvlbXMvf/IQdUp3/jkOOqP7GHs+Onw5XDiHt4L3vnhq+jh87n/4A4CHowp1TSfPEdA0kDLPgE9wTnGNZU0FjQCNmU6capBsK+3z8AQzKjVi9+k4xjnwOZY4xbfAdqf1WLWm9lD4hjtUPsQClIZJSesMnk4YTqiOuU3WDQkMAsuwCvgKw0veTc0qs+wrbhWw/XNddpJ4/XnpeWw36fbKNdD0ljQs9B31rjgyfH7AcYRkCPhLwI4kT7SPyJARTpNN8kzgy/tMIIymjbLP2mtrrYkwmnOC9ow4KznXefx4Z3agtUHzxLOn86U1NHeFu9fBKoTJyUpMo04uDwpPto6TzW2L9IrdyihKoUwbznjQp2seLUkvl/KgtKS02fTy9FZzZjJScaMxKDEFsrU1ubnQvl4EQchNzAqOnY/8EAnPME2CjNoLDQrHSxDM1Q9nUeGrl64jMEIyz3V0tno1zjSGs72yKTG3cd0zW3Uf+KL8rQFaheWJzExOzboNs80OS5gKoMlfSUGKJUxRT+iS19WgLjqxyDV6ttd4GLeXNaCz0nM+8nSyAnNddf+5mn6UxAhJDYvwjlzPFw8jDgeME8riic3KOksvjWFRFNR3q3PuEXGec9n06nTOc9XxivCIr+Iwf/DLss72R3tHAXEHDMrvjSEPag7tDwtOC001ytiLJYxIDt1RftS2Fw4syO+m8kfy37JWsU0vnu5fbg3u96/E8oy2w7zLwv4Hi0tATJYMrIwGSo4JY8exh3YIegpIjczSKJWGGIStt7A5cWPxwPGp8JDvXS5uLl8wb/J1NkS72UIxRwDL2k1dDrfMjEuwSi1I30hISbVLn1BXU/dXJdmG7ksxSvLycuxxtTBCrviueC7zsMOzpThf/WeCu0csSwCNdQ2cS9zKGQeIhzVIispGTTlQyNTkV+4aPe0mLmMupy52bWYsh+wNLLyuenHCd+y73cGSRufJ5ktXi2/J6EgshjYGqMdfyfRNatIP1lFZJZrAbVotquzo65eqo6n96UnqLWudby71OPuNgoyH7QsvDHpM3Ix8CYVHoYagxx6Jzk5Kk6BXNpmI26ntwi8w7zatluv1KgdqsCvorp3ycbijv3ZEyAlMjMqMqwrASX0IQUb1BlLJMozUUVGVx1jB2wvcFW37rebs0iubaepo0SoQa5vvpLS8e67BcUVHCMjJvkheBgtDTMEagFoCX0dKDTbS7VbUmhJbhW1bbbTtkyw9Kmsp+Or8rNwwSHX9/KTBsgXxSERI4sgYBgKEngJ0winFNMkdTy2UjJgtWlcbXuzsLCQrIqpjaWyoxiozbEdxuXaefeZDjMduybLKCYhtRioDaQKbg2WGZAwNUSHUhJg8GbXamStvaq8pi2gCZ/1oiirHrluzsLlzADfE20fICD5HJ8PyQUw/rsDvAx1HXIzBkggVdJgo2e/aF+nE6L7npycm571pJqug7uu1GnxDgkRGFQgORtAFFcLnwfnBf0OPiDfMn1FBFZeYWdnv2n9aAyrQqZ3o/GjYqiNsCnB8tRU70kEYxFgGHsYyQ8uBxIBov9jB2IV/Cd+PbJNlFmYXrRgbWBYo12gJ54GnUufH6fMtePJQeDK9T0I6g5FEVEKLQE++Rr42wLRFOAmZj07TidcgGG4Y5Bj7V8CqMmknaSrphGscbeRyirdavQmAkAJsAUCABX6GPap9lz9oAjEHuIzBUdvU0NZxVxkXAFZ/KXtobuhCaUUq2+2WMcj2wjwugAhCx0LyQS5/uD4Mvev/dULmh70MsZDyk/RVOdWOlWSUkBPR6LboTel6qrDtuDHaN7A8GH9PgKZAbL3RvC46kbsQPEz/SQS5yi2OihH9k9VU7tRqU4mTF2kcaONppGtNbfIxh/bzO4l+3sABAB6+fvwB+vI6PXqYPgiDLshMjaERTJOMFKUUnpQTEwwR32lOKflqvWyiMCx0/PmwPToAAEBGf009ITtOunP6nv02QTSGBAvTD75SD5OQU4uTMJJ0Ea2prSmWqpksJ66d8wP3czqi/R4+RL3eO1258HkcONR63n8ixCQJcc3jEM9SrBMUUzgSOdC1D57plqoA64ttj7DZNKB5Mbxm/SS8jXsGOT430Tf1OUC8iEEVRpLLYQ7LkR5SV1JrkVFQQA9uTsoqN+rMbNBvhLMWt4L66bylfX08Pjq6+Ii3tTf9ubv9SUKzB+oMAs84UKhQ3pCHD3NNw40jDQWpoKqUrK9vpLNCt4o6tDwx++m60nls+Af3NXf4ukZ+dcKfx3nLsk5HT9rQlpA5DwaOY04EjqnqH6s2LQowGLOkdqw5cfoXuhR5FDgI9lM1hvbn+Zh9d4K8yBdMRk7XEFBRDZCQT4zOkQ2uzUyOxCrdbJTvXnL8tnQ46jqQOuN5RveEtnL1oDY4+B98vsH3hvJLUI7BUKrRO9Diz/SOJIz4jFTNi8+lqywtZbDAtRx4kPrx+0x5y/hIdp3163WWNuc5jf3Cw1nIn0wzzplPa48ejlyNLQvQy0mMI43k0ALrdW3dMXw0yLfV+Lz4KLdMdbQ0AXO6M921qfjbfcbDx0jnzEHOqE9Yz0NO7I2mzPwMFcxCTt7RVWthriNxLnQ3thf3dDdq9dJ0mbOosvUzp7WJudM+ooP5yLvMPs5RT5mPgo7MzZSMZQvQzK0OQZDEqk9skG/XcqV1cPZ6tvm2ETU6cxpzA/OpNb+44z3ZwzGIPgxLjzgQIpAFT1uN1AxtC4WL/gyTT52Sjupm7R9wdDN1dNJ1bLTy8vgxaHETcTIx9XSH+N/+GMPpSRXNLs78D11PO81RC+4K2Qpny3oNEdCEk/Tqey34MUBzm7U09O+z53Jr8P+wf/AYcVi0H/iOfg+DoohvDGVOWU9kzzROAYz7C9FLnQy8Dm4RitSg13ptdTB08s40qbQRMx6xfy/ab0JvLLBkMvS2wPzhgxnIV4xKjq2PiI9bTf2L50qkCb6JyQwXjwtSYxWgmKltZm/dMh3y1/JjsPbvde5Aretu/bDANBB42H7iRHEJWAyezlnOLE01S8pKZAlDyf/K4I3w0awVvRic7CavJvExMdnx5HC6rp6tFexabPTuU3Eq9Tk62UCxBkjKlw0GzhGNZ8uUSgXI2oheSTULbs6UkmoWIJkMmx1uYy/EcPAwAu7ZreuseOw8LMTuzPLKtz08QQJRxs4LEA0mjZbMrwrbyKjIAQiVycVMPE9p03xWwNmym2Kt9G8Fr6kupO0Wa4rq3CpO63XstS+5tAL55/9PxVVI/Ir8C6nL8kpXyLPITIiaiYGModA1E0aWtVlpW0Vtuq7Q7yntxi0E7CpqxCqdKzQsSnAcNPJ628DoBeIKcYyYjNNLxQnUh4VGXIWNxpaIjIz40U4VTtigGtBcRK7mrzVu0+z0K6Oq/aqO6tJsDq5s8ho37D3Bg0cHG0kryjpJgQjbhzkEooUoRiQIv8yN0RkVCxgTGqfby5xNLZvtWiuZafQoqqiKqMsqVS0o8Lt1lbyWQwXHMEj5CfxIXwcVBXvDYQIQArREVIjPTVZSo5YFWIyaa1sx7BEsVGu1qmRph2nxakOsCS7dc214hP5Xw3qG7ghGyXQIm4dJxXeDVMJ4g1UGB8n3jjcRwlXzmGEaQdrTGyfsSSxSKzJqLqm6aXQpwm1t8CT0ffkefvACoIWNByOG48Ucw/UDP8IWAkJFBEiVjOaRO1Qt1ySYopleGYDrLqnd6OAoc6jZaSNqRO1bMW43MD0aQyKGhEfCiQkIdYXUgx4A3gETQhlFl0nhjXlRkpW2mCxZYRmxmVDrmGquqjIpjmkG6UEq7G0i8Yk2aHuKf45CxoT1RJ7CvcEfvqu+T75GQRMD9kfIjOaRz9Tm1vXYK1iV2C/rUWpZ6dvpsmmYqsQsem86s1N3qfzcgSMCTMP1A2pB7sBtf8xA30I9xIeJVg3bEXaUlxZml4fYElhl1/8sQGsfaoqqiKvrbWJwbDOVuO78qUDNA5PENwORAYJ/qr5ufmI/iUHMRR8JZI2+kbZUOpVRVccWFxXL60ZrKepgql2rVu3IsKM02XlBPeQATUOCBEhDJMCz/tT9lT1HftOCf8XPCkqOldEyUsRUE5TuVIDUASpiqeRp6KrFbLcvAXIfdcO6ZL2wQN3COsEKv/j9k/yJvTg+Q0FpRC8IJkwaUFsS3FS61M6U+xR/k5mrRWusK7BsZG3ncZC1b/lGPcQAkQHiwX+Aq39C/O38Wv04vx4CiYbny4qPTpGXUt1TXROYkz+SeiraatorLGys7u0ygnbQO55/+kGjQfMBjP+8Pba79Lrle9M+EgKzR/2MGM/CEkcTYdOhExwShRF76luqsCsFrAludDE09SN4ffsjfWB9Jns/OZh4JbhwOQc7Z8BARNYKSc8QEd5TZxOZE5hSxRF20AgqWuqXK7utFDAZc6w4YfywPpu+pz2O+/v6dnoE+ww8zkAHBPrKBM4V0IxRl1HskY9Q+Q+oD2ip7+poK//uW7Iati36Lv0CfmN9I/uauah4lPflOQ38IwD8BlPMMs9pUbcSQdMvUisRd5Cb0GVqmmu4Lb2xH/U1Of99Bj8Xv1C+Y/1oe1z52fq4PJRBNoYHSsZO15Dwkh0Sb9FfkLvOxI5Q6WUqBWv37hyxkjZYOTU7hbvYOzl4kfbtdp239roU/w1EO4nBDZKQWBHjUZhQzk/jDlTOPM7b6i5rtO5Tsk218LhceoR6pTkT94Z28XZr9/q7rIFzhufL987CELvRdZF/UBTPTQ4HDlCo0KqArXGwvTRP+BW6gjuQ+vi4oLawdVG2lbnUvxsEoInzDabQAtFs0Q8QPE58jUeNjs8xKMarvu6Kswg3Lnm0umb5nzfetlp1ZTZAuXv+SYToShLOZZFU0kLSptHxEKJPbs9eEMRpPitAbzly/7a8+To58zip9oh1b3Q0tSZ4tj1Ug1IJZU26kG6RlVDCj7+Ni8xJjYLO+meSamjtu/IGtci4Dbh8tkc1uXNIct+01flOf9yF+kuLT1aReRHEEa9QXs6zjdPPe9HNKMisB3BUc+j1iDZr9MHzR7H+sU4ydHU1uxACX8j5DjXQWJFd0IgPLg02zH1NoJC6lEdqjS7vsog1xLdmdmD0KfI58GQxabQr+ZJAyEeBzToPydFNkTrO0AymC+BMg8/4U7Wo+azBsXY0aDYvNRbz3vFH8BrxAvOheNGAGocYzTxPyFF80QnPNo12DROOGBDIlHloWqy0cLe0LrV+s++yKbBB8A7xPDPEeY8AfYbvjK4PlhCAT45NtIw7C0eNQ9DVlM6onSwLL9xyQ/MHMa7vlO6Qbp7wPPTbu9wDtUmEDiCPxY9yjYaL7YpoSgGMcxCOVetpGe17sAgxhbGrr/+uLO1BbgZwQHTFvJbEd0nnzeFPfU3cy9BJ0QmBis5OvVLeV7zqe+78sYbyrfFrr1xtxS2B7xxy9LjRQJsHk8z7Tw8PBE00yqrIr8jBC6OPl9Sq2LGrE29NMQAwpu7ebTesoC1X8DQ1ITwRxBLKbs20DzxONAw9SryJEwp0zZiTNRfAGxVt9e/UMDPvRK3x7Kisra31scB4SUCehwaL6E5VTcGMAQpASQHJ08xa0VLWf1mJXBvu37B2LvetQWwLq5Gstm9U9QC86gOoifQMwQ3ITC8JlogrB8OKIo5IU44YORrPbdqviy++LfjsSKss61wtFzFOeHQ/WsXZyayK/YqZSNXGzMWfht0K9VBmFYHZbFsiri/uUa1nK3IqMum1Kt8tyjOrOrwBmQd0Sq8KdcjQxvuFB8WdSAENgJLNluAZ1Vtn7nAubi0rK7bqj+uobnSyybmpQEeGJAlvyiZJXEbghAbDxwUuSb5PLlROWCKaEa47rkXtS6wGqlHqGSt1LmbzifnvgRiGEshWiPmHUYUaQ93EV8cjC5YQnRTJ2DpZue1T7RCrsypNKcVqQKybsE82IH0QgvCHN8hsx3VGCwQUwwUD70bqS9QRhJXt2EqZZe0DrNerkeqJqqEqz20qsV53nf8GBCKHNofzhlDEyYNrgxTFnsmPDvZTZtad2KiZfK6SbVDsJ2rTqswsNa71NDW5pf+SQ9CF5QXWA/UBLL8EPn0A6EWoC9CRHdS81phXqSzxLEyrXWqdqqvr5G6T86r5yP/5RC9GkAa0hG3CEoD3wS9DVcdvTC3RNtRFlpnXX9dMrElrMGpqaqTrqq4cMkB32XzkgSVDpIQcwnIAEv9i/wLBbcXGjCiQvROcFfaW7hbvbYIs0qw6q5qtGO/bc4h4S733wY+D6EPRQoV/k/4qPbr+2oLxiCsNZtHO1ESVgNWhFR/ry+suquUrQWzir4j0fHnL/v/CXQNPAhXACz4N/eo+GsDxRX0KaE8xEgMUK5SKlHlTxKt0qr5qpGuWblvyODb0PEqA4cLKhCMCkcDkftg+1ECiBGiITMzkEILTSZSv1PnUC5Nr6yBq0qugrKnul/IVNoC75AASAWLAvf6j/MD72Tv3PRNAlkUViv7PF1IdkwzTi1MfUnnq/mpXKqmrgi2E8W51lnqF/rIBNcGbwJz+Crx5PG59doEaBZgKa08OUcaTzZRL09aTUxJ3K3KrkCwHrjtwR3V/uiY+R4C9QJE++f0Ku606qHsp/WHBkob8C8XQHxJQE3PTbVJzUWrqreqyqtSshi7Usrd28jwV/14A+YEc/4c8x7u1utj9TEDQBZXKUk3hERkStNMMEwMSQJGCqqmqnCtmLFcvAXLGdlz6ArvQvOK8Yjt7upM6G/oBPdCCN8ezzTdQRVKwU4VUDlQfEt8SAVGiatkr7G1HcGd0PrfrPOk+Zf8TPga8NHpI+XJ6WryKP8CFZEnkThgRO9JQ0vER29FB0DtOlilJacrrF+2TsME1ivl9/Gt+AP4u/Cm6srmQOe766L4FAscIKMuBT4TRRlJAUp7SMxF/EKFQgqnQa1Jta3BXNY55THuUvK88YPqD98i3UvcP9sh6an+YhUTKz88a0dATKNN0k2cSUVEsEIwon+lvq2fuaLHjteP5JPut/C38tznb9xU2ObWCt0J6iL/hhe0KeI7dEUWSiJL1UZiQ/s8uzo5QJSh/qjRs2nCotHR4MTq5PG86Dzh1NlL1fzYzORd9usOgCI2OXRGOkyDTZdML0lmREI+Zj2BQQmfRaketsrHqtkG4kLorei45m3fvdfH1u3V19197jUJjyADNG1Bt0ZzSG1JS0XwQXM6SDrDPouaEKOarwO9D85p23bkAemt5Z/gLtnj0vLXN+Dh8mkKZR4GLkA8QUYrTBtLa0WiPr87fT1mQrRLTKAMrHO5j87J3cTnReZB5UndV9XZzYPQztRZ46P1URAKKSY7c0UrTJNLV0gtQys8qjo/PPdD2pf/oFGtyr3ZzJXYptwG2AnRy8p0xSjGGc362hvxBgq3Ja05dEVkSxhKK0WlPoQ4ljSqNbE8UUhHmP+joLMOwRrKvM+X0MnKHsa4wDnFZslz0t7pSAD1GVc1ZUSoS5xKuUYLRF87OzhhOpk9BUrZWCWjVbHIwsrQ6NW00/nOD8Vrv9m9l8GwypPcxPh3FbguaT8HS+xLXEpbRSg7zjLfLVsz3D6bTSqaDaoeu83K9dPa123SJsyFxM7BMcLpxFrWie7nBzEiojUaQhxGUUFsPZM15i09KNIulDrYSvxZr6IyszvB9Mor01vNGsdWwNu80b0cxAzUU+u3Bacg9zPSQCRGI0YlQXA6NDXXMGk2Y0L0T81fiKgFu8rHzNI714nRtsqMwOa7Xb0dv87MY+Ej+04UhyuQO59CGkLqP4A49TF2MU42bj2WTJpZ/aBwr/K8JMSixQHDsbwwtwi26bV5u2TKW+KR/XEXay5DOok8TT1zOBownSVyI2YqYjbkRfBW4GSjrTe7UMV/yYbCZ7x8tx+2c7ZqwqXSx+fJ/QcawSovNjg50TfHL3smdiSHKH8wCkCMUB9fUGliuhrFDcolyOnCd7xRuNq2Qbs2xevYrvAzCuofEivaMJgwmCtrI1sddRpjIR0v4EGOUrFgf7Etvr7DocTsvYa2V7HusCW0i7zdynDidvwKF6cloC1BLMon+x4uGyMb7B2OKn4+VlG5XxBof758w47BaL8UuNKy0rBRspm7ostX4nv9TxXaJE8s0i0CK9EjGR1IHW8i9C/xQoZSZV4/ZuHA3cWIxdvBgLojtR215bn2xMDUD+66BQ4adSYmKzwqzyJ2Gw4WeheiITgxMEbBVBpfoGVOwtrFv8EVu0+3/bHJtC+9A8055Dj+mxe1KBAsjS7AKKAgdxlhF4kbPyYXOCFKxlX2XfhhDr95vOu4H7V2sRKyqLk4xu7ZV/C/CskbISSRJgAj/hi8EJYNgxJVHLEs+D8mTpxXPlxLvS6857fNs3OyL7OTuLTDTNZo7FoCwhL7G9IbABaCDWcI/wfmDtQeJDKeQmFP21agWv7Alb4VuwC3TrTLtkq/u83n4Qn3nQviFvwbGhsKE0MJAwYaC1YX2Sa/OJdF508/VZJXmsDVu7u3tLaat+u+xcjJ21LxSAVJFGoZ2RWVDxMGTgPQA2MM8htwLlE/cUp9ULpSUr4IvGe4/7U6tyy8vMWL11juggNeEE8V7BS1DfMFZwOqAxkMYBswLuk/AkooT2xR/cB1vYm5HrewuBe9IcgR2g3t+v92C4cQiQ7VB+oADf0p/0IMwh7VMphAA0nyTBJOkL95vFC5s7i3u4LCQdEq5QX6qQm4ELIQcgvSAqz/NgAxBsMWnyi4OqJF20rnTOlLrb12uvC33Lg3vQfH3dZe6ZH6GwSHB0YDZP2w9sr2x/3ZDIsglzObP7lGVEk2SWVGp7irtiK3fLuMxC3U5eeu+GsD1QYqBZv+V/fe+B3/5gxQIGAyQz93RQ5I1ke3RHC5dLhcurm/V8oA26ruWP5cBaYEIgAB+dj0nfXf/UcP4CPbNZxAdUVORnFDwz/Lte60xbZ9vEvIEtpF7iH/oAYCB/j+lflG+Ub8pAidGCYtgTsoQ9NGSEZgQxRADrdxuKm7F8Vy1uHo2fnlA9QEBv6k9fHw8e+z9CsGYxtgLb86UkFsQuw/8juItNa0lbfovnLLX9y47R/6AP7m+YPxweyU60rz9QPsGdIsfTroQDdEuEEAPh05E7Nttd28RMgF2o7rF/kI/TP91Pag7/nqvPFkAGsV5yklODQ/3EFPQKU9WDqAszm1dLvJx9vXzOis9k/7uPdz8U3ta+wD89MAIRYvKDU1ND0/P5I7nDXGMJavqbLEufPEK9QP5b/xCfew87rraeLg41DwlwI6FyQr3zeaPeQ9LTx7N14yMK/Asq66cseZ2OzoR/Hh8YXr9eX44V3jj/F/B2ge8jAEOwFBDUBfPEE4GTRBsMS0ML4GzX7dYuu97yfssuUM3zXdCeNX8msFmx0CMPs4mjvcOD00WTBjLySw1rU/wT3UQOVN8L3yH+185/fiDOVG8KEAsxkkLco39DyGO0425TFhLW8wBbBOtzDFydbA5gbsNOjB4kfeQ9wy44ryVAm0H5gwBjtUPPk3nzPZMFwx8TY3tRjB4tDT4Y7r0ecf4bfaqNm+2mnmKftmEsQoxjc5PKw7zzRzLyIuBDEWPIi6SsmG2MzihOU/4ubZV9Uf1vLcRertAqsb1i5dN2w5XzXYLAMpTi5uNqq24sPo0n7f1uSZ5Zvd4Ncw1iDZzuQK+0wWdSrYNCw5/jPiLhMpWiizLtk6KbubyB7W9N6Q4Pja9NR40bvTrNwY7+8IqCAKMFE4PTYbM2gsmCqeLSA3d0MVxBvSAt0W3o/amtNV0Y7QUdmd6TT9CBUIKBcwgzOLLpsoByGRIV8reznhumPHhtEi2AHWh9LOzCrM5c523LfwTAjmHJMpeSwDLGEmSyOgImYoETQeQMzECdGi14jXf9IHzFHKsM2m1XzoqAKAGYYqDi/SLzYrdyLyIfshUyo0OG1DbMvc1SPZ19gS0tLNOc1v0ILbCe9cBcsX1SIhJvEijxtDFp0Wxx3BLJM8tUYN0G/XfNq41prRqc0czTbT3d+l9ssNdhsHIpIjxR9BG50XaBsCJbsyaz/eRhDScNZ61HLQxsrFyWPLLtQD55H7/hB6Hu0g8R+iGC8Rsw9mFFoitzAaOutBMM5F0VTS6c0hygnKzc453C3uZwAgEVIX2RlJF4AQfQuYDWgVnCPfMSs+DkTU1R7XrNJnz5jOws6C1fTeTPAmBZkSzhpOGagPOg5UCqILvBbmIhgxpTqSQIrW9NMV0EvNN8t8zS3VPOEF8uUFlBOLGboZ7hK9DE0IqwtzFDkhQDCdOc8+1NUr1WrRdM5XzYvP69Vm4cHy0QSYEJsWchYIErELWAY/Cc8PmR4ILxQ4wzuJPVTVbdN/0MTQZtI11nTg4vEIA1oP2xUjFk8R7Af8ApMGYA/2G80pUTJRN1Y5NtVG03TOUs2wzqDSyts+6Jb69QkIEZETLA9KB8wCsANzCoISCR+NKwYzRjY5Nx/VNdE70EbQB9NN2X/livTBAUUKQQ0zCzgHoAL8ACUCdglFF3IjPyzvMCIzZDMs00LRws/hz0TUOd4K6nf3SQJsCO0JQAjZAzz/Vv6BAjMNpxuYJuYsujA/Mnkx69Rp05DSqtP417rgGe0D+v0CHgYJBigCcP0X+nT8egP1DC4aGyXJK8IuGy94LvXV+NNt1WzYkt0X5kDzgP86B70IOwh2AVT89vwO//oGoxKbHaAlSCr5LEUtbyvE1VbU19P81TnbOOTn7tz4OwHnA4P/LPw8+Z/4afpIAWwM1RYCIWgnGCpOKscoz9a+1WXWZNkd3h/mAPDp+ZUB1wNXA9H+t/pR+Q79nQJHDc4XxCDyJbEoOykJKPYlfNce18nYxNwS4/3rAPXl/PL/MgAY/bD40/Vc92X9eAe6EcYawCA+JJwlQSWiI20hCdbc1ozZVd6f5sPwevkh/lP/Qv1D+aL31vd9+lsBSAsGF7IeiyOLJakkESPRIFbXo9f42BjcVuFt6VPzpfiH+yz74fY081jx9PPz+s0DYxDLGcIfBiN8I/0i6SAJHoLXp9f52ebdmOSB7fL1A/vP/F76FPjw9Hb0w/a7/SQHahGAGewe8iDBIMYf6R0DHPzXBtmH27DgSehZ8dz3CPu6+zn5PvbD84zzjvc+/vEH5xEyGU8d2R5yHjMcLhrUGCbYutnp3JHi2OmA8+j57frQ+sr3TvSK8bnxePa1/X8IehMoGhQeHh/ZHtsdERyuGlIbwNoA3ofjkevS8nj44PpU+Zb2PvOm8UfynfYU/2sIiBHlF14b9Bw1HGIarBfxFRQXpNmG3Mfhnuij74/1gff+9k70gPGh8J7xUfbW/qAHPBGrFwwbFhw/G6oZFxeBFYMXqNpi3XTiQOnW74jzt/XS9B7xwe3i7VzvI/TN/LkGBRAQF8Ea6BsFG4IYBBb/FOYWfRo33fzhn+iV7j707vVy9JDxzu5U7nTvKvSB/HMFag7pFRQZ0BnpGOIW7hMVExUVVhkn3e7hGejI7SvywPSC84rwK+557Wfv7PQW/Z8GrQ+OFW8YcRk/GNQVlRPFEqQUVRit3Z7iZ+ia7kby5vJP8YXuT+xr6yDtvPE2+mMEuQ0FFSAYhhhBF5gUFxP/EfITNhjbHHrifui17rjywfLz8GHuWewC7OntjvJL+kUEbg0RE1QVIhX7E7cR6g9vEOUSaBcQHfDjBOor7/DxJPKg7y/tDuvY6mPt+fI1+6UEpwyQEakTLBQdEvMPtQ6hDjkSwBfyHKfkselp7mzwfe/j7Rjr9ul96ynv8PYKACoJMRBIFGAWOhWFEjgQDQ56D2wT2hjJHbPmKOyn79Dwlu8C7brqgOrG7CzxCvgyAqsJqw/QEisT5hD2DT8MqgxoD6QUwRnSHZfo1uzI7kPuUezR6dnoHeqT7fzzr/ysBVsMhBC9EZgQ/A1qDG4LYQ1CEQEXkxvs5zTsx+5a7pvsierC6QDqx+x48ZL5PQK5CUoOARC9DiMMhglMCC0JPw0+E8wYhBwN60jty+0J7YDrQupK6hLsoPAd91wASgc4CwoNbwxpCuAHPQbMBzYMuRF4FyUbwuss7nfuRO0c62np6OnE6/fvl/U5/poFFQrtC5sLCAmaBjEG2AeJC68QShbbGdQbTu5D7nXsBevf6YnqaezH8CH3eP5MBY8I3gg2B6QExALpAmIFxQpsEKIVFhnh7YTv+e4X7aXrUetf7MnuWPNq+hABcQYQCdEI8AZwBI0ECQaRCd4OXxPAFr0YUO/I7zjuvezR67br3u0g8rz4NQBrBWwIuwhIB0IFhwPyAyMGaAqOD8cTWBZAF/HvDu+Y7bDs/ezE7iLy7fbv/Z8DwQZUBw0GzAMTAnsCKQWKCWUOKBJZFE0VFfAr7+ztQe1Y7bnu3/FX99v9MANTBj8G2QQpAwEC7wIwBYwJRw7yEewTmxSG8mHx1u8572Hv0vAt89z3R/2MAU4ESATIAjIBWQDoAcYEOAmwDYYQ8xGDEhryG/EB8O7v5PB+8jT2jvvj/1UD9gOVA/oBEAA9AEkBywTdCRsNlQ+AEG0QCvJN8QjxdvGo8o31j/kh/s4BKAPoAmkBp/+U/lX/gQKvBm4KNg1qDqIOYQ5P8vfxDvIO8yv1RPie/EMAmQFZAUQAs/7F/bT+3wFlBQEJnwvZDDANvQwC85Xyf/KD82z1fPiC/Pr/EwK8AWMA5f7P/e3+0AElBScIbAqZC/gL2wuL9PTz//Pk9Pj2kfkD/aD/mAADAEf/1P24/dz+tQFWBXgIHQrTCsIKXgpA9QT1VvVJ9jH4/PoZ/ggA6QA3ABP/Of5U/p3/ZQK8BeIHIAleCQwJjQjG9bj1R/Zj97j5A/yY/ub/m//l/g7+2v1w/ngAjwP+BZsHRAhMCOMHQgfu9hz33/dW+WL7aP2a/vL+P/5c/QP9SP3m/pgBXwQzBhQHNwftBngG9vcm+MD4D/oO/CD+gP+2/13/lv7w/eH9+f4oAZgDfAVjBosGXgbdBaYFJ/md+Xf6w/tX/Xv+2/6T/uH9mf2W/eH+AQEAA4IEPwVtBSUFqgRoBMj5Fvrf+if8uv3//kH/Dv9g/ib+Xf6P/xkBkAKsA1AEXARHBPkDygPP+jf7G/xO/Yv+TP9T/93+WP4W/lP+9v56AO0B+wJRA1ADDQO8AqgCpfsI/NX80P3e/pb/iP8g/5P+Yv7B/ov/wQDAAVICjQJ0Aj4CAwICAoP84/yN/Tv+1f4b/wb/0f6H/pz+Ev/n/9IAkQHcAdoBnQFXAS0BQwFm/b39L/6+/h3/Tv9G/x7/Cv81/7j/YAD0AFYBVgFCAR8B9wDlAAEBcf66/gP/Y/99/3D/V/9F/1D/ev/T/zQAaACHAIYAcABUAD8ASAAt/0v/ef+b/67/sf+x/7D/t//C/9f/5//x//D/6f8=`},{name:`shoulder_right`,sampleRate:12e3,frames:8160,wav:`UklGRuQ/AABXQVZFZm10IBAAAAABAAEA4C4AAMBdAAACABAAZGF0YcA/AAAQAGkApwCgACAARv9s/tL9XP0A/a79s/5q/77/tf////wA9AJiBV8HggcYB58FfwI//qf6Z/g397f2jfaz+WL8Af4b/iL+5/5WAQgGHQvtDmUPsQ4yDAAHof+r+BL0CvIs8cbwIPJg9jX5a/qi+uf6Jf1fAiAKORKEF1wYqxYKE9ALOgEJ9xrwH+zd6hjqAure7NPx2vRn9uX1ivbe+WcAhwmDFOocVyI0IaUeDxk2D0wCIvff7WLoh+UK5YrkYOOp5n3s5e7Y8HXwkfC38vL3AwDrC4AYjCLAKbws3yoCJhYegxKHBMf2eu3i5d/h+98k4Azfid3N2vrgYOX35xjo+OfS56PqVe+I98kCiRBRHtUqqzPuORA+7TwzOZ8zmCoKHn4Q8wBZ83Lq9OLr3qLcSduP2uzXqtcy1lzQNdDi0w/X7djV2XHaPtoj2z3gVuPp6ArwoPnVBtsUXSKBLeA2fUCwR2tNsVHpU21SK0/DS+5G4D5hNdEp+x3mESEEJfuf8LHltd9M29zV4NKX0GXQ4c2IzNLLQ8z0y3DMTMrBx/7CYcO+xyLLPMu1yezJkcjsxXjFGccExuTGocdeyPzHq8v00QXXHdwf5CDti/i9BBcQwhsRJn4vcTnwQdZKglFLV8Zb7F/WYxNnuGnYatxplGcNZbtiul2kV6hRMUrORJg+zTWYLsomTxyEE8EIzAFJ+Mbx+euH5YzdGtvJ1pLV3NPa0dzOMc2Dy3bOhs4x0CDOhM4A0PrQsc9+y/fEW8DJvBi5MrO8sj+0rLZJuvm6CryFvdq83r2Bv8i+vb7/vanCIsXixErKnM2L0SDbW+T+8Az+zRDIIMwtuDmBRstPMljYXdBh02MLaP9qImzmZx1j5FwIU1JJ+D5LMDQhJBNKAxr0Puhg5WzeK9oF1RTTYNXp1rrWzdYy1CXPGsh7wLq2Sa5xsfCz7bCxsTG02rRWub7CoM1Q2PnoJP6xEAMkDTphTL1WZ19DZr1pcm0LZuRaWkqVNMMdkwj++NfxMOoq4xHiMOTc4A7f/tlpywK46Kjcq+ernKzXriixxbquzc7lhwGlHy87ulAiYSRrgG4HbIhc7UTQLBwSivg/6U/iaeE85ILk49wJz0W9KalZqBWn4KmAro66l9Oy84wb6T5wVpZjrWgjaxteKULvIXAC9uw54b/h/ON34/rWxcUGr72lbqgqqwmzYcfe4ncNfzi7VOtgymcwa6ZdAj7HIE4Mvf67++P8ifgs6MTPfLRlpoqnEqszt07TV/czIaxBH1RRXa5eGlL7MiIZWAmDBdD/ZPsW8W3ZHLaHnt2diqEhrB7IJfT7IW1DolQbW0Fbj0ntLU0YsArWBYACp/zS6JDK3KpAm+qb2qTXvHvkfRXvO2lQRVvGXbNZND38H30OoQfwBCb9SOhCyTKvWpgKmZmgmLKf00IDTSv9RWxOf1T1VQlD6iqeGwQXQhk1FpcJ+ewSyjqsaJ9qpV+0U81M9PccaTnRSHhPE1CbUEE8JyocIRAebBq3FNwEfulCxmirvJnOnDSmdLxl3gMF6yQfOihCREUXR9ZHxzWWIrsZNxwnHIsZaQ7M83zTbbhYpOacn6COrIq/IN0HA5Qg1DItPktB6UPlQ5hGZTb2KnMnyyYYJzYgkBJj/8vkY8tGsVWjAZ2ooXusKb6I1gX2IA4lJBsyxjdXOv45Ejv6PO89cDSsLo8qJyqdKLMneyBSF7gHVO721XLB2rAjp+ugkqTJq5W11cbk3mL20A5AIG4raTQmNZE2LjfENzw3KDmxPWs0Oi3ILFArvyzxK8YsCCn+I8Me+BJ3AMbufNt1y929qbKXqzylUKL8pTWs+bNjvYrIg9q56UT9sA6KG3EjlyaLK+wsRS1uLncuADDFLf8uTzMUN9w9bD3EOLE0qjD4MGkxKjG2MG8uty39K6YqRieZIroZsRF9CIP8PPAD5KPVZ8lqwOq5FrM7ri2rhqjPpTimGalirAivybTdurfCU8o31trgT+xZ+VADUg2XFCobViF/Jigs9y7LLnYy4i9BMLYwCjGmLpMuEi/0MPIvrzQcNrE3ejpMPHM3OzQeM+Q0NTPzMmk1TDbpNKQ03TMNM/IvvSz4KaohExryEfIL9gI2+EjrHOHW16PS4MqNwby5ILUzsRCu4qsTqtWobqu3r6u1bbrHv+rI+9OW3sToC/Ml+3wC0Qt3FQwaJxwGGz4ZKxspHB8cmhkmGtMawR16IiMqbjJROfg3bTmXOEE79zknPKY7qTeeNMsvUSduHSUNZP1N6gjbu83SwI62w68uq1SouKasqwyzSr0qzEbdQ+0j+YkIAxN+Gfwc3R/BH8ogSSN3KEUtHDWXP9dDTkKGQQhAET6JOpYzESowGmUElexV1F/CL7Z+rk2pO6aCrpG8R88i5jj6wwl8E8oZ5xzjG9Id2SEzLAE7tUabRNtDU0JuPik1sSStClvsUdAkvPWuIKi0pOqs8MBd28j2ygqzFDcYXhjOGD8g+SuXPcVF6kRwQyY+wC45F6r0jtQxvPOu2qd8pVG1m9Ab7vgCyAz+DswO2BKCH1o3DE9pT3xNsUScNHYS0OnNx3qyeqijpWGyZc4c6xsAOghCCRoKixZQLBtKclMDUlpKxTRODmTijMH4rx2pU6gZw+zgzvo9BGsGLwehEz0uZk6aWIJXrku5MU4GtNm2vKiuIquxtrLXyfT2/QT84fudCREoOUrVW59X00l6LSH9N8+TtLeq3ajUtKrV8u699sT3t//MF+I8sVxzY9FdB0k0II3tk8ZRtGSuE64OyUzlYvB+8SHyyQHOIc9JUWShYDVQ/i3c+TjPibenrrCuHLQE0EjiSebF6DPyEg7iNw1a1mXlXJVE7hhT5H3ACLLor+ywRbvh0h3eD+Je5vD1LhieQHxfe2oJYRhJ+RvS6U3GHrUZsUaydbA1xh/SHdTb0/ffh/zzKblPdWgkaodcMkBRFbzmx8uEvVO7rbi0swXFbc/I0FjSmtfU6y4PSzxlXLttGmpPWYM6TQ7F5B7M/r+au4e6YrcTuLzDi8icypHNPtgu79US8jlEV+hpMG65YyJNxilFA4Pi9s2nxZPDKsTWv2u3gbxQxKPFlcRKyA7Ur+lAB6EsrE0NYjxunnDnZt5TujcIGCP4/N+M0oXMsck1x+rFcb42tOm7XL96wKm/EMX/zBnd/vIFDxEuuEmvWzNo5m6Pa1RfIk/5NUUZBADy7KrdzdRz02nPEc5ozynL/sBmtZq1F7nIuc67vbqvvdLDLc/93MzzvA0LJcQ7xE4bXOBlQWtWb6doeV7kT1g+YSrdFcQBAPEt5TPdt9We1RjVQNQL0WDP7cwOxVq7lrFytPi0/7bDtli3PbjQu+DAlcgz09bi9vI1CmkcyzEqQ5ZQ2FvWY7xopmwWbvNpR2FtVotJcDhtJ78WzAXF9qrsIuQt3kzc0NuB2vnZWNog2wPZrtMNzi/EXrq9sFKyQbRatHy0LrWwtUq4frtOwHnFXM993dbq0/7iExgoxjnURzBU2VxPYtlnE2p3a6dm3F7JU0NJ7Dj8J28W6gWx+kXwmOnG5wnh1N/D4R3gGeFX3RDas9Brxx2+5bXVtoS4kbeqt2G3vLsRwLDJx9b65oD5vRHLJe84mUYYU6BZwl4hYnVk91zUUnFFKzTuIFAPDgBz9tbv6+9J7q/tZ/Dk7aToCd8E0Ma98ba+tl235LZMuN++A8la2xfyBRDTKuY7zEpCVblaf13VWF1LjjUGIFIPMgD390Tz4PH771DrQeQ61HvBu7EYrTitJK5Psjy+LM+t68QOyywuQtBPoFd6WltYtEZ6LmMYjgcr/aL61fs3+X3z0ObE0D64ha9CsQm287561qfzRBUUNeVGa1GCVcZVO0IDJtcKEv92+Fj6lQD/9zjlI85ttQK0drf0wNvUAvHyF4I4wEm1UDFUzkyrM9UXCwraBLQC+P1f9XbcI76crLevJbXsxF7lAA5FMn9Fzk5RUKRGCyygFCAHbwdCAy/4BOKbxFGvcK7AtXXJi+kEFHw2KkdWTEZKazvUIxEXzhW+FJYMhPf+2Ey5aK+ws9vBWeEYDw4yWUIURTpGWDohJe0XAhVPEuoE9+d6xw6upKqns9rKYPMnGd4zED9aP9FAVCzKHU0aNBx3GOgC+d/KvqeuLbbTyIPvoheUNb1AKURBQ7c7mSU9IcweExyeCqLoqcKHrB+rpLc90Bn5SB7kMU04SjpRPxkxMigAJvwjnxqS/rPX+LjJp5Csqbws378HHiJQMIE4gzlNPqQxPCqJKA8mcR7VBo3bAbq9qW2r9Lpl18D9jBuaLOw0xDbkOfw7+yz/KBQoeiPiFvv7adZMuRGqwqeHs6nLyexMDqkh+SxBL7IvIDWjOZ0yPDMYMD4uXB88B9Tnfsazs8WoV6ypu0bUN/bkD8cgYCixJ2gn9Sk4NUE22DGTMvQwsCvvHegGned6yae0LKpppI6qdLdGzI7pIga3GH8gFx8qHEkd+iW+M986xDYHNSk2rTb8MXEnNhH59NPZ3cEGtEqqUKWzqKqzBMXo2ufzHQg8EwMbPh7yHdEd9SC2JHcsUjmJP5U/p0DpQbhANT/GOiMwZB4MC9jw+tbWwru0Qakko+agRqWWrb27es7x4dTzxAj1F9kefyBEIqkhmR+oIhYn7CmSLk83yUFOTcVN4U0iTQ5MaEy3S3RGWUA8OCYptR22DgT8auYw1grJW7sHsaSqU6THod+fNKDgo92qjrabwkTTeuR776L7UwImD8EWWB3pHkUeWRwFGtkafh9tH0Uhth8eJ38vDTeBP4BH20ouS3FKw0twSfBH00oMSuhIskUGQTs8nDNJLQQjJBJLAoTwj+TG0jzIqb/ptGasq6Vto5qh+p2unGObTZ8ipritT7miww/OeNm05izxPfwMBDILxA8wFHsV+xW3GHsY6BQrF9oaHBx+H4slkS66M7Q/80tvT1BPLU0jTY5MpU3gS9pHIUXBPbo0DS7pHsMO4/ar6KnWVsUqtqasEqRAn3OcMpyZmQmdFKcytnXDxdfk6Ub1UfstASoH6gmCDfoOkApOCqwQZhmGJi0x/z70TKhVzFaXVyVXFVaWURdLK0ANLpcVd/vi3svCJ7Pqpi2f4Zr6mGCalKlavvDY1eoU+uAHQQ3SEB4RJhDqE6ob9SjuPL1O5V/RXh5fW1tyVNlHSjQyFnP1+dPnuWmnx5/tmvua8abwvQPbH/JxAMUHNgdHA4AEUwtMIH83olKvYR5ioF2tV5VE3CYa/2HWAbl0pjCfQJ7Pn3e45dC45qryYPbB+Gb6tQX4HpY/f131Y0VhIVcSQogfl/JIyqKx4KQIodShergZ15brTvVV9kf1ZwHfF1Y45lneZ+VkSFnmP9oVp+Rzv2CrJ6XBo82t7MxP5Ajuw+4P8OT/ORvDQ1Fiu2f5Xs1JECPS7+PFWq/LpzCn4avvyfHfzOV06DLw3wcsLeNULm6AaaBZijj/Ba/VJrtEsQawNK0QyWXhLOn66kT0fQpQMmNXk22AaAFXVjK7+37KvbVhrh6vRK2WyNHYAt3+3Yruyg6jPPRfG25kZKJLOxzr53THA7olt8CzMLjQzEHTftWN3K7wPxrCRmdjjmn8Wjw8ugc43fbGbb8ZwLK+wsMV0fPR69ad4KH98ChoUJBmn2moWkQ53gkc4SfNTsYXxtfCJ73bx+3LEcw61R3qOhKYPUJajmjgXtZFgRrI7krV68thyq3Jx8MvvTzExceByBHS1OgtENI53FQgZPti3U/MMHkHoOdf1sDP39LdzoDF1Lx3wZDCnsNAy+3iJQKFLZRLn10PZg9gLUzBK44Geeze23fX49ao1XjNR8LavxHB9cDGwkTMxd11+pEeYT31UeNdLmNZWYRFhyuHDhj3mOkA5e7ipt/Z3I/SAsNCuzG+V7wYvSDCPc9v4zICfiFxO6FMplgcXnlfX1NbQJkmVA02+Qbq4uD33sfgiuFZ4T7a884cwAC6hrpyuuO6RL76wxHRKuI8+gUWxC2rP9hLB1TTV6pZu1OPRnE2ySLED4kAtvOZ7NjpkOdY6ETnz+Z340fb+M8Lw7y4wLq/uRi5HLpTvCTBO8vX2AXpZvrfDrAjNDPDQCVJJE5bUSBTRlWuT7tE3jhvJ8gYFAqgAVr4M/N371butuzM7rTuo+0w6P7iedjVzm3DjblftVu2/bXctme4sLyowS7JX9Lj3vnutQNLFTAlKjJZPR9F0UrLTlJQHFFoUf1NbUVdOX0tMyBiFFcKhwEb/+H98/nV+Vv5VPn4+NL27PGG6G/dGNNZx9+7J7VYtaS0GrUFtzm7MsB0x5HS9OLJ8dkFBhpwKoI4EUK1SLRMIU59UGRQskvPP8ww9iOAF+IN+AVw/nv8Nvvw+3j5+vQE8dDnjN1u0erDXLe1srq0NLXrt2G9BsYq1Ozme//JFg0pPzrRQoJJgkwjTe5NdEeJN4wngxdoDaMEtQGOAroBmP8w+V/twttdyZ66/649r/6w2bSPvgDOOun1Bj8g3jErQG5Hf0yKTYxNezy6KiMbyRSSEVEQ6wqTBsL7Q+hUzzm7sKvDrEexU7tzzGbo1AsvKr86hkUCSVpKd0qiN5gm+Rj/E0ASLhDxA0HwedQmuyepqqqGsPy/ANueAGkj3jU4P4pBpkEkOlQiGRIPDZoJEQq5/iDoIcjLr66lwqvEu63bYgRnKE86+UKEQ7BFVTerIykbZxnMF2sKou0zx6aqrqMlrOnBH+qXFLkv4DnlPlRAUjZoIkYd1RthFb0C2d3It3qiz6PItIrVzAZZKI04JD3XPho/JS84KpkpFB+6BqXaP7Ldnj+nncCb8LEe3DXAN/o4wz7kNCgrPCmWIOsCF9bHsLCcOKFkuVfnRxUvK2UzVDS+PgA8wDYOOHMspwxj2mewbp42rCXNn/8rIQIylDJTNKNFhTxVOLs0hh828nG+76EXm5iw+NvjC6IkhiqRKRs2zkN8Qk9BtDOEDo3WR64lncag1sLC8IUW3CQ8JAQtqEHDRxFKmkKPK8z9ecQApAGYe6Mgyan12w/SFjsWISL9PxZJXUeMPvcgeO/gvbigP5eYoNbE0vALDQgT+xSsH4w7NFSjVD5RnkDXGvTj6rhdpK6cRLRr3qwCIBX2Gkgeciv4RFpeXFwQV4BFriGo5xS7g6T9nGKhesKT5/ECBQkSCl8Q/B8yPnVculv0VwFJdClj+cHH+6zJobWdvqz00A/zQwHdB9IMrBKRJZBAiluKY3VfcFbYPsUUvuirvoSsQKKIn+ejub/r2/bwn/S49Uv03wFzF5Q04lVlZlxmaWAaU2I3OhKR5lDHo7P3qYGk0KPLsULLhtqU5dbl1+Wq5Vjx3QG1G60+Y1sOal5pjmTZWWhAHSHH/afUDroZq8yjn58RnoueGqusvbLPCN1T5VjnEejc7az22wvsIvc/y1QlZKdtQmvbZRNe+03sNEcR0/St3FTH4Lnvshyuha2prSatk7KlwTzM59h/3/ngl9/B4V7mafDQ+wwPECB5N2ZONF+xa0hx0m/cbIJmtl3PUOY6kCU6CT3yvdv4zOvA0rgWst2y+rCTsfKwSbp3wjzOoNez3GbaOtwy3/3bqdyk58zsg/5BEdIqpEI1UTdfUmkub7Vu+2hKZENZTky7Ny4l2QpS91viQdExw3O8gLLEsuOvirE5sKuuyK41ugjFCs3V0HvQWc80z9TPjtNV2trl8vNVA2sf6jZtSK9WZGDiZ8lm9mIRXGROxzufJIoO8vmE5WPWN8anv8a7QLy/vJK7armdtqW0Yb9jxA7H68cEyK3K5NAX2cTlWvjLEe4r6kH1VC9gGWeNaWtinlbhQj8sGhQi+3DliNfZyxPKB8kjzUfKo8SyvBXES8y90PLPYdWg1PHc5uu2ArAdVTdETiNcsGOdZGNbFUrPMBAU6fkX3kPTrM+MzyjPd83SxiC9X8JJx0fIlMiO0bPhPPtGHBs68lDZXg1lFl5lS0EuOQsv7IXd3dQh0zvVadF/x0+++8GvworCQMgN1PbvFBqTPMxSCl+4YFNRgDcMF0T4luP33Rjb29lCzsq7bLo2utW9FcS61nX8MiOgRspYqV8fVgc8HBv1/z/tguwb617kJdONvYe+Yb+zxarbof4WKhBI7lbQXPxMzC7SCwH1Ke0/6c7mINQfu/a0QrX6uQLNdfYMJaNEEVXCWL5FsSLpBqv2WPNx84TpBdB5u6a7scFr1MD8oyhRRzdT+Vb2RfMkrwnO/hn+lvzX6r/MgrVDty3A8ddiA6cuzkUIUGFR/TU2FQsDLP+X+nzu5da4tuSvnLMRxXDnRxhVPN9KLk5WQwwjIRALBk4INP/d5yTGZbD0szvB3d9mDRcz6EXHS3NLqi+sFpQLdwkGCBL1QNO1s7+tVLXpym712CC3Oa5D7EPxPfUghBAAC/EIKv9H5r/D2Kx6rJy4sNJ4/LQkuToXQgBD8z5xJUwYyhNpFPwLiO91zYWyCKvcssrF7OqfF2YwYD+gQAhAxzekJjAfAR+HGd4MCO4LzECzjqg6sI7BKeLkCQwosTaiOq460T6rMgopwSWLJgwiHBWL+MnVxbnlqamsJ7nX0NHyuRLUKBYyZzKENOA2IzRNKdMlRyU3IaUWYQPQ5trI47I8pzypx7TSycnoiglzHnssTzHhMj0zkDVyOnQwECwkLGgsuSdxG54EyenUzdy4+atopY6rYLiezWTpHQTHGS8mASwMLsgsJS3+MDk7XTejM9o0MTUCMjMv2SWNFaf+tOV2zHm4vK1Np3um3a6evBTOkOQ+++gORRrWIZol7yRoI8olxSeoLjA4BjpyNyo3xDhKOJs29TGMKnEdQQs19uXe0MrEu9+vYakXpKuhWaiYsTO/5M+i49L1vAMUERsbjyA1Il0hdx8bH3kgJyZZLV81Fj8iRNVDoEVDReJE1kNgQSA8YzQdKU8beAdb9gLjdtFYwdW0zqz2p+CjRqETn6Olsa2quFjHF9b/4nTx6f6GCDwRchWlGVIbARxEHsUd1SC/JgErbDPDOpNFRkzjSqFLHUvCSQZKMkmORd4/uDQrKcEZ+wZ483Le8MvxvaWxrqq2pV2hf58gnpikLa4guxnMktt26rP5lQGLCawPyBP6FVEWDBjxFzcc+iSwLVw5uUVyUg9TzVINVLdS3k+mSxREVDgiJ00SB/vT4E7KirlzrDikzJ4jnNWam6IpslnEStgN7tn9HglMDv0QKhOCEV0TlBhRH2UtGUFWUqBWmFUgVExR3ku7QYouoRL49UDaO8EYsW+m/p80nY6hRLQry+rf2vQ9AgYKNwtOCx4M7BJYIGM3+U0EXCpbUVgpUu1E8StVCYbkT8UbsXGjop5DnL+js70w2/rwnv3AAYECGwP2DBkfOjpKVhhiYl+5WMBKJS8zBNnXrbdpptSf457ypNHBw95m9Ef+EwCuAK8LvSMqRF5gCWefYNVRaTQKClDbiLlCqMWiDKJ7tKDTh+lt7+Dw//RrCKUroVB6a1RopFyEQskSsN7ZuiyqH6a2po+979pR6wvrs+7J/U8gH0zMaG5qVF6LP6cNCNnBuj6uUqscq/jKZd4W30vgIOxfDf89U2NxbEFfcj9fCnDXq71ftTe1RbZx0Rbbftqg4DD6FCc0VodvgmY0SVgXOOBdwUS4pLhNs8PIe9UH1hzclPVnJyVXN2+hZTBG9gsD2Om+bbneudCuBMQFyn7NKN1fBCA6imGdb2ZhrTg1AFPUjcPywfi8grXSwHW+XcHf2LkNw0PxZLNp5VBqHQvqbM5GxhHFv72bvFDC/MVU1ZH6SzOjXMFtJGoKShMZVexB2TPUP9Awwsq+1r3qv5fMpPIAK7lXa2rQZQtHzhaY8A/h4OAt3KXKzrqevrvAdM2s7wYl5E60ZFxsclWyK2j+tOaw3wTd1NgAxNq0RrjHutbKW+9lJfRNX2CrZu5Way6zAmbpi+LE4+Hfe868s5awQrPNuuHQCPmTJndKgl0JZIRYojgtGH39bfR08B/x5uC2x8+swq3krZe4D89n+i0jm0UEVy5eF10hRQskOwmm97XzMfVE87PlpMwUr5yqZqtgtFLFxOEHBTgp20PYU4tZ71uMRkAsaRVFCisHAAvZCiECzO7h12684q1IrnSx7Lh5yiDorQzVKRRBk06cVOVWVVOgQHApeRbcDMMFnQCc/lL5PfD+3ffIlrNOqQ+qMq97uk3J0dou+sEZKjITQ95MqFHKT09QK0sfOFMmDRsOEsYPrw3rD50NnQbS+SLmic8NvAqtPazSrl+1Br33yunekfpjFPgr9DolQmtIr0t5TJ9NcUosPNUqhBvOE6wMBwukC1oJawZaAC/23uqV2Y7GBbaRqnGo8qxEsla6Y8Yb267y+gi8IOoyWjugQvZGU0ZhRCVDOUaeOywrOh8dE80KKwlmCHkHDwQGAbH8QvEa40vVt8hPu9+vRq5RsGW2nL7+ysHcYvIoCDweVi+LOQ0/GETDRSNGEkXpRsY6RCx0JfMgFh3sHDUaSRdOEXsF3fjY49rNcLzwsE+peKs/sMC3ksTM2FTzhQofIGAw9jiQPbBB0kFQQpBDejlTLQ4ihB89IOwd5B1dEo8Fiu9z2NHF8LPQqLGqubAPvAnOveaFAdIUnSPSLI4tkS44Mi01TTJDJzklIiclI/khzxUGA1DrZc0GubCrSKeMr767AtRL8ckNYySDMHozijBXL181zS+8KNglYyUuH8EUMwGV5YLHFrSxp3mpTLS4yrLrFAsSII8qQiyrLkkxijqIM8sx+TEKLFIckP4K2ee6wapvp1m0v8788XEQAyEIKPssAzDKPFA5gDgdOQ407SIbAevTZbZDqYqvlstW80sUhSYWKEUq1S88PtE9DjhbMxomFQEP1bez+KY1qJS+Q+aXDNodWyD0IvQt1EGlP6k+XjR5Fm3oqMFHrV2lLbsP4rIHUB5ZJIgllzE7R/RFKUTiNisUZOHzu1qrO6y/zHH0EQxNF0AWtB+cOlxEwURuOMIaZuejuoGqWagsxxHynwgzD4EPEB70PgVLykiqOtAR1du/tv2p96wW0fT13wWECt4SZyt/T/tR6UsbM5IBH8o1rGemdbmC4cP+EgS4Bg8Xlj3xV+hTFkMkGNzavLdxq+mpTs9H9BYAmPtwCDEoe1C8WSFQMDIZ+QzH+bChqou1xdjb7132qvf/CkY1XFsEWlJKbyEj5jy7+61Hrcq8reGT8m72m/6AF0VF9GJNYOdPDiVS6Pq7Bq/NrNiyxdKR5JLlzundA0QyUlgcZPZY0DjcAgjSNblMsdqxk8A810req97j51sGdzhpXFFmqFrZPL4Gv9juvbG1ULJQshLMm9fY2Bre3+/uFm1FBmRnaTtcPj7cDGLf4sMjugy6FLnExOHR9NQF2VXi2v6FKXRQnWZ7al5cJD4QDSvieceNvaC7ibvBtpPHDNCz0tDU+97Z+LAg50e+YFBuPmjeU4wzxAfl4ibMHcNzw2XBk7vHuSPCKsfsx6HLrNgY8akXnjxwWJppRG9dZFlPQi4OCLLnKNWczTfLqckHyOS/mbmSvzrBBMHmwpzK9dp58vcYbTpeVrBmKW8XazhcV0bDJdUBYehd1VzKBMYHySrKJ8exwRSy+7fquju8+rz3vlLFjdTO6jYMJyxdR4NbpGhub61wOGaIVs0+1yNcCFHvCt9S1T3QE85Uz/rNq8jqv+60pq/Vshu1FLjAtU64Wr+Jy9HdQfc4FNItakJZVEVhUmlgbsBp1VzeSUI1Wx7bDBz6feqe4Vbdj92e3qjdVtrO0TPHpbvprVuwWbHcsIOwkbKbtUK9bcki26HzSxBYLCxEI1VWYcJoPmxlbTVjRVRNQQMqlRIu//3uDepm5dvhauFE4FHfMttb0aDGHLXFrQmxq69FsSKx8rV+v5DNneHT9xwSlC8jRZJWr2CFZvhpAGhUXPpLtzTdHaYJ9frP8NbrDezH6MDl0+IW2GjJMbf/qtaqAas6rQyxBLgFyPje4v7uH6c8X085XLFilGV7YuBQVTgQHowE+vQh6qLouulg6K7jzNkwyMq1zKk6qyCtZ7K8vUjRcO1tEhIy9khXVhBePmGSWlxEvyiMDUL9yPdi+Kb39vIJ58DSZLphqhCpWqumslPED+LICJ8taUarVIhaB12tSi0uQRT3BHL9fv3H+rbzmNwqwKOnrafRq4C60tMW/Vwmf0VOVaha/FvZSrgszRQ5B3ED2P/B9J3g9MJ2pymmlqtTvCbg0Q6GNpxLMFPwVYlL8y0TFesICQj8BCb1R9j1tfikvqlTuObXkgeGMFtHjE8CUX5FlSdOFDEPQA7tBCvrxMURqAanZLGhyTT3RySPP3VJCUvsPyYiOxQbFE0QvP/x3vC3BKP9qZe7NuEMFGM2I0TJRXhGWCyzG5ca9BpdCpzkeb31pCWrK77w5U8VxTHvPLM/JEBaJ6sceh0SGWkDpdvptIylRLBSzH79ZiZBOfI7zTveNeIlzyCkHvkOae0kwwSqRKm9vP7kfhXGMlw6pThSPPouAihBJ68gYwdt2Zq1qKY6tBfV4gURKQw1XjXSNtc8RjCBLgUpSxbg8B3G+6zCpxe4QN14CtElaizZLS001TeoM5Azry1UFlTtfsXIrQSqN76t4psMaCQnK0croS7+Oz41UTTGMB4fT/sV0Ni0dap9swbPofZ4FLYhOyQoJz8w7TyYOmY71DUvJCT+8dNluSytP7ARyO/sSQ2hHVAj2iJRKAw1rj5JPz89fDM9HWT3d9C7to2sq6pEvdPa/PtvEQwZqBqzGkckrzaqPXs/Mj6kMxcfafzY2K+91rCAqnGtPcBz3I31YgZ/C94NwA22FxkskED9QbZCZz/1NZcg4wJj4KnDe7VlrSusuLdJzyLroADsDbISQhG/FGsWPyO+NS9JAkjJR4tFBj73LRwSj/Xq1zbC47Vjr4ms2bFBwnjad+1L+d0AcgXcBjkIbg9QHCEqVD2HTd1NN038SIhDLzYPIZgISOit0k7BubbUsQOuV62ztLPDpNhz50L0Svuk/G8ApwJXAgMJ0BCKIEIyS0NxUmZQQ07ITPJJgENdNPsc4ARk7f/Y0MYFvMqyTK5erYKt9LB1vdvNSuCT6/n1wvuj/9AAEgO2AwsHWgxPG5Ao0jkySP9VEFY0VSpSLk08RPE26iXIEb33hOE7zVLAIrdTryCu5qtUqSephrZXxs3TVeRE9DH5w/v0//3/fAATAqkIfxMyHS0vBUBvT4hbb1q6WcBWf07sRSI1ux74BJTnO9FnwMK0drC9qwus36xxrE25U8vY2nXpzPCr82jzCvPE9W/73gM2EDUghTTHS69Zh16xW59VUE1dQeEtBxNL83HaEclIv9y4r7KMr4mvx7QlyAba8uJ75Obn2emi6yzxUf2vElcruUhFXZFlPGNmXiFTGz8LHdP34tUfwYixP601qjeq366zxTjYhN8j4bnlXeTU6yr8LBTgNXNTe2b/Yf1YVEeRJzUFXuPgyYC82rdjus+6wMv+5hPtQ+8v7F7uJfoWFSs6s1gda1Fo/1t8Q9Ecn/Yl1enBbbpptj+2BsW32fbcpNk321fsXws4Odlbnm6oZWZU5Cup+0/Rl8CHufW3lrOpwdrPGNZy12rlKAK8LmlYFW7AaYBUfCdc9AjNa8ELwwbBmr5X0J/UitTf3fwBKzIhWZ9uK2VRRpEQy97TxI6+EL+3t0y9bMUtxN/QgvYpMfZcI3CsZd5BfgOI2VHGesFLvpivZLsovuLCJt2eEQFH3WfWZn1BPQbZ11XJnsq9xYmyErdjufHDl+SSI4lViGyHZbg5GgIU4djUoNPZw3CyZLdmugbSxASvPbVjD22zSRcNK9q3zI/OY8e6qrGpCa9bwDz1eDx3YU1tKViQJ6r4w+SA5sfZO7cSrf2up7zx6tIuD1kYaPBYNij3/AXtK+x44uHCEa1Yroy8JempKc1TAGWzW4wqLvvG6n7qEeH3wdOkF6jNtRbbJxvsTYBhxWbkQ8wUDfiO9+rwzdqysOGjKKg2v6H30TUsVZZfrFckKksFQvsy/FLsMszdo0qh8KhqxkEAJDjkUpRcqlVNLA0LeQTMBSL+kdvxsUOgk6U9v+nw8yX2SB9WQlgCQDYeLw5mC+EJnPkh04Kor52Ppz/BTvIBJxFHSVNKVKtM0StuEz8PDQ6JA/jmd72InyeexKhcxVT2qCgjRT9QUFJHUjA2OSD9GVcbRhdhAq/csLTknGaem6s5yHPxnh1zPTBK704wUNVFrTFuJ5UlGSRAGbgD7eDeuZ2i2p0Zp8W8zeA0DmwxmULHSKxKe0liR2A0yyvGJ6UnpCQrGQD9p9q7u3amZ58Rp/a11tF39kEZtC6LN7o59zqvP0tEMTSMKjcpxyntJlQcEg1V7UnPcbUEpD2cQ6IFryjFM+K6BhcdUy1/N3E5VzqAOqw/4DjQLN4qaypCKUwm+xyoDfT13NobwCqtMaP1oPmoVrZwynTmxgPzHBYuWzRsOOM1ljaOOYw+bzuDNU0zFTMEMnMwKiZdFxMCPOhizIi5r6vYo2Cjp6wSus7O5OYPAsISSCFOKKIsXSr0K/QtCTSlO8k1bjINM1czNTTLLRgiVBL0+xfhi8oIuWCt9KVFqFyyUcAe1hDwYgZZFosjRivPK2UsCS33MQY5xECtPu458jjQN+Uy9itQGssC0OSLyeK3ba0ep1en8bCjwULZ4vQ7CRwbASDNISoiNiMpJhgwEj0SPHg6Izt7Ossz5CXxEpb1Ztg6wUizwqxCq8O4gM2A5tf/NRMMHH8fjR2XHVYiaS8BQRM/jD3oPLg3yCmWEavxL9OeuzqwbKq7roG/lNnr9WYKsxLZFVAWKxztJjA6UkTGQ6RC7TqiK0IOvubLx361Y67aq+C87dnJ9sUHkRCJEV8VVBtkMLRGHkY5RJY9RSuGCZvehcHTsQKthLPHzfTqEwAXB8QHeA1/HH42uUnLSTxEPTSiEvDmv8TDtLSumbao0p3yIAFpBSIGlA9bKBpHzEtqSD44fhfm6I3GbLfdspm+S+Hw+ukElQR/CNEeCj/1T+NLajsDF6rjQsEDtMiw27sK3IHwMPaY90AIACoaTNlRFknILsj9YNLcvCy2ZrnV2RbyhvUR968HaSodTn1U7kjqJ8nw7ckButa37MGp3/PtpOzz9akT+T5IWexSwjwWDf3ansS5v0+/79xc7Trv1/LSDX86Mll2Vo5BwhBB3OjC8b5uvjvSD+EK5J3n4gTtNUBYbVdgPucK9deLwme/m76gzsDXR9nJ5OoHBD1pW2VaSj9SCK/a18iKxH/Cws311krWpOOmDuBBI15MXBM+rgaH2TLKQsnBw1fHmc2Oz3vdiwpVQNlcwF5QRHIS1+TL0ZfQtstkwzPNbs5S2Gf8OzImV59lZ1DVICXtDdMzz+rOMMBlv+/BSsdQ3UoPgEO0XTFkM02jHUnwrNr02HPUO8PevLG+asG013sEKTymXPtnxln1M04GvOzs5YvmSdq5vQi517mUwEDauQ3IPuhapGWrWfI2eg0Q8+fpa+Vr3XfGPLF7sGayU7+34b4WuUQMXHllilkAOb4QBPVa64LqLuYR1fe2DKvNqmKvIsIe5x8Zs0MlW2ZkcGTJSd0naAgL+eDyXvCS6FbW4LnbpuClJatltlrU5gGFLWVNn11bY3xlQ01FLisPN/0r+2v6K/WS6KzSTLZCoDyh0qM0rAi/LuJiDOIyIUwSWl1fymCDVOY5OyCODa0FtgOtACT+UPQf4VjHC6wEnkuc/Z76puG2ZNOj9qYX/jPbSEVUzVeBWVlV7zulJOYTGgpqAS8CzQPXABH2VOaszk62q6APmBeZ/Z1sp/+4LtCe74YRmi+jQcJPb1dLW95Z5lmRUM45BSf6GIgPxgrWCiMOCQyzA8b5F+jJ0eW9zKikmTiZy5sUovOuOMC219X0khPMKnY9NU3wVK9VZ1YIV11SXkjZM/kgcxXADp0O7A67DzQRGAqfAHDxZNfpwUysF5z+kf6TvpjMoLWtpcMO3W38Xhn1MaNFVE4+VG9WgVZUVhNYJ0ecNHIiZxtgGBgWrxZMFoMKRQAl74DZQsCpqD6a+I/4kYiZ46aHuFzScvHfEpYq3TsHSPVMbkpkSupM0k5FPH8uBCgIJfcj3CUeIlwSUf/U5gzNGbMTnxuSzpMOmn2pYMLL4lADBx1wM3NDi0rDTr1PJ1FZSuM8SzdkMzsvfioeIf0J/OtmypeuJZyLkxeaeqfRvlrkPQu6KjM6lD+tQDFBVUdMSRI+zDtqOKQ1gipmFYLyKso8qiaY+JFInJKwwNfV/Lwa2Sx3NMk0yDr0RVBBizsEPYg7RTAqFA/sZb8noveVKZwRsCrXsACEHqgt4TEVMow3qEHrO7I4DTXTJpMIi91Qt8Khc5j2pgTK/PVPFxYkqiYoJ3Ax/kJQQAo9CDa1Id37+sqGqxeeJ6TPwkbvDRVIJHsnISvYO7NN9khXRuU8HiMR7/u/5qcpnvW0Z+NAC78ZVBd+G0Qtuki/TDNL7jzuE3ndJbYZpjCmh8WT6rEGDg2fEDYfVz+8SZVLvj2SGzXhirdRp5ajYsC76pcAbglZC/ccUj4rTAJKszewD9PWqrTGqXSsidB49WIHWwsyFW4tak55UPBJ4DBU/cPMS7RVrDjA4+WS/l0BgwSeGKY3X1BvTR46sRAA2iy7a6+RraPOIO0W9Kf02/+6If5JzU+4RoEpy/L/yIq6y7ZGxjji++6T73v0aA6gNzNSiU64OS8Rc9/swwi8W7oj0tfnh+y48HD85SBuRMxTlUsmMl0DRtmOxBu/h79g1QTmh+wG7rv71hwYQIxTtEtDNU8MHt9zyXfDOcPayzjdI+EV4gzoswLRKn1JTlElRHEoGf3/2xPPTMzuyx3Q/ts83pnd2ub//rYmD0UQUqBIni+PC67lV9GOy8bJxMgR0XHb7N/R317ssAhVLXxHaVNZTp08fBuO+77ldNsA2J/WR85U1I/Z5dxg3g/rJQfeKQVDMVAtUeVEnipRCXvsy9oa1zHY69YC0YTSC9VC177XZuOF9scTuTDvQrNMHU3pPPkkQwUh7WDg5Np6233aBtbvzDXOH9CtzxHQE9if6mwK9ia5OxNIiE60TDtAAi1kE1P58equ4+Tj9OLS3ZvU+8ecxqnIJMlizR3S7uCX9iIToCv1O0FGsksjSos8FiowE8P+fvMx65LpLOkJ6zHqyeCa1XPLjMpHy9XLiM9Q2C7q1P5DFesqIDuVRUlL5U5dSoY8qir2F4gGivw/9z30YvRp9JXwHut935bQWsZoxuzGDcd6yifTpOEt9N0LPyQzNOg+A0abSb9KYkDDMVgfQA0O/u3yAe9K7abv8OwB6K/fRtKaxVq+lL3Lvsa/AsSJzTHa4vI6DGIjSzaXQJNH1EoXSyZDGTE5IDgOjAGK+0b4GPqw+qr4vvC94obQkMDturG7nrxWwhPLLty08mgQMCiTOCZDkEj5StxLF0BQLxkd4g5QB3UDBgUIApj7Ye+m3nvKG7nettq3jbvsxaLZCvR+ECMqEzsfREhI8ElgQiYvqR04EW4JBwbrBkcCzfTz3s7JN7detFa3LsBEz2npbws2KoU6REL/RGlGgDg5IWwPXQaQA0EAxvi46JfRr7uurkexc7jdx6zlRQtyKVg5BkEiQzBDdy48HYgVghUvFK8JqvM81Ky53bEFufvJ+emzEZ4wjD5dQVdBUzuaJVMZ2xX3EwcHde7uzQ20m60GtZbJbe0tFk8vzDdROXw8SylxHpocXxpfCdDpVMSCrmGuCrut204HGSdRNLk1Rjd0KK0eqx3FGNsG4t5WvCOrwLPNzZP6diHfMJ8ydDVAMSQm6iVZICsJU95Tu52qBLaI1vcDvSERLmUu3jWOLkwrTyqqGLPyqMYQr1yykc4F/N4dkidKJ0swRy/0LNEq6hTd6XPABa6+t+rZugaFH+kipCWeNb8wZy8fJLj/c8+ls7WtO8Ve8e0U/R4sIEQuPjkKOKswsROi3t66Ea8qwIXqdg6vGZAbrSfoPEo7jzVrGb7l4b+xssi+Q+f3ClgWyhacJNw+wz0mN7MbP+mWwZ20tr2P4ywEHg4zDywe1DvvPsg3kRs857zCGbb4upLeTv6qB88JnRnEOIZCPT0dJaj03cq2u9+4qtdR+p0GigfaEt0ubEVZQawwhAg42MLB+ryPyinr4PpF/ooDEBthPQtGjD2SI+H2KdJPxODB69QE77n50vmAAm8dFD5CRac8RSLM9QXSX8QNw/jNhOac8GDx4PYEDosxrEaSQbwvbQkk4a7LhcYGxvzTZuUU6Wfpv/M6DscvF0UdQKQvEA6C6BzTest5y9HO++EF6m3q0e6m/yYdFzkvRtY/yy48DvDsmNcA0G/PIs7Q2xjmOuig6oTy3Qk/KMU9xUWJPnssfQ9p8LHdlNcS14/VZNby4LrjF+Mp5/z1Tw3PKNE7n0M6POYt9BKL9j/iFNqB1ojVmNLy1EDap9ty2/XfL+ytA3seOzPqPQ8/hjZEJDgNdfcG6XLhR94c34Dc5NRZ3FneJ97Q35PnOvYdC3wgpzH7OzFAjznXLDkZ3wLY8ETo+ONA4RLfj925143XJdqz25ncvODT6pb6bg16IfwvaTgpPLU49S5VHr4KTPqP7THm6+Of5NfjUeEa223WT9is1zjZ9NvW4ALrWflmDgogJSyUNBQ4Uza1Lp0iSBNYBJL4tfKr7z3v4e0y6mbkYdyo1qTWm9Zb1wXaD+BT6yz7eA1yHpwq1TDqM7IzaCuoHswPcwHG9pTwse9n8GLxpu8O63PiytiD2ZzZLdlc2yvgy+qX+sIMmRwgKPktczEyMiIqzh2uDswBfPg/9Drz1fJH85Lw7+lI387XG9gv2EXaL9+/50v3YAhMGjclECzGLpEvHycGHGcOrQQZ/YX8jft7+v32i+4f4jLZ8di02WbcH+N08DwDHBb2IhYpwSuwLBQkkBaeCMj/svw9+y375Pdw7+HjIdlE2hvbk9+m6iT85A+KH8smgSmxKZAjzRTlCFgAMv+9/uj9N/bp6FDbn9c42QLfCOuw/XAROR+6JUkozideGoUNHAY+BcoGzwM9+ELnIdk82AHcUuU99/kM1RvuI3MlIyafGGMMhAbaBawDdPy/7sPe89Yp23Tknvb0DS8d7CM6JOMj/haLC3cJJwovBYz3NuTj1sXYneBd8iIJphliIB0hXSC0ExwMsQtPC4wCRe/Z3JjWZ9xb6vgBwRSjHHgdhx+pFWgOFA0CC2X/C+zf2nHWjdwJ7rwHLRjsHCQesR6tFacTvBH3CVf2huDA1VHar+hNAacUrhvTHPwenBcwFnoUdAzB+DjhAtah2SvoZAC0D70TnBSrGHMSIBH2DvQELO+k3DTVNtzg7dQGwxMbFlUWMhuaFh4WNxDyAOHm7NjJ1lricvmcCzARXhKlFRcYOxajEygI8O/93bHWN9vY7JEDERCgE00UIB2IHF0bGBddBHfrA9yz1nDiuvf2CPgOTQ6LFNceMR5LHRcUWf9Y5j7b2tmC6fX9iQhNCTULbxLJHcYcHBo5DZ/1i+Hg2fHYL+jy+8wGuQcjCRoRKCAnH9EcjBFD/IfmO9zc2KfgCvG3/+oDUAN7B+YVQh9fHz0bZQ6e9mXj9Nvo2fnjd/VWAKYDKAJ4B48VGyKsIbAdzBKu/aLoLt9b3JHenOyE+YD/gf3y/hAG0RX2IashkR1iEi39f+sk4eTdPd3l55f0LPxj/Yj+YQOTDSgcXCSGItAdQxFs/g7sN+GG3b/cVd+J6vDyiPeJ9i35vwCnDcUbhiX9I48g8RZRCHb01ueL4G/enN6r4l3slfOB96H4jfiN/9cI0BVLINkl6yQ0IGgYUQsF+i/uFeY/43/ikuLd56TuzPPL9Gv0ofRK+O3+OwptF7Qh3CQ3I9YexRcODS0BsvQR7NbmvuQg5HbkmOiX7znzjPXD9a32zPmU/6QHExEwGnQhfyX2I/kgQxvnEY4GX/mS8MPqJOhG5+jmm+Ya5U/pke/F8FryM/IE8131e/pXAl4L7BM5HQki4SQaIzQgMRrnETsHqPwR8jfswOgo55bmf+fK5pnlGegt7HXuv+7L7ibvtO4b8jL4EgB2CfwSDRtYIBQjyyEIH9MZrhEGCfj+g/bh8IntxOtC69jqB+v96Ufojux+7nPvJ/Aa8PXxWfXb+FoA6wjiEUoZTB6xIQ0iXB90G9kUJAuKAIX4h/J37rXtFO4s7tPt+OwI67vtVvBs8KzvO/AR8k72Xf1+Bl4QQBdYHGkfqh/RHNAXHhCIBl79MPbf8tvwr/FJ8Uzw/O3965ntwu5a7pnvqPAl9VD8fwZrEKAXEhwEHl4bxxWeDcUDhvqL9Rvzw/L+8nLyv/AL7R3ve+/E7v/vCvN3+mMEVA63FekZ0xv1F70QNAbE/ND2VvQs9AX0pfKj7o3sl+1H7vnvz/M0/MoHkhFjFyoaaBkrE8wJLAB0+hj4mvfK9+L0RO+Y7gjvRvDx83j8AQlQEjkXKBneFpAOiwQR/Uv5g/h5+PP0au7e7FftBe/c9GIA5wylE9wWuBa0D/oF0v62+y/7nvny9KztRe0N7vfx+vpdCGkR8BTaFVYP4QUh/8P8yfzC+rv0+O017nTwOvY2Ak4NwhL2E9QQswfVADP+1P58/Iv1Ge6u7g7xsPgCBYcOPhLgErUNGwWxAM7/jf5e+Q/x/e2J7yT1/v9vCpoP8xDTDc8FdwH5AJj/gPos8sLuR/Cm9eEAdwqDDv4OwQqdAzYBugDq/hz4bPA/7zXyMPrDBT4MJQ4hDuQHjQOSAr8Byfw59E7v0vBU9roAlQmfDPUM1grVBYgEhwSSAGT4bvEA8bb0ov39Bj8LrgsRDBYHeAQbBOAB2/qE86fwCPOj+aQDbgmbCrAKhQioBVMFKQQF/872uPFH8nT2cv9xBkUJZQkUCmYH4QZmBs0Djvx89SPyYPQb+i0CzAYZCBYI4AhkBmcGugUdAqP67fTg8ln1X/uOAkIGCgf1Bp4I2garBgIGkQIP/Ej22PNQ9RP6qwDYBAQGHQYhB7QHEwecBvwEKQAR+uX1aPSU9j/7xAALBKkE4gTrBSoH4Qa3BlYFegHO+433vvVl9vP5IP/5AkYEOgSDBFQGKwcMB78GqwSVAJD7Gvid9r72dPnM/VsBcgPiA/QD9wT/BjAHMwenBqMEtQAB/Pr4ofdk93/5z/zh/8ABHwIvArMCeQSbBokGSgacBccDUACG/N35kPgj+DP5/fvQ/t4AwgHlAQQCtwJcBEAGNAYeBnkFEARqATv+c/v6+Vr5KPld+oH8s/5KALQA+wDsAI0BtgKjBL0FtwVsBdAEbQMzAX3+Xfz++mf6N/qj+jb8Cf6A/zQAZABdAHsAGwFVAs4DCgUEBdwEbQSMAwwC7f/q/ZD8vvtp+077vPsb/Xf+e/8IABsAAQAFAHUAOgFWApcDPQQzBPcDhgOUAjYBiv8S/gT9mvxP/Ej8TfxW/VP+Hv+a/9L/wv/V/w0AoQBbAWcCQgNxA1gDIgOlAt0BwgCE/3r+yf13/Vf9Uf19/SP+1/5m/7j/3f/l/wQAPwC+AHABEwKEAnICQwL7AXkBygDr/y//qf5t/l7+Xv5n/sf+Mf92/5//qP+j/77/AABnAOAAQAF7AWMBQgEJAacAMwDB/3j/WP9R/1j/Xf+J/8H/3f/g/+H/6f/8/x4ARQBjAG4AYwBSADoAIAAMAAQABwA=`}],Dn={head_burst:`decapitation`,eye_left:`jaw_dislocation`,eye_right:`jaw_dislocation`,ribs_left:`shoulder_left`,ribs_right:`shoulder_right`},On={duration:1.1,first:[330,420],second:[760,880],gain:.16},kn={duration:1.2,first:[520,360],second:[980,720],gain:.13},An={duration:1.3,first:[300,280],second:[640,600],gain:.11,delay:.75},jn=2.5,Mn=20,Nn=.05,Pn=2e4,Fn=650,In=[`pointerdown`,`pointerup`,`touchend`,`click`,`keydown`],Ln={jab:{from:520,to:1500,duration:.09},straight:{from:420,to:1150,duration:.12},hook:{from:300,to:880,duration:.14},uppercut:{from:240,to:820,duration:.15}};function Rn(e){return Math.round(14+(1-Math.max(0,Math.min(1,e)))*22)}var zn=class{settings;context=null;master=null;noiseBuffer=null;injuryBuffers=new Map;crowdGain=null;unlocked=!1;unlockPromise=null;crowdStarted=!1;destroyed=!1;timers=new Set;lastBreathTick=-300;lastHeartbeatTick=-300;clapperRound=0;murmurGain=null;murmurLevel=0;lastOohAt=-1/0;lastChantAt=-1/0;muffle=null;rockedLevel=0;lastRockedBeat=-300;lastTick=-1;unlockListener=()=>{let e=this.context;e!==null&&e.state!==`running`&&e.resume().catch(()=>void 0),this.unlock().catch(()=>void 0)};visibility=()=>{let e=this.context;if(e===null)return;let t=document.hidden?e.suspend():this.unlocked?e.resume():null;t!==null&&t.catch(()=>void 0)};constructor(e){this.settings=e;for(let e of In)window.addEventListener(e,this.unlockListener);document.addEventListener(`visibilitychange`,this.visibility)}unlock(){return this.destroyed||this.unlocked?Promise.resolve():(this.unlockPromise===null&&(this.unlockPromise=this.performUnlock().finally(()=>{this.unlockPromise=null})),this.unlockPromise)}async performUnlock(){let e=this.context;if(e===null){e=new AudioContext;let t=e.createGain();t.gain.value=Math.min(.8,Math.max(0,this.settings().volume));let n=e.createBiquadFilter();n.type=`lowpass`,n.frequency.value=Pn,n.Q.value=.7,t.connect(n).connect(e.destination),this.context=e,this.master=t,this.muffle=n}if(e.state===`suspended`&&await e.resume(),this.destroyed||this.context!==e)return;let t=await this.decodeInjuryBuffers(e);if(!(this.destroyed||this.context!==e)){this.injuryBuffers=t,this.unlocked=!0;for(let e of In)window.removeEventListener(e,this.unlockListener);this.crowdStarted||(this.crowdStarted=!0,this.crowdBed())}}setVolume(){this.master!==null&&(this.master.gain.value=Math.min(.8,Math.max(0,this.settings().volume)))}event(e){if(this.unlocked)switch(e.kind){case`punch_start`:{let t=Ln[e.detail.split(`:`)[1]??`jab`]??Ln.jab;this.noise({duration:t.duration,frequency:t.from,sweepTo:t.to,gain:.1,type:`bandpass`,q:1.2});break}case`whiff`:this.noise({duration:.11,frequency:850,sweepTo:320,gain:.07,type:`bandpass`,q:1.1});break;case`hit`:case`counter_hit`:{let t=e.detail.endsWith(`body`),n=Math.min(1,e.amount/260),r=Math.min(.36,.14+n*.2)*(e.kind===`counter_hit`?1.25:1);this.impact(r,t,n),(e.kind===`counter_hit`||n>.7)&&this.crowdSwell(.35+n*.4),(e.kind===`counter_hit`&&e.amount>=50||e.amount>=90)&&this.ooh(Math.min(1,e.amount/160));break}case`block`:this.noise({duration:.045,frequency:1200,sweepTo:600,gain:.14,type:`bandpass`,q:1.1}),this.noise({duration:.09,frequency:260,gain:.08}),this.tone({from:160,to:110,duration:.06,type:`triangle`,gain:.05});break;case`perfect_block`:this.noise({duration:.05,frequency:1800,sweepTo:900,gain:.16,type:`bandpass`,q:1.6}),this.tone({from:280,to:210,duration:.06,type:`triangle`,gain:.06});break;case`guard_break`:this.impact(.3,!1,.9),this.tone({from:230,to:78,duration:.2,type:`sawtooth`,gain:.08}),this.crowdSwell(.5),this.ooh(.6);break;case`stun`:this.tone({from:96,to:74,duration:.22,type:`sine`,gain:.1}),this.crowdSwell(.3);break;case`parry`:this.noise({duration:.06,frequency:2400,sweepTo:1100,gain:.18,type:`bandpass`,q:1.8}),this.tone({from:330,to:180,duration:.09,type:`triangle`,gain:.07}),this.crowdSwell(.35);break;case`body_collapse`:this.tone({from:118,to:52,duration:.42,type:`sine`,gain:.16}),this.noise({duration:.26,frequency:420,sweepTo:180,gain:.05,type:`bandpass`,q:.9}),this.crowdSwell(.6);break;case`eye_shut`:this.crowdSwell(.25);break;case`corner`:this.noise({duration:.22,frequency:2600,sweepTo:1500,gain:.05,type:`bandpass`,q:.8});break;case`knockdown`:this.impact(.34,!1,1),this.tone({from:60,to:28,duration:.42,type:`sine`,gain:.3}),this.noise({duration:.3,frequency:140,gain:.24}),this.crowdRoar(1),this.hush();break;case`bell`:{let t=e.detail===`round_start`?3:1;for(let e=0;e10*r||this.clapperRound===t||(this.clapperRound=t,!this.unlocked))return;this.noise({duration:.05,frequency:2600,gain:.42,type:`bandpass`,q:1.4}),this.noise({duration:.09,frequency:700,gain:.18,type:`bandpass`,q:.8});let i=window.setTimeout(()=>{this.timers.delete(i),this.noise({duration:.05,frequency:2400,gain:.4,type:`bandpass`,q:1.4}),this.noise({duration:.09,frequency:660,gain:.16,type:`bandpass`,q:.8})},120);this.timers.add(i)}chant(){let e=this.context;if(!(!this.unlocked||e===null||e.currentTime-this.lastChantAtthis.murmurLevel;this.murmurLevel=r,n.gain.setTargetAtTime(r*Nn,t.currentTime,i?.3:.8)}rocked(e,t){this.follow(t);let n=this.context,r=this.muffle;if(!this.unlocked||n===null||r===null)return;let i=Math.max(0,Math.min(1,e));if(Math.abs(i-this.rockedLevel)>=.02||i===0&&this.rockedLevel!==0){let e=i>this.rockedLevel;this.rockedLevel=i,r.frequency.setTargetAtTime(Pn*(Fn/Pn)**+i,n.currentTime,e?.05:.9)}i>.15&&t-this.lastRockedBeat>=Rn(i)&&(this.lastRockedBeat=t,this.tone({from:58,to:40,duration:.12,type:`sine`,gain:.09+i*.08}),this.tone({from:50,to:36,duration:.14,type:`sine`,gain:.07+i*.06,delay:.16}))}snapshot(e,t,n,r){if(this.follow(e),!this.unlocked)return;let i=1-t/Math.max(1,n);i>.55&&e-this.lastBreathTick>=75&&(this.lastBreathTick=e,this.noise({duration:.18,frequency:420,gain:.045+i*.04})),(i>.72||r>500)&&this.rockedLevel<=.15&&e-this.lastHeartbeatTick>=24&&(this.lastHeartbeatTick=e,this.tone({from:52,duration:.09,type:`sine`,gain:.055}))}result(e){if(this.openMuffle(),!this.unlocked)return;this.crowdSwell(1),this.tone({from:e.winner_id===null?280:520,duration:.45,type:`triangle`,gain:.18});let t=window.setTimeout(()=>{this.timers.delete(t),this.tone({from:650,duration:.5,type:`triangle`,gain:.14})},160);this.timers.add(t)}injury(e){let t=this.context,n=this.master,r=this.injuryBuffers.get(Dn[e]??e),i=this.settings();if(!this.unlocked||t===null||n===null||r===void 0||i.blood!==`full`||i.reducedMotion)return;let a=t.createBufferSource(),o=t.createGain();a.buffer=r,o.gain.value=.16,a.connect(o).connect(n),a.start()}async decodeInjuryBuffers(e){let t=await Promise.all(En.map(async t=>{let n=atob(t.wav),r=new Uint8Array(n.length);for(let e=0;evoid 0)}},Bn=class extends Error{code;reloadRequired;name=`ClientError`;constructor(e,t=!1){super(e),this.code=e,this.reloadRequired=t}};async function Vn(e,t){if(!e.ok)throw new Bn(e.status===429?`rate_limited`:t);if(!(e.headers.get(`content-type`)??``).toLowerCase().startsWith(`application/json`))throw new Bn(t);try{return pe(await e.text())}catch{throw new Bn(t)}}function Hn(e=window.location.search){let t=new URLSearchParams(e).getAll(`instance_id`);if(t.length!==1||!/^[A-Za-z0-9_-]{1,255}$/u.test(t[0]??``))throw new Bn(`invalid_launch`);return t[0]}function Un(e){if(typeof e==`object`&&e&&`protocol`in e&&e.protocol!==4)throw new Bn(`client_outdated`,!0)}async function Wn(e,t){let n=new URL(`/api/hands/bootstrap`,window.location.origin);try{let r=await Vn(await fetch(n,{method:`POST`,credentials:`same-origin`,cache:`no-store`,headers:{"Content-Type":`application/json`},body:JSON.stringify({instance_id:e}),...t===void 0?{}:{signal:t}}),`bootstrap_failed`);return Un(r),Ze(r)}catch(e){throw e instanceof Bn?e:e instanceof M?new Bn(`invalid_bootstrap`):new Bn(`bootstrap_failed`)}}async function Gn(e,t,n){try{return Qe(await Vn(await fetch(new URL(`/api/hands/token`,window.location.origin),{method:`POST`,credentials:`same-origin`,cache:`no-store`,headers:{"Content-Type":`application/json`},body:JSON.stringify({code:e,state:t}),...n===void 0?{}:{signal:n}}),`token_failed`))}catch(e){throw e instanceof Bn?e:e instanceof M?new Bn(`invalid_token_response`):new Bn(`token_failed`)}}function Kn(e){return e instanceof Bn?e.code:e instanceof M?`protocol_error`:`unexpected_error`}var qn=typeof globalThis<`u`?globalThis:typeof window<`u`?window:typeof global<`u`?global:typeof self<`u`?self:{};function Jn(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,`default`)?e.default:e}var Yn={exports:{}},Xn;function Zn(){return Xn?Yn.exports:(Xn=1,(function(e){var t=Object.prototype.hasOwnProperty,n=`~`;function r(){}Object.create&&(r.prototype=Object.create(null),new r().__proto__||(n=!1));function i(e,t,n){this.fn=e,this.context=t,this.once=n||!1}function a(e,t,r,a,o){if(typeof r!=`function`)throw TypeError(`The listener must be a function`);var s=new i(r,a||e,o),c=n?n+t:t;return e._events[c]?e._events[c].fn?e._events[c]=[e._events[c],s]:e._events[c].push(s):(e._events[c]=s,e._eventsCount++),e}function o(e,t){--e._eventsCount===0?e._events=new r:delete e._events[t]}function s(){this._events=new r,this._eventsCount=0}s.prototype.eventNames=function(){var e=[],r,i;if(this._eventsCount===0)return e;for(i in r=this._events)t.call(r,i)&&e.push(n?i.slice(1):i);return Object.getOwnPropertySymbols?e.concat(Object.getOwnPropertySymbols(r)):e},s.prototype.listeners=function(e){var t=n?n+e:e,r=this._events[t];if(!r)return[];if(r.fn)return[r.fn];for(var i=0,a=r.length,o=Array(a);ie;function t(e){}e.assertIs=t;function n(e){throw Error()}e.assertNever=n,e.arrayToEnum=e=>{let t={};for(let n of e)t[n]=n;return t},e.getValidEnumValues=t=>{let n=e.objectKeys(t).filter(e=>typeof t[t[e]]!=`number`),r={};for(let e of n)r[e]=t[e];return e.objectValues(r)},e.objectValues=t=>e.objectKeys(t).map(function(e){return t[e]}),e.objectKeys=typeof Object.keys==`function`?e=>Object.keys(e):e=>{let t=[];for(let n in e)Object.prototype.hasOwnProperty.call(e,n)&&t.push(n);return t},e.find=(e,t)=>{for(let n of e)if(t(n))return n},e.isInteger=typeof Number.isInteger==`function`?e=>Number.isInteger(e):e=>typeof e==`number`&&isFinite(e)&&Math.floor(e)===e;function r(e,t=` | `){return e.map(e=>typeof e==`string`?`'${e}'`:e).join(t)}e.joinValues=r,e.jsonStringifyReplacer=(e,t)=>typeof t==`bigint`?t.toString():t})($n||={});var er;(function(e){e.mergeShapes=(e,t)=>({...e,...t})})(er||={});var R=$n.arrayToEnum([`string`,`nan`,`number`,`integer`,`float`,`boolean`,`date`,`bigint`,`symbol`,`function`,`undefined`,`null`,`array`,`object`,`unknown`,`promise`,`void`,`never`,`map`,`set`]),tr=e=>{switch(typeof e){case`undefined`:return R.undefined;case`string`:return R.string;case`number`:return isNaN(e)?R.nan:R.number;case`boolean`:return R.boolean;case`function`:return R.function;case`bigint`:return R.bigint;case`symbol`:return R.symbol;case`object`:return Array.isArray(e)?R.array:e===null?R.null:e.then&&typeof e.then==`function`&&e.catch&&typeof e.catch==`function`?R.promise:typeof Map<`u`&&e instanceof Map?R.map:typeof Set<`u`&&e instanceof Set?R.set:typeof Date<`u`&&e instanceof Date?R.date:R.object;default:return R.unknown}},z=$n.arrayToEnum([`invalid_type`,`invalid_literal`,`custom`,`invalid_union`,`invalid_union_discriminator`,`invalid_enum_value`,`unrecognized_keys`,`invalid_arguments`,`invalid_return_type`,`invalid_date`,`invalid_string`,`too_small`,`too_big`,`invalid_intersection_types`,`not_multiple_of`,`not_finite`]),nr=e=>JSON.stringify(e,null,2).replace(/"([^"]+)":/g,`$1:`),rr=class e extends Error{constructor(e){super(),this.issues=[],this.addIssue=e=>{this.issues=[...this.issues,e]},this.addIssues=(e=[])=>{this.issues=[...this.issues,...e]};let t=new.target.prototype;Object.setPrototypeOf?Object.setPrototypeOf(this,t):this.__proto__=t,this.name=`ZodError`,this.issues=e}get errors(){return this.issues}format(e){let t=e||function(e){return e.message},n={_errors:[]},r=e=>{for(let i of e.issues)if(i.code===`invalid_union`)i.unionErrors.map(r);else if(i.code===`invalid_return_type`)r(i.returnTypeError);else if(i.code===`invalid_arguments`)r(i.argumentsError);else if(i.path.length===0)n._errors.push(t(i));else{let e=n,r=0;for(;re.message){let t={},n=[];for(let r of this.issues)r.path.length>0?(t[r.path[0]]=t[r.path[0]]||[],t[r.path[0]].push(e(r))):n.push(e(r));return{formErrors:n,fieldErrors:t}}get formErrors(){return this.flatten()}};rr.create=e=>new rr(e);var ir=(e,t)=>{let n;switch(e.code){case z.invalid_type:n=e.received===R.undefined?`Required`:`Expected ${e.expected}, received ${e.received}`;break;case z.invalid_literal:n=`Invalid literal value, expected ${JSON.stringify(e.expected,$n.jsonStringifyReplacer)}`;break;case z.unrecognized_keys:n=`Unrecognized key(s) in object: ${$n.joinValues(e.keys,`, `)}`;break;case z.invalid_union:n=`Invalid input`;break;case z.invalid_union_discriminator:n=`Invalid discriminator value. Expected ${$n.joinValues(e.options)}`;break;case z.invalid_enum_value:n=`Invalid enum value. Expected ${$n.joinValues(e.options)}, received '${e.received}'`;break;case z.invalid_arguments:n=`Invalid function arguments`;break;case z.invalid_return_type:n=`Invalid function return type`;break;case z.invalid_date:n=`Invalid date`;break;case z.invalid_string:typeof e.validation==`object`?`includes`in e.validation?(n=`Invalid input: must include "${e.validation.includes}"`,typeof e.validation.position==`number`&&(n=`${n} at one or more positions greater than or equal to ${e.validation.position}`)):`startsWith`in e.validation?n=`Invalid input: must start with "${e.validation.startsWith}"`:`endsWith`in e.validation?n=`Invalid input: must end with "${e.validation.endsWith}"`:$n.assertNever(e.validation):n=e.validation===`regex`?`Invalid`:`Invalid ${e.validation}`;break;case z.too_small:n=e.type===`array`?`Array must contain ${e.exact?`exactly`:e.inclusive?`at least`:`more than`} ${e.minimum} element(s)`:e.type===`string`?`String must contain ${e.exact?`exactly`:e.inclusive?`at least`:`over`} ${e.minimum} character(s)`:e.type===`number`?`Number must be ${e.exact?`exactly equal to `:e.inclusive?`greater than or equal to `:`greater than `}${e.minimum}`:e.type===`date`?`Date must be ${e.exact?`exactly equal to `:e.inclusive?`greater than or equal to `:`greater than `}${new Date(Number(e.minimum))}`:`Invalid input`;break;case z.too_big:n=e.type===`array`?`Array must contain ${e.exact?`exactly`:e.inclusive?`at most`:`less than`} ${e.maximum} element(s)`:e.type===`string`?`String must contain ${e.exact?`exactly`:e.inclusive?`at most`:`under`} ${e.maximum} character(s)`:e.type===`number`?`Number must be ${e.exact?`exactly`:e.inclusive?`less than or equal to`:`less than`} ${e.maximum}`:e.type===`bigint`?`BigInt must be ${e.exact?`exactly`:e.inclusive?`less than or equal to`:`less than`} ${e.maximum}`:e.type===`date`?`Date must be ${e.exact?`exactly`:e.inclusive?`smaller than or equal to`:`smaller than`} ${new Date(Number(e.maximum))}`:`Invalid input`;break;case z.custom:n=`Invalid input`;break;case z.invalid_intersection_types:n=`Intersection results could not be merged`;break;case z.not_multiple_of:n=`Number must be a multiple of ${e.multipleOf}`;break;case z.not_finite:n=`Number must be finite`;break;default:n=t.defaultError,$n.assertNever(e)}return{message:n}},ar=ir;function or(e){ar=e}function sr(){return ar}var cr=e=>{let{data:t,path:n,errorMaps:r,issueData:i}=e,a=[...n,...i.path||[]],o={...i,path:a};if(i.message!==void 0)return{...i,path:a,message:i.message};let s=``,c=r.filter(e=>!!e).slice().reverse();for(let e of c)s=e(o,{data:t,defaultError:s}).message;return{...i,path:a,message:s}},lr=[];function B(e,t){let n=sr(),r=cr({issueData:t,data:e.data,path:e.path,errorMaps:[e.common.contextualErrorMap,e.schemaErrorMap,n,n===ir?void 0:ir].filter(e=>!!e)});e.common.issues.push(r)}var ur=class e{constructor(){this.value=`valid`}dirty(){this.value===`valid`&&(this.value=`dirty`)}abort(){this.value!==`aborted`&&(this.value=`aborted`)}static mergeArray(e,t){let n=[];for(let r of t){if(r.status===`aborted`)return dr;r.status===`dirty`&&e.dirty(),n.push(r.value)}return{status:e.value,value:n}}static async mergeObjectAsync(t,n){let r=[];for(let e of n){let t=await e.key,n=await e.value;r.push({key:t,value:n})}return e.mergeObjectSync(t,r)}static mergeObjectSync(e,t){let n={};for(let r of t){let{key:t,value:i}=r;if(t.status===`aborted`||i.status===`aborted`)return dr;t.status===`dirty`&&e.dirty(),i.status===`dirty`&&e.dirty(),t.value!==`__proto__`&&(i.value!==void 0||r.alwaysSet)&&(n[t.value]=i.value)}return{status:e.value,value:n}}},dr=Object.freeze({status:`aborted`}),fr=e=>({status:`dirty`,value:e}),pr=e=>({status:`valid`,value:e}),mr=e=>e.status===`aborted`,hr=e=>e.status===`dirty`,gr=e=>e.status===`valid`,_r=e=>typeof Promise<`u`&&e instanceof Promise;function vr(e,t,n,r){if(typeof t==`function`?e!==t||!r:!t.has(e))throw TypeError(`Cannot read private member from an object whose class did not declare it`);return t.get(e)}function yr(e,t,n,r,i){if(typeof t==`function`?e!==t||!i:!t.has(e))throw TypeError(`Cannot write private member to an object whose class did not declare it`);return t.set(e,n),n}var br;(function(e){e.errToObj=e=>typeof e==`string`?{message:e}:e||{},e.toString=e=>typeof e==`string`?e:e?.message})(br||={});var xr,Sr,Cr=class{constructor(e,t,n,r){this._cachedPath=[],this.parent=e,this.data=t,this._path=n,this._key=r}get path(){return this._cachedPath.length||(this._key instanceof Array?this._cachedPath.push(...this._path,...this._key):this._cachedPath.push(...this._path,this._key)),this._cachedPath}},wr=(e,t)=>{if(gr(t))return{success:!0,data:t.value};if(!e.common.issues.length)throw Error(`Validation failed but no issues detected.`);return{success:!1,get error(){if(this._error)return this._error;let t=new rr(e.common.issues);return this._error=t,this._error}}};function Tr(e){if(!e)return{};let{errorMap:t,invalid_type_error:n,required_error:r,description:i}=e;if(t&&(n||r))throw Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`);return t?{errorMap:t,description:i}:{errorMap:(t,i)=>{let{message:a}=e;return t.code===`invalid_enum_value`?{message:a??i.defaultError}:i.data===void 0?{message:a??r??i.defaultError}:t.code===`invalid_type`?{message:a??n??i.defaultError}:{message:i.defaultError}},description:i}}var Er=class{constructor(e){this.spa=this.safeParseAsync,this._def=e,this.parse=this.parse.bind(this),this.safeParse=this.safeParse.bind(this),this.parseAsync=this.parseAsync.bind(this),this.safeParseAsync=this.safeParseAsync.bind(this),this.spa=this.spa.bind(this),this.refine=this.refine.bind(this),this.refinement=this.refinement.bind(this),this.superRefine=this.superRefine.bind(this),this.optional=this.optional.bind(this),this.nullable=this.nullable.bind(this),this.nullish=this.nullish.bind(this),this.array=this.array.bind(this),this.promise=this.promise.bind(this),this.or=this.or.bind(this),this.and=this.and.bind(this),this.transform=this.transform.bind(this),this.brand=this.brand.bind(this),this.default=this.default.bind(this),this.catch=this.catch.bind(this),this.describe=this.describe.bind(this),this.pipe=this.pipe.bind(this),this.readonly=this.readonly.bind(this),this.isNullable=this.isNullable.bind(this),this.isOptional=this.isOptional.bind(this)}get description(){return this._def.description}_getType(e){return tr(e.data)}_getOrReturnCtx(e,t){return t||{common:e.parent.common,data:e.data,parsedType:tr(e.data),schemaErrorMap:this._def.errorMap,path:e.path,parent:e.parent}}_processInputParams(e){return{status:new ur,ctx:{common:e.parent.common,data:e.data,parsedType:tr(e.data),schemaErrorMap:this._def.errorMap,path:e.path,parent:e.parent}}}_parseSync(e){let t=this._parse(e);if(_r(t))throw Error(`Synchronous parse encountered promise.`);return t}_parseAsync(e){let t=this._parse(e);return Promise.resolve(t)}parse(e,t){let n=this.safeParse(e,t);if(n.success)return n.data;throw n.error}safeParse(e,t){let n={common:{issues:[],async:t?.async??!1,contextualErrorMap:t?.errorMap},path:t?.path||[],schemaErrorMap:this._def.errorMap,parent:null,data:e,parsedType:tr(e)};return wr(n,this._parseSync({data:e,path:n.path,parent:n}))}async parseAsync(e,t){let n=await this.safeParseAsync(e,t);if(n.success)return n.data;throw n.error}async safeParseAsync(e,t){let n={common:{issues:[],contextualErrorMap:t?.errorMap,async:!0},path:t?.path||[],schemaErrorMap:this._def.errorMap,parent:null,data:e,parsedType:tr(e)},r=this._parse({data:e,path:n.path,parent:n});return wr(n,await(_r(r)?r:Promise.resolve(r)))}refine(e,t){let n=e=>typeof t==`string`||t===void 0?{message:t}:typeof t==`function`?t(e):t;return this._refinement((t,r)=>{let i=e(t),a=()=>r.addIssue({code:z.custom,...n(t)});return typeof Promise<`u`&&i instanceof Promise?i.then(e=>e?!0:(a(),!1)):i?!0:(a(),!1)})}refinement(e,t){return this._refinement((n,r)=>e(n)?!0:(r.addIssue(typeof t==`function`?t(n,r):t),!1))}_refinement(e){return new Ci({schema:this,typeName:Fi.ZodEffects,effect:{type:`refinement`,refinement:e}})}superRefine(e){return this._refinement(e)}optional(){return wi.create(this,this._def)}nullable(){return Ti.create(this,this._def)}nullish(){return this.nullable().optional()}array(){return ii.create(this,this._def)}promise(){return Si.create(this,this._def)}or(e){return si.create([this,e],this._def)}and(e){return di.create(this,e,this._def)}transform(e){return new Ci({...Tr(this._def),schema:this,typeName:Fi.ZodEffects,effect:{type:`transform`,transform:e}})}default(e){let t=typeof e==`function`?e:()=>e;return new Ei({...Tr(this._def),innerType:this,defaultValue:t,typeName:Fi.ZodDefault})}brand(){return new Ai({typeName:Fi.ZodBranded,type:this,...Tr(this._def)})}catch(e){let t=typeof e==`function`?e:()=>e;return new Di({...Tr(this._def),innerType:this,catchValue:t,typeName:Fi.ZodCatch})}describe(e){let t=this.constructor;return new t({...this._def,description:e})}pipe(e){return ji.create(this,e)}readonly(){return Mi.create(this)}isOptional(){return this.safeParse(void 0).success}isNullable(){return this.safeParse(null).success}},Dr=/^c[^\s-]{8,}$/i,Or=/^[0-9a-z]+$/,kr=/^[0-9A-HJKMNP-TV-Z]{26}$/,Ar=/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i,jr=/^[a-z0-9_-]{21}$/i,Mr=/^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/,Nr=/^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i,Pr=`^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`,Fr,Ir=/^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/,Lr=/^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/,Rr=/^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/,zr=`((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`,Br=RegExp(`^${zr}$`);function Vr(e){let t=`([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d`;return e.precision?t=`${t}\\.\\d{${e.precision}}`:e.precision??(t=`${t}(\\.\\d+)?`),t}function Hr(e){return RegExp(`^${Vr(e)}$`)}function Ur(e){let t=`${zr}T${Vr(e)}`,n=[];return n.push(e.local?`Z?`:`Z`),e.offset&&n.push(`([+-]\\d{2}:?\\d{2})`),t=`${t}(${n.join(`|`)})`,RegExp(`^${t}$`)}function Wr(e,t){return!!((t===`v4`||!t)&&Ir.test(e)||(t===`v6`||!t)&&Lr.test(e))}var Gr=class e extends Er{_parse(e){if(this._def.coerce&&(e.data=String(e.data)),this._getType(e)!==R.string){let t=this._getOrReturnCtx(e);return B(t,{code:z.invalid_type,expected:R.string,received:t.parsedType}),dr}let t=new ur,n;for(let r of this._def.checks)if(r.kind===`min`)e.data.lengthr.value&&(n=this._getOrReturnCtx(e,n),B(n,{code:z.too_big,maximum:r.value,type:`string`,inclusive:!0,exact:!1,message:r.message}),t.dirty());else if(r.kind===`length`){let i=e.data.length>r.value,a=e.data.lengthe.test(t),{validation:t,code:z.invalid_string,...br.errToObj(n)})}_addCheck(t){return new e({...this._def,checks:[...this._def.checks,t]})}email(e){return this._addCheck({kind:`email`,...br.errToObj(e)})}url(e){return this._addCheck({kind:`url`,...br.errToObj(e)})}emoji(e){return this._addCheck({kind:`emoji`,...br.errToObj(e)})}uuid(e){return this._addCheck({kind:`uuid`,...br.errToObj(e)})}nanoid(e){return this._addCheck({kind:`nanoid`,...br.errToObj(e)})}cuid(e){return this._addCheck({kind:`cuid`,...br.errToObj(e)})}cuid2(e){return this._addCheck({kind:`cuid2`,...br.errToObj(e)})}ulid(e){return this._addCheck({kind:`ulid`,...br.errToObj(e)})}base64(e){return this._addCheck({kind:`base64`,...br.errToObj(e)})}ip(e){return this._addCheck({kind:`ip`,...br.errToObj(e)})}datetime(e){return typeof e==`string`?this._addCheck({kind:`datetime`,precision:null,offset:!1,local:!1,message:e}):this._addCheck({kind:`datetime`,precision:e?.precision===void 0?null:e?.precision,offset:e?.offset??!1,local:e?.local??!1,...br.errToObj(e?.message)})}date(e){return this._addCheck({kind:`date`,message:e})}time(e){return typeof e==`string`?this._addCheck({kind:`time`,precision:null,message:e}):this._addCheck({kind:`time`,precision:e?.precision===void 0?null:e?.precision,...br.errToObj(e?.message)})}duration(e){return this._addCheck({kind:`duration`,...br.errToObj(e)})}regex(e,t){return this._addCheck({kind:`regex`,regex:e,...br.errToObj(t)})}includes(e,t){return this._addCheck({kind:`includes`,value:e,position:t?.position,...br.errToObj(t?.message)})}startsWith(e,t){return this._addCheck({kind:`startsWith`,value:e,...br.errToObj(t)})}endsWith(e,t){return this._addCheck({kind:`endsWith`,value:e,...br.errToObj(t)})}min(e,t){return this._addCheck({kind:`min`,value:e,...br.errToObj(t)})}max(e,t){return this._addCheck({kind:`max`,value:e,...br.errToObj(t)})}length(e,t){return this._addCheck({kind:`length`,value:e,...br.errToObj(t)})}nonempty(e){return this.min(1,br.errToObj(e))}trim(){return new e({...this._def,checks:[...this._def.checks,{kind:`trim`}]})}toLowerCase(){return new e({...this._def,checks:[...this._def.checks,{kind:`toLowerCase`}]})}toUpperCase(){return new e({...this._def,checks:[...this._def.checks,{kind:`toUpperCase`}]})}get isDatetime(){return!!this._def.checks.find(e=>e.kind===`datetime`)}get isDate(){return!!this._def.checks.find(e=>e.kind===`date`)}get isTime(){return!!this._def.checks.find(e=>e.kind===`time`)}get isDuration(){return!!this._def.checks.find(e=>e.kind===`duration`)}get isEmail(){return!!this._def.checks.find(e=>e.kind===`email`)}get isURL(){return!!this._def.checks.find(e=>e.kind===`url`)}get isEmoji(){return!!this._def.checks.find(e=>e.kind===`emoji`)}get isUUID(){return!!this._def.checks.find(e=>e.kind===`uuid`)}get isNANOID(){return!!this._def.checks.find(e=>e.kind===`nanoid`)}get isCUID(){return!!this._def.checks.find(e=>e.kind===`cuid`)}get isCUID2(){return!!this._def.checks.find(e=>e.kind===`cuid2`)}get isULID(){return!!this._def.checks.find(e=>e.kind===`ulid`)}get isIP(){return!!this._def.checks.find(e=>e.kind===`ip`)}get isBase64(){return!!this._def.checks.find(e=>e.kind===`base64`)}get minLength(){let e=null;for(let t of this._def.checks)t.kind===`min`&&(e===null||t.value>e)&&(e=t.value);return e}get maxLength(){let e=null;for(let t of this._def.checks)t.kind===`max`&&(e===null||t.valuenew Gr({checks:[],typeName:Fi.ZodString,coerce:e?.coerce??!1,...Tr(e)});function Kr(e,t){let n=(e.toString().split(`.`)[1]||``).length,r=(t.toString().split(`.`)[1]||``).length,i=n>r?n:r;return parseInt(e.toFixed(i).replace(`.`,``))%parseInt(t.toFixed(i).replace(`.`,``))/10**i}var qr=class e extends Er{constructor(){super(...arguments),this.min=this.gte,this.max=this.lte,this.step=this.multipleOf}_parse(e){if(this._def.coerce&&(e.data=Number(e.data)),this._getType(e)!==R.number){let t=this._getOrReturnCtx(e);return B(t,{code:z.invalid_type,expected:R.number,received:t.parsedType}),dr}let t,n=new ur;for(let r of this._def.checks)r.kind===`int`?$n.isInteger(e.data)||(t=this._getOrReturnCtx(e,t),B(t,{code:z.invalid_type,expected:`integer`,received:`float`,message:r.message}),n.dirty()):r.kind===`min`?(r.inclusive?e.datar.value:e.data>=r.value)&&(t=this._getOrReturnCtx(e,t),B(t,{code:z.too_big,maximum:r.value,type:`number`,inclusive:r.inclusive,exact:!1,message:r.message}),n.dirty()):r.kind===`multipleOf`?Kr(e.data,r.value)!==0&&(t=this._getOrReturnCtx(e,t),B(t,{code:z.not_multiple_of,multipleOf:r.value,message:r.message}),n.dirty()):r.kind===`finite`?Number.isFinite(e.data)||(t=this._getOrReturnCtx(e,t),B(t,{code:z.not_finite,message:r.message}),n.dirty()):$n.assertNever(r);return{status:n.value,value:e.data}}gte(e,t){return this.setLimit(`min`,e,!0,br.toString(t))}gt(e,t){return this.setLimit(`min`,e,!1,br.toString(t))}lte(e,t){return this.setLimit(`max`,e,!0,br.toString(t))}lt(e,t){return this.setLimit(`max`,e,!1,br.toString(t))}setLimit(t,n,r,i){return new e({...this._def,checks:[...this._def.checks,{kind:t,value:n,inclusive:r,message:br.toString(i)}]})}_addCheck(t){return new e({...this._def,checks:[...this._def.checks,t]})}int(e){return this._addCheck({kind:`int`,message:br.toString(e)})}positive(e){return this._addCheck({kind:`min`,value:0,inclusive:!1,message:br.toString(e)})}negative(e){return this._addCheck({kind:`max`,value:0,inclusive:!1,message:br.toString(e)})}nonpositive(e){return this._addCheck({kind:`max`,value:0,inclusive:!0,message:br.toString(e)})}nonnegative(e){return this._addCheck({kind:`min`,value:0,inclusive:!0,message:br.toString(e)})}multipleOf(e,t){return this._addCheck({kind:`multipleOf`,value:e,message:br.toString(t)})}finite(e){return this._addCheck({kind:`finite`,message:br.toString(e)})}safe(e){return this._addCheck({kind:`min`,inclusive:!0,value:-(2**53-1),message:br.toString(e)})._addCheck({kind:`max`,inclusive:!0,value:2**53-1,message:br.toString(e)})}get minValue(){let e=null;for(let t of this._def.checks)t.kind===`min`&&(e===null||t.value>e)&&(e=t.value);return e}get maxValue(){let e=null;for(let t of this._def.checks)t.kind===`max`&&(e===null||t.valuee.kind===`int`||e.kind===`multipleOf`&&$n.isInteger(e.value))}get isFinite(){let e=null,t=null;for(let n of this._def.checks)if(n.kind===`finite`||n.kind===`int`||n.kind===`multipleOf`)return!0;else n.kind===`min`?(t===null||n.value>t)&&(t=n.value):n.kind===`max`&&(e===null||n.valuenew qr({checks:[],typeName:Fi.ZodNumber,coerce:e?.coerce||!1,...Tr(e)});var Jr=class e extends Er{constructor(){super(...arguments),this.min=this.gte,this.max=this.lte}_parse(e){if(this._def.coerce&&(e.data=BigInt(e.data)),this._getType(e)!==R.bigint){let t=this._getOrReturnCtx(e);return B(t,{code:z.invalid_type,expected:R.bigint,received:t.parsedType}),dr}let t,n=new ur;for(let r of this._def.checks)r.kind===`min`?(r.inclusive?e.datar.value:e.data>=r.value)&&(t=this._getOrReturnCtx(e,t),B(t,{code:z.too_big,type:`bigint`,maximum:r.value,inclusive:r.inclusive,message:r.message}),n.dirty()):r.kind===`multipleOf`?e.data%r.value!==BigInt(0)&&(t=this._getOrReturnCtx(e,t),B(t,{code:z.not_multiple_of,multipleOf:r.value,message:r.message}),n.dirty()):$n.assertNever(r);return{status:n.value,value:e.data}}gte(e,t){return this.setLimit(`min`,e,!0,br.toString(t))}gt(e,t){return this.setLimit(`min`,e,!1,br.toString(t))}lte(e,t){return this.setLimit(`max`,e,!0,br.toString(t))}lt(e,t){return this.setLimit(`max`,e,!1,br.toString(t))}setLimit(t,n,r,i){return new e({...this._def,checks:[...this._def.checks,{kind:t,value:n,inclusive:r,message:br.toString(i)}]})}_addCheck(t){return new e({...this._def,checks:[...this._def.checks,t]})}positive(e){return this._addCheck({kind:`min`,value:BigInt(0),inclusive:!1,message:br.toString(e)})}negative(e){return this._addCheck({kind:`max`,value:BigInt(0),inclusive:!1,message:br.toString(e)})}nonpositive(e){return this._addCheck({kind:`max`,value:BigInt(0),inclusive:!0,message:br.toString(e)})}nonnegative(e){return this._addCheck({kind:`min`,value:BigInt(0),inclusive:!0,message:br.toString(e)})}multipleOf(e,t){return this._addCheck({kind:`multipleOf`,value:e,message:br.toString(t)})}get minValue(){let e=null;for(let t of this._def.checks)t.kind===`min`&&(e===null||t.value>e)&&(e=t.value);return e}get maxValue(){let e=null;for(let t of this._def.checks)t.kind===`max`&&(e===null||t.valuenew Jr({checks:[],typeName:Fi.ZodBigInt,coerce:e?.coerce??!1,...Tr(e)});var Yr=class extends Er{_parse(e){if(this._def.coerce&&(e.data=!!e.data),this._getType(e)!==R.boolean){let t=this._getOrReturnCtx(e);return B(t,{code:z.invalid_type,expected:R.boolean,received:t.parsedType}),dr}return pr(e.data)}};Yr.create=e=>new Yr({typeName:Fi.ZodBoolean,coerce:e?.coerce||!1,...Tr(e)});var Xr=class e extends Er{_parse(e){if(this._def.coerce&&(e.data=new Date(e.data)),this._getType(e)!==R.date){let t=this._getOrReturnCtx(e);return B(t,{code:z.invalid_type,expected:R.date,received:t.parsedType}),dr}if(isNaN(e.data.getTime()))return B(this._getOrReturnCtx(e),{code:z.invalid_date}),dr;let t=new ur,n;for(let r of this._def.checks)r.kind===`min`?e.data.getTime()r.value&&(n=this._getOrReturnCtx(e,n),B(n,{code:z.too_big,message:r.message,inclusive:!0,exact:!1,maximum:r.value,type:`date`}),t.dirty()):$n.assertNever(r);return{status:t.value,value:new Date(e.data.getTime())}}_addCheck(t){return new e({...this._def,checks:[...this._def.checks,t]})}min(e,t){return this._addCheck({kind:`min`,value:e.getTime(),message:br.toString(t)})}max(e,t){return this._addCheck({kind:`max`,value:e.getTime(),message:br.toString(t)})}get minDate(){let e=null;for(let t of this._def.checks)t.kind===`min`&&(e===null||t.value>e)&&(e=t.value);return e==null?null:new Date(e)}get maxDate(){let e=null;for(let t of this._def.checks)t.kind===`max`&&(e===null||t.valuenew Xr({checks:[],coerce:e?.coerce||!1,typeName:Fi.ZodDate,...Tr(e)});var Zr=class extends Er{_parse(e){if(this._getType(e)!==R.symbol){let t=this._getOrReturnCtx(e);return B(t,{code:z.invalid_type,expected:R.symbol,received:t.parsedType}),dr}return pr(e.data)}};Zr.create=e=>new Zr({typeName:Fi.ZodSymbol,...Tr(e)});var Qr=class extends Er{_parse(e){if(this._getType(e)!==R.undefined){let t=this._getOrReturnCtx(e);return B(t,{code:z.invalid_type,expected:R.undefined,received:t.parsedType}),dr}return pr(e.data)}};Qr.create=e=>new Qr({typeName:Fi.ZodUndefined,...Tr(e)});var $r=class extends Er{_parse(e){if(this._getType(e)!==R.null){let t=this._getOrReturnCtx(e);return B(t,{code:z.invalid_type,expected:R.null,received:t.parsedType}),dr}return pr(e.data)}};$r.create=e=>new $r({typeName:Fi.ZodNull,...Tr(e)});var ei=class extends Er{constructor(){super(...arguments),this._any=!0}_parse(e){return pr(e.data)}};ei.create=e=>new ei({typeName:Fi.ZodAny,...Tr(e)});var ti=class extends Er{constructor(){super(...arguments),this._unknown=!0}_parse(e){return pr(e.data)}};ti.create=e=>new ti({typeName:Fi.ZodUnknown,...Tr(e)});var ni=class extends Er{_parse(e){let t=this._getOrReturnCtx(e);return B(t,{code:z.invalid_type,expected:R.never,received:t.parsedType}),dr}};ni.create=e=>new ni({typeName:Fi.ZodNever,...Tr(e)});var ri=class extends Er{_parse(e){if(this._getType(e)!==R.undefined){let t=this._getOrReturnCtx(e);return B(t,{code:z.invalid_type,expected:R.void,received:t.parsedType}),dr}return pr(e.data)}};ri.create=e=>new ri({typeName:Fi.ZodVoid,...Tr(e)});var ii=class e extends Er{_parse(e){let{ctx:t,status:n}=this._processInputParams(e),r=this._def;if(t.parsedType!==R.array)return B(t,{code:z.invalid_type,expected:R.array,received:t.parsedType}),dr;if(r.exactLength!==null){let e=t.data.length>r.exactLength.value,i=t.data.lengthr.maxLength.value&&(B(t,{code:z.too_big,maximum:r.maxLength.value,type:`array`,inclusive:!0,exact:!1,message:r.maxLength.message}),n.dirty()),t.common.async)return Promise.all([...t.data].map((e,n)=>r.type._parseAsync(new Cr(t,e,t.path,n)))).then(e=>ur.mergeArray(n,e));let i=[...t.data].map((e,n)=>r.type._parseSync(new Cr(t,e,t.path,n)));return ur.mergeArray(n,i)}get element(){return this._def.type}min(t,n){return new e({...this._def,minLength:{value:t,message:br.toString(n)}})}max(t,n){return new e({...this._def,maxLength:{value:t,message:br.toString(n)}})}length(t,n){return new e({...this._def,exactLength:{value:t,message:br.toString(n)}})}nonempty(e){return this.min(1,e)}};ii.create=(e,t)=>new ii({type:e,minLength:null,maxLength:null,exactLength:null,typeName:Fi.ZodArray,...Tr(t)});function ai(e){if(e instanceof oi){let t={};for(let n in e.shape){let r=e.shape[n];t[n]=wi.create(ai(r))}return new oi({...e._def,shape:()=>t})}else if(e instanceof ii)return new ii({...e._def,type:ai(e.element)});else if(e instanceof wi)return wi.create(ai(e.unwrap()));else if(e instanceof Ti)return Ti.create(ai(e.unwrap()));else if(e instanceof fi)return fi.create(e.items.map(e=>ai(e)));else return e}var oi=class e extends Er{constructor(){super(...arguments),this._cached=null,this.nonstrict=this.passthrough,this.augment=this.extend}_getCached(){if(this._cached!==null)return this._cached;let e=this._def.shape(),t=$n.objectKeys(e);return this._cached={shape:e,keys:t}}_parse(e){if(this._getType(e)!==R.object){let t=this._getOrReturnCtx(e);return B(t,{code:z.invalid_type,expected:R.object,received:t.parsedType}),dr}let{status:t,ctx:n}=this._processInputParams(e),{shape:r,keys:i}=this._getCached(),a=[];if(!(this._def.catchall instanceof ni&&this._def.unknownKeys===`strip`))for(let e in n.data)i.includes(e)||a.push(e);let o=[];for(let e of i){let t=r[e],i=n.data[e];o.push({key:{status:`valid`,value:e},value:t._parse(new Cr(n,i,n.path,e)),alwaysSet:e in n.data})}if(this._def.catchall instanceof ni){let e=this._def.unknownKeys;if(e===`passthrough`)for(let e of a)o.push({key:{status:`valid`,value:e},value:{status:`valid`,value:n.data[e]}});else if(e===`strict`)a.length>0&&(B(n,{code:z.unrecognized_keys,keys:a}),t.dirty());else if(e!==`strip`)throw Error(`Internal ZodObject error: invalid unknownKeys value.`)}else{let e=this._def.catchall;for(let t of a){let r=n.data[t];o.push({key:{status:`valid`,value:t},value:e._parse(new Cr(n,r,n.path,t)),alwaysSet:t in n.data})}}return n.common.async?Promise.resolve().then(async()=>{let e=[];for(let t of o){let n=await t.key,r=await t.value;e.push({key:n,value:r,alwaysSet:t.alwaysSet})}return e}).then(e=>ur.mergeObjectSync(t,e)):ur.mergeObjectSync(t,o)}get shape(){return this._def.shape()}strict(t){return br.errToObj,new e({...this._def,unknownKeys:`strict`,...t===void 0?{}:{errorMap:(e,n)=>{var r;let i=(r=this._def).errorMap?.call(r,e,n).message??n.defaultError;return e.code===`unrecognized_keys`?{message:br.errToObj(t).message??i}:{message:i}}}})}strip(){return new e({...this._def,unknownKeys:`strip`})}passthrough(){return new e({...this._def,unknownKeys:`passthrough`})}extend(t){return new e({...this._def,shape:()=>({...this._def.shape(),...t})})}merge(t){return new e({unknownKeys:t._def.unknownKeys,catchall:t._def.catchall,shape:()=>({...this._def.shape(),...t._def.shape()}),typeName:Fi.ZodObject})}setKey(e,t){return this.augment({[e]:t})}catchall(t){return new e({...this._def,catchall:t})}pick(t){let n={};return $n.objectKeys(t).forEach(e=>{t[e]&&this.shape[e]&&(n[e]=this.shape[e])}),new e({...this._def,shape:()=>n})}omit(t){let n={};return $n.objectKeys(this.shape).forEach(e=>{t[e]||(n[e]=this.shape[e])}),new e({...this._def,shape:()=>n})}deepPartial(){return ai(this)}partial(t){let n={};return $n.objectKeys(this.shape).forEach(e=>{let r=this.shape[e];t&&!t[e]?n[e]=r:n[e]=r.optional()}),new e({...this._def,shape:()=>n})}required(t){let n={};return $n.objectKeys(this.shape).forEach(e=>{if(t&&!t[e])n[e]=this.shape[e];else{let t=this.shape[e];for(;t instanceof wi;)t=t._def.innerType;n[e]=t}}),new e({...this._def,shape:()=>n})}keyof(){return yi($n.objectKeys(this.shape))}};oi.create=(e,t)=>new oi({shape:()=>e,unknownKeys:`strip`,catchall:ni.create(),typeName:Fi.ZodObject,...Tr(t)}),oi.strictCreate=(e,t)=>new oi({shape:()=>e,unknownKeys:`strict`,catchall:ni.create(),typeName:Fi.ZodObject,...Tr(t)}),oi.lazycreate=(e,t)=>new oi({shape:e,unknownKeys:`strip`,catchall:ni.create(),typeName:Fi.ZodObject,...Tr(t)});var si=class extends Er{_parse(e){let{ctx:t}=this._processInputParams(e),n=this._def.options;function r(e){for(let t of e)if(t.result.status===`valid`)return t.result;for(let n of e)if(n.result.status===`dirty`)return t.common.issues.push(...n.ctx.common.issues),n.result;let n=e.map(e=>new rr(e.ctx.common.issues));return B(t,{code:z.invalid_union,unionErrors:n}),dr}if(t.common.async)return Promise.all(n.map(async e=>{let n={...t,common:{...t.common,issues:[]},parent:null};return{result:await e._parseAsync({data:t.data,path:t.path,parent:n}),ctx:n}})).then(r);{let e,r=[];for(let i of n){let n={...t,common:{...t.common,issues:[]},parent:null},a=i._parseSync({data:t.data,path:t.path,parent:n});if(a.status===`valid`)return a;a.status===`dirty`&&!e&&(e={result:a,ctx:n}),n.common.issues.length&&r.push(n.common.issues)}if(e)return t.common.issues.push(...e.ctx.common.issues),e.result;let i=r.map(e=>new rr(e));return B(t,{code:z.invalid_union,unionErrors:i}),dr}}get options(){return this._def.options}};si.create=(e,t)=>new si({options:e,typeName:Fi.ZodUnion,...Tr(t)});var ci=e=>e instanceof _i?ci(e.schema):e instanceof Ci?ci(e.innerType()):e instanceof vi?[e.value]:e instanceof bi?e.options:e instanceof xi?$n.objectValues(e.enum):e instanceof Ei?ci(e._def.innerType):e instanceof Qr?[void 0]:e instanceof $r?[null]:e instanceof wi?[void 0,...ci(e.unwrap())]:e instanceof Ti?[null,...ci(e.unwrap())]:e instanceof Ai||e instanceof Mi?ci(e.unwrap()):e instanceof Di?ci(e._def.innerType):[],li=class e extends Er{_parse(e){let{ctx:t}=this._processInputParams(e);if(t.parsedType!==R.object)return B(t,{code:z.invalid_type,expected:R.object,received:t.parsedType}),dr;let n=this.discriminator,r=t.data[n],i=this.optionsMap.get(r);return i?t.common.async?i._parseAsync({data:t.data,path:t.path,parent:t}):i._parseSync({data:t.data,path:t.path,parent:t}):(B(t,{code:z.invalid_union_discriminator,options:Array.from(this.optionsMap.keys()),path:[n]}),dr)}get discriminator(){return this._def.discriminator}get options(){return this._def.options}get optionsMap(){return this._def.optionsMap}static create(t,n,r){let i=new Map;for(let e of n){let n=ci(e.shape[t]);if(!n.length)throw Error(`A discriminator value for key \`${t}\` could not be extracted from all schema options`);for(let r of n){if(i.has(r))throw Error(`Discriminator property ${String(t)} has duplicate value ${String(r)}`);i.set(r,e)}}return new e({typeName:Fi.ZodDiscriminatedUnion,discriminator:t,options:n,optionsMap:i,...Tr(r)})}};function ui(e,t){let n=tr(e),r=tr(t);if(e===t)return{valid:!0,data:e};if(n===R.object&&r===R.object){let n=$n.objectKeys(t),r=$n.objectKeys(e).filter(e=>n.indexOf(e)!==-1),i={...e,...t};for(let n of r){let r=ui(e[n],t[n]);if(!r.valid)return{valid:!1};i[n]=r.data}return{valid:!0,data:i}}else if(n===R.array&&r===R.array){if(e.length!==t.length)return{valid:!1};let n=[];for(let r=0;r{if(mr(e)||mr(r))return dr;let i=ui(e.value,r.value);return i.valid?((hr(e)||hr(r))&&t.dirty(),{status:t.value,value:i.data}):(B(n,{code:z.invalid_intersection_types}),dr)};return n.common.async?Promise.all([this._def.left._parseAsync({data:n.data,path:n.path,parent:n}),this._def.right._parseAsync({data:n.data,path:n.path,parent:n})]).then(([e,t])=>r(e,t)):r(this._def.left._parseSync({data:n.data,path:n.path,parent:n}),this._def.right._parseSync({data:n.data,path:n.path,parent:n}))}};di.create=(e,t,n)=>new di({left:e,right:t,typeName:Fi.ZodIntersection,...Tr(n)});var fi=class e extends Er{_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==R.array)return B(n,{code:z.invalid_type,expected:R.array,received:n.parsedType}),dr;if(n.data.lengththis._def.items.length&&(B(n,{code:z.too_big,maximum:this._def.items.length,inclusive:!0,exact:!1,type:`array`}),t.dirty());let r=[...n.data].map((e,t)=>{let r=this._def.items[t]||this._def.rest;return r?r._parse(new Cr(n,e,n.path,t)):null}).filter(e=>!!e);return n.common.async?Promise.all(r).then(e=>ur.mergeArray(t,e)):ur.mergeArray(t,r)}get items(){return this._def.items}rest(t){return new e({...this._def,rest:t})}};fi.create=(e,t)=>{if(!Array.isArray(e))throw Error(`You must pass an array of schemas to z.tuple([ ... ])`);return new fi({items:e,typeName:Fi.ZodTuple,rest:null,...Tr(t)})};var pi=class e extends Er{get keySchema(){return this._def.keyType}get valueSchema(){return this._def.valueType}_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==R.object)return B(n,{code:z.invalid_type,expected:R.object,received:n.parsedType}),dr;let r=[],i=this._def.keyType,a=this._def.valueType;for(let e in n.data)r.push({key:i._parse(new Cr(n,e,n.path,e)),value:a._parse(new Cr(n,n.data[e],n.path,e)),alwaysSet:e in n.data});return n.common.async?ur.mergeObjectAsync(t,r):ur.mergeObjectSync(t,r)}get element(){return this._def.valueType}static create(t,n,r){return n instanceof Er?new e({keyType:t,valueType:n,typeName:Fi.ZodRecord,...Tr(r)}):new e({keyType:Gr.create(),valueType:t,typeName:Fi.ZodRecord,...Tr(n)})}},mi=class extends Er{get keySchema(){return this._def.keyType}get valueSchema(){return this._def.valueType}_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==R.map)return B(n,{code:z.invalid_type,expected:R.map,received:n.parsedType}),dr;let r=this._def.keyType,i=this._def.valueType,a=[...n.data.entries()].map(([e,t],a)=>({key:r._parse(new Cr(n,e,n.path,[a,`key`])),value:i._parse(new Cr(n,t,n.path,[a,`value`]))}));if(n.common.async){let e=new Map;return Promise.resolve().then(async()=>{for(let n of a){let r=await n.key,i=await n.value;if(r.status===`aborted`||i.status===`aborted`)return dr;(r.status===`dirty`||i.status===`dirty`)&&t.dirty(),e.set(r.value,i.value)}return{status:t.value,value:e}})}else{let e=new Map;for(let n of a){let r=n.key,i=n.value;if(r.status===`aborted`||i.status===`aborted`)return dr;(r.status===`dirty`||i.status===`dirty`)&&t.dirty(),e.set(r.value,i.value)}return{status:t.value,value:e}}}};mi.create=(e,t,n)=>new mi({valueType:t,keyType:e,typeName:Fi.ZodMap,...Tr(n)});var hi=class e extends Er{_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==R.set)return B(n,{code:z.invalid_type,expected:R.set,received:n.parsedType}),dr;let r=this._def;r.minSize!==null&&n.data.sizer.maxSize.value&&(B(n,{code:z.too_big,maximum:r.maxSize.value,type:`set`,inclusive:!0,exact:!1,message:r.maxSize.message}),t.dirty());let i=this._def.valueType;function a(e){let n=new Set;for(let r of e){if(r.status===`aborted`)return dr;r.status===`dirty`&&t.dirty(),n.add(r.value)}return{status:t.value,value:n}}let o=[...n.data.values()].map((e,t)=>i._parse(new Cr(n,e,n.path,t)));return n.common.async?Promise.all(o).then(e=>a(e)):a(o)}min(t,n){return new e({...this._def,minSize:{value:t,message:br.toString(n)}})}max(t,n){return new e({...this._def,maxSize:{value:t,message:br.toString(n)}})}size(e,t){return this.min(e,t).max(e,t)}nonempty(e){return this.min(1,e)}};hi.create=(e,t)=>new hi({valueType:e,minSize:null,maxSize:null,typeName:Fi.ZodSet,...Tr(t)});var gi=class e extends Er{constructor(){super(...arguments),this.validate=this.implement}_parse(e){let{ctx:t}=this._processInputParams(e);if(t.parsedType!==R.function)return B(t,{code:z.invalid_type,expected:R.function,received:t.parsedType}),dr;function n(e,n){return cr({data:e,path:t.path,errorMaps:[t.common.contextualErrorMap,t.schemaErrorMap,sr(),ir].filter(e=>!!e),issueData:{code:z.invalid_arguments,argumentsError:n}})}function r(e,n){return cr({data:e,path:t.path,errorMaps:[t.common.contextualErrorMap,t.schemaErrorMap,sr(),ir].filter(e=>!!e),issueData:{code:z.invalid_return_type,returnTypeError:n}})}let i={errorMap:t.common.contextualErrorMap},a=t.data;if(this._def.returns instanceof Si){let e=this;return pr(async function(...t){let o=new rr([]),s=await e._def.args.parseAsync(t,i).catch(e=>{throw o.addIssue(n(t,e)),o}),c=await Reflect.apply(a,this,s);return await e._def.returns._def.type.parseAsync(c,i).catch(e=>{throw o.addIssue(r(c,e)),o})})}else{let e=this;return pr(function(...t){let o=e._def.args.safeParse(t,i);if(!o.success)throw new rr([n(t,o.error)]);let s=Reflect.apply(a,this,o.data),c=e._def.returns.safeParse(s,i);if(!c.success)throw new rr([r(s,c.error)]);return c.data})}}parameters(){return this._def.args}returnType(){return this._def.returns}args(...t){return new e({...this._def,args:fi.create(t).rest(ti.create())})}returns(t){return new e({...this._def,returns:t})}implement(e){return this.parse(e)}strictImplement(e){return this.parse(e)}static create(t,n,r){return new e({args:t||fi.create([]).rest(ti.create()),returns:n||ti.create(),typeName:Fi.ZodFunction,...Tr(r)})}},_i=class extends Er{get schema(){return this._def.getter()}_parse(e){let{ctx:t}=this._processInputParams(e);return this._def.getter()._parse({data:t.data,path:t.path,parent:t})}};_i.create=(e,t)=>new _i({getter:e,typeName:Fi.ZodLazy,...Tr(t)});var vi=class extends Er{_parse(e){if(e.data!==this._def.value){let t=this._getOrReturnCtx(e);return B(t,{received:t.data,code:z.invalid_literal,expected:this._def.value}),dr}return{status:`valid`,value:e.data}}get value(){return this._def.value}};vi.create=(e,t)=>new vi({value:e,typeName:Fi.ZodLiteral,...Tr(t)});function yi(e,t){return new bi({values:e,typeName:Fi.ZodEnum,...Tr(t)})}var bi=class e extends Er{constructor(){super(...arguments),xr.set(this,void 0)}_parse(e){if(typeof e.data!=`string`){let t=this._getOrReturnCtx(e),n=this._def.values;return B(t,{expected:$n.joinValues(n),received:t.parsedType,code:z.invalid_type}),dr}if(vr(this,xr)||yr(this,xr,new Set(this._def.values)),!vr(this,xr).has(e.data)){let t=this._getOrReturnCtx(e),n=this._def.values;return B(t,{received:t.data,code:z.invalid_enum_value,options:n}),dr}return pr(e.data)}get options(){return this._def.values}get enum(){let e={};for(let t of this._def.values)e[t]=t;return e}get Values(){let e={};for(let t of this._def.values)e[t]=t;return e}get Enum(){let e={};for(let t of this._def.values)e[t]=t;return e}extract(t,n=this._def){return e.create(t,{...this._def,...n})}exclude(t,n=this._def){return e.create(this.options.filter(e=>!t.includes(e)),{...this._def,...n})}};xr=new WeakMap,bi.create=yi;var xi=class extends Er{constructor(){super(...arguments),Sr.set(this,void 0)}_parse(e){let t=$n.getValidEnumValues(this._def.values),n=this._getOrReturnCtx(e);if(n.parsedType!==R.string&&n.parsedType!==R.number){let e=$n.objectValues(t);return B(n,{expected:$n.joinValues(e),received:n.parsedType,code:z.invalid_type}),dr}if(vr(this,Sr)||yr(this,Sr,new Set($n.getValidEnumValues(this._def.values))),!vr(this,Sr).has(e.data)){let e=$n.objectValues(t);return B(n,{received:n.data,code:z.invalid_enum_value,options:e}),dr}return pr(e.data)}get enum(){return this._def.values}};Sr=new WeakMap,xi.create=(e,t)=>new xi({values:e,typeName:Fi.ZodNativeEnum,...Tr(t)});var Si=class extends Er{unwrap(){return this._def.type}_parse(e){let{ctx:t}=this._processInputParams(e);return t.parsedType!==R.promise&&t.common.async===!1?(B(t,{code:z.invalid_type,expected:R.promise,received:t.parsedType}),dr):pr((t.parsedType===R.promise?t.data:Promise.resolve(t.data)).then(e=>this._def.type.parseAsync(e,{path:t.path,errorMap:t.common.contextualErrorMap})))}};Si.create=(e,t)=>new Si({type:e,typeName:Fi.ZodPromise,...Tr(t)});var Ci=class extends Er{innerType(){return this._def.schema}sourceType(){return this._def.schema._def.typeName===Fi.ZodEffects?this._def.schema.sourceType():this._def.schema}_parse(e){let{status:t,ctx:n}=this._processInputParams(e),r=this._def.effect||null,i={addIssue:e=>{B(n,e),e.fatal?t.abort():t.dirty()},get path(){return n.path}};if(i.addIssue=i.addIssue.bind(i),r.type===`preprocess`){let e=r.transform(n.data,i);if(n.common.async)return Promise.resolve(e).then(async e=>{if(t.value===`aborted`)return dr;let r=await this._def.schema._parseAsync({data:e,path:n.path,parent:n});return r.status===`aborted`?dr:r.status===`dirty`||t.value===`dirty`?fr(r.value):r});{if(t.value===`aborted`)return dr;let r=this._def.schema._parseSync({data:e,path:n.path,parent:n});return r.status===`aborted`?dr:r.status===`dirty`||t.value===`dirty`?fr(r.value):r}}if(r.type===`refinement`){let e=e=>{let t=r.refinement(e,i);if(n.common.async)return Promise.resolve(t);if(t instanceof Promise)throw Error(`Async refinement encountered during synchronous parse operation. Use .parseAsync instead.`);return e};if(n.common.async===!1){let r=this._def.schema._parseSync({data:n.data,path:n.path,parent:n});return r.status===`aborted`?dr:(r.status===`dirty`&&t.dirty(),e(r.value),{status:t.value,value:r.value})}else return this._def.schema._parseAsync({data:n.data,path:n.path,parent:n}).then(n=>n.status===`aborted`?dr:(n.status===`dirty`&&t.dirty(),e(n.value).then(()=>({status:t.value,value:n.value}))))}if(r.type===`transform`)if(n.common.async===!1){let e=this._def.schema._parseSync({data:n.data,path:n.path,parent:n});if(!gr(e))return e;let a=r.transform(e.value,i);if(a instanceof Promise)throw Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`);return{status:t.value,value:a}}else return this._def.schema._parseAsync({data:n.data,path:n.path,parent:n}).then(e=>gr(e)?Promise.resolve(r.transform(e.value,i)).then(e=>({status:t.value,value:e})):e);$n.assertNever(r)}};Ci.create=(e,t,n)=>new Ci({schema:e,typeName:Fi.ZodEffects,effect:t,...Tr(n)}),Ci.createWithPreprocess=(e,t,n)=>new Ci({schema:t,effect:{type:`preprocess`,transform:e},typeName:Fi.ZodEffects,...Tr(n)});var wi=class extends Er{_parse(e){return this._getType(e)===R.undefined?pr(void 0):this._def.innerType._parse(e)}unwrap(){return this._def.innerType}};wi.create=(e,t)=>new wi({innerType:e,typeName:Fi.ZodOptional,...Tr(t)});var Ti=class extends Er{_parse(e){return this._getType(e)===R.null?pr(null):this._def.innerType._parse(e)}unwrap(){return this._def.innerType}};Ti.create=(e,t)=>new Ti({innerType:e,typeName:Fi.ZodNullable,...Tr(t)});var Ei=class extends Er{_parse(e){let{ctx:t}=this._processInputParams(e),n=t.data;return t.parsedType===R.undefined&&(n=this._def.defaultValue()),this._def.innerType._parse({data:n,path:t.path,parent:t})}removeDefault(){return this._def.innerType}};Ei.create=(e,t)=>new Ei({innerType:e,typeName:Fi.ZodDefault,defaultValue:typeof t.default==`function`?t.default:()=>t.default,...Tr(t)});var Di=class extends Er{_parse(e){let{ctx:t}=this._processInputParams(e),n={...t,common:{...t.common,issues:[]}},r=this._def.innerType._parse({data:n.data,path:n.path,parent:{...n}});return _r(r)?r.then(e=>({status:`valid`,value:e.status===`valid`?e.value:this._def.catchValue({get error(){return new rr(n.common.issues)},input:n.data})})):{status:`valid`,value:r.status===`valid`?r.value:this._def.catchValue({get error(){return new rr(n.common.issues)},input:n.data})}}removeCatch(){return this._def.innerType}};Di.create=(e,t)=>new Di({innerType:e,typeName:Fi.ZodCatch,catchValue:typeof t.catch==`function`?t.catch:()=>t.catch,...Tr(t)});var Oi=class extends Er{_parse(e){if(this._getType(e)!==R.nan){let t=this._getOrReturnCtx(e);return B(t,{code:z.invalid_type,expected:R.nan,received:t.parsedType}),dr}return{status:`valid`,value:e.data}}};Oi.create=e=>new Oi({typeName:Fi.ZodNaN,...Tr(e)});var ki=Symbol(`zod_brand`),Ai=class extends Er{_parse(e){let{ctx:t}=this._processInputParams(e),n=t.data;return this._def.type._parse({data:n,path:t.path,parent:t})}unwrap(){return this._def.type}},ji=class e extends Er{_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.common.async)return(async()=>{let e=await this._def.in._parseAsync({data:n.data,path:n.path,parent:n});return e.status===`aborted`?dr:e.status===`dirty`?(t.dirty(),fr(e.value)):this._def.out._parseAsync({data:e.value,path:n.path,parent:n})})();{let e=this._def.in._parseSync({data:n.data,path:n.path,parent:n});return e.status===`aborted`?dr:e.status===`dirty`?(t.dirty(),{status:`dirty`,value:e.value}):this._def.out._parseSync({data:e.value,path:n.path,parent:n})}}static create(t,n){return new e({in:t,out:n,typeName:Fi.ZodPipeline})}},Mi=class extends Er{_parse(e){let t=this._def.innerType._parse(e),n=e=>(gr(e)&&(e.value=Object.freeze(e.value)),e);return _r(t)?t.then(e=>n(e)):n(t)}unwrap(){return this._def.innerType}};Mi.create=(e,t)=>new Mi({innerType:e,typeName:Fi.ZodReadonly,...Tr(t)});function Ni(e,t={},n){return e?ei.create().superRefine((r,i)=>{if(!e(r)){let e=typeof t==`function`?t(r):typeof t==`string`?{message:t}:t,a=e.fatal??n??!0,o=typeof e==`string`?{message:e}:e;i.addIssue({code:`custom`,...o,fatal:a})}}):ei.create()}var Pi={object:oi.lazycreate},Fi;(function(e){e.ZodString=`ZodString`,e.ZodNumber=`ZodNumber`,e.ZodNaN=`ZodNaN`,e.ZodBigInt=`ZodBigInt`,e.ZodBoolean=`ZodBoolean`,e.ZodDate=`ZodDate`,e.ZodSymbol=`ZodSymbol`,e.ZodUndefined=`ZodUndefined`,e.ZodNull=`ZodNull`,e.ZodAny=`ZodAny`,e.ZodUnknown=`ZodUnknown`,e.ZodNever=`ZodNever`,e.ZodVoid=`ZodVoid`,e.ZodArray=`ZodArray`,e.ZodObject=`ZodObject`,e.ZodUnion=`ZodUnion`,e.ZodDiscriminatedUnion=`ZodDiscriminatedUnion`,e.ZodIntersection=`ZodIntersection`,e.ZodTuple=`ZodTuple`,e.ZodRecord=`ZodRecord`,e.ZodMap=`ZodMap`,e.ZodSet=`ZodSet`,e.ZodFunction=`ZodFunction`,e.ZodLazy=`ZodLazy`,e.ZodLiteral=`ZodLiteral`,e.ZodEnum=`ZodEnum`,e.ZodEffects=`ZodEffects`,e.ZodNativeEnum=`ZodNativeEnum`,e.ZodOptional=`ZodOptional`,e.ZodNullable=`ZodNullable`,e.ZodDefault=`ZodDefault`,e.ZodCatch=`ZodCatch`,e.ZodPromise=`ZodPromise`,e.ZodBranded=`ZodBranded`,e.ZodPipeline=`ZodPipeline`,e.ZodReadonly=`ZodReadonly`})(Fi||={});var Ii=(e,t={message:`Input not instance of ${e.name}`})=>Ni(t=>t instanceof e,t),V=Gr.create,Li=qr.create,Ri=Oi.create,zi=Jr.create,Bi=Yr.create,Vi=Xr.create,Hi=Zr.create,Ui=Qr.create,Wi=$r.create,Gi=ei.create,Ki=ti.create,qi=ni.create,Ji=ri.create,Yi=ii.create,H=oi.create,Xi=oi.strictCreate,Zi=si.create,Qi=li.create,$i=di.create,ea=fi.create,ta=pi.create,na=mi.create,ra=hi.create,ia=gi.create,aa=_i.create,oa=vi.create,sa=bi.create,ca=xi.create,la=Si.create,ua=Ci.create,da=wi.create,fa=Ti.create,pa=Ci.createWithPreprocess,ma=ji.create,U=Object.freeze({__proto__:null,defaultErrorMap:ir,setErrorMap:or,getErrorMap:sr,makeIssue:cr,EMPTY_PATH:lr,addIssueToContext:B,ParseStatus:ur,INVALID:dr,DIRTY:fr,OK:pr,isAborted:mr,isDirty:hr,isValid:gr,isAsync:_r,get util(){return $n},get objectUtil(){return er},ZodParsedType:R,getParsedType:tr,ZodType:Er,datetimeRegex:Ur,ZodString:Gr,ZodNumber:qr,ZodBigInt:Jr,ZodBoolean:Yr,ZodDate:Xr,ZodSymbol:Zr,ZodUndefined:Qr,ZodNull:$r,ZodAny:ei,ZodUnknown:ti,ZodNever:ni,ZodVoid:ri,ZodArray:ii,ZodObject:oi,ZodUnion:si,ZodDiscriminatedUnion:li,ZodIntersection:di,ZodTuple:fi,ZodRecord:pi,ZodMap:mi,ZodSet:hi,ZodFunction:gi,ZodLazy:_i,ZodLiteral:vi,ZodEnum:bi,ZodNativeEnum:xi,ZodPromise:Si,ZodEffects:Ci,ZodTransformer:Ci,ZodOptional:wi,ZodNullable:Ti,ZodDefault:Ei,ZodCatch:Di,ZodNaN:Oi,BRAND:ki,ZodBranded:Ai,ZodPipeline:ji,ZodReadonly:Mi,custom:Ni,Schema:Er,ZodSchema:Er,late:Pi,get ZodFirstPartyTypeKind(){return Fi},coerce:{string:(e=>Gr.create({...e,coerce:!0})),number:(e=>qr.create({...e,coerce:!0})),boolean:(e=>Yr.create({...e,coerce:!0})),bigint:(e=>Jr.create({...e,coerce:!0})),date:(e=>Xr.create({...e,coerce:!0}))},any:Gi,array:Yi,bigint:zi,boolean:Bi,date:Vi,discriminatedUnion:Qi,effect:ua,enum:sa,function:ia,instanceof:Ii,intersection:$i,lazy:aa,literal:oa,map:na,nan:Ri,nativeEnum:ca,never:qi,null:Wi,nullable:fa,number:Li,object:H,oboolean:()=>Bi().optional(),onumber:()=>Li().optional(),optional:da,ostring:()=>V().optional(),pipeline:ma,preprocess:pa,promise:la,record:ta,set:ra,strictObject:Xi,string:V,symbol:Hi,transformer:ua,tuple:ea,undefined:Ui,union:Zi,unknown:Ki,void:Ji,NEVER:dr,ZodIssueCode:z,quotelessJson:nr,ZodError:rr}),ha={exports:{}},ga;function _a(){return ga?ha.exports:(ga=1,(function(e){var t=(function(e){var n=1e7,r=9007199254740992,i=f(r),a=`0123456789abcdefghijklmnopqrstuvwxyz`,o=typeof BigInt==`function`;function s(e,t,n,r){return e===void 0?s[0]:t===void 0||+t==10&&!n?I(e):he(e,t,n,r)}function c(e,t){this.value=e,this.sign=t,this.isSmall=!1}c.prototype=Object.create(s.prototype);function l(e){this.value=e,this.sign=e<0,this.isSmall=!0}l.prototype=Object.create(s.prototype);function u(e){this.value=e}u.prototype=Object.create(s.prototype);function d(e){return-r0?Math.floor(e):Math.ceil(e)}function _(e,t){var r=e.length,i=t.length,a=Array(r),o=0,s=n,c,l;for(l=0;l=s),a[l]=c-o*s;for(;l0&&a.push(o),a}function v(e,t){return e.length>=t.length?_(e,t):_(t,e)}function y(e,t){var r=e.length,i=Array(r),a=n,o,s;for(s=0;s0;)i[s++]=t%a,t=Math.floor(t/a);return i}c.prototype.add=function(e){var t=I(e);if(this.sign!==t.sign)return this.subtract(t.negate());var n=this.value,r=t.value;return t.isSmall?new c(y(n,Math.abs(r)),this.sign):new c(v(n,r),this.sign)},c.prototype.plus=c.prototype.add,l.prototype.add=function(e){var t=I(e),n=this.value;if(n<0!==t.sign)return this.subtract(t.negate());var r=t.value;if(t.isSmall){if(d(n+r))return new l(n+r);r=f(Math.abs(r))}return new c(y(r,Math.abs(n)),n<0)},l.prototype.plus=l.prototype.add,u.prototype.add=function(e){return new u(this.value+I(e).value)},u.prototype.plus=u.prototype.add;function b(e,t){var r=e.length,i=t.length,a=Array(r),o=0,s=n,c,l;for(c=0;c=0?r=b(e,t):(r=b(t,e),n=!n),r=p(r),typeof r==`number`?(n&&(r=-r),new l(r)):new c(r,n)}function S(e,t,r){var i=e.length,a=Array(i),o=-t,s=n,u,d;for(u=0;u=0)},l.prototype.minus=l.prototype.subtract,u.prototype.subtract=function(e){return new u(this.value-I(e).value)},u.prototype.minus=u.prototype.subtract,c.prototype.negate=function(){return new c(this.value,!this.sign)},l.prototype.negate=function(){var e=this.sign,t=new l(-this.value);return t.sign=!e,t},u.prototype.negate=function(){return new u(-this.value)},c.prototype.abs=function(){return new c(this.value,!1)},l.prototype.abs=function(){return new l(Math.abs(this.value))},u.prototype.abs=function(){return new u(this.value>=0?this.value:-this.value)};function C(e,t){var r=e.length,i=t.length,a=h(r+i),o=n,s,c,l,u,d;for(l=0;l0;)i[c++]=o%a,o=Math.floor(o/a);return i}function T(e,t){for(var n=[];t-->0;)n.push(0);return n.concat(e)}function E(e,t){var n=Math.max(e.length,t.length);if(n<=30)return C(e,t);n=Math.ceil(n/2);var r=e.slice(n),i=e.slice(0,n),a=t.slice(n),o=t.slice(0,n),s=E(i,o),c=E(r,a),l=v(v(s,T(b(b(E(v(i,r),v(o,a)),s),c),n)),T(c,2*n));return m(l),l}function D(e,t){return-.012*e-.012*t+15e-6*e*t>0}c.prototype.multiply=function(e){var t=I(e),r=this.value,i=t.value,a=this.sign!==t.sign,o;if(t.isSmall){if(i===0)return s[0];if(i===1)return this;if(i===-1)return this.negate();if(o=Math.abs(i),o=0;f--){for(d=a-1,l[f+i]!==s&&(d=Math.floor((l[f+i]*a+l[f+i-1])/s)),m=0,g=0,v=u.length,_=0;_i&&(u=(u+1)*s),c=Math.ceil(u/d);do{if(f=w(t,c),re(f,o)<=0)break;c--}while(c);a.push(c),o=b(o,f)}return a.reverse(),[p(a),p(o)]}function ne(e,t){var r=e.length,i=h(r),a=n,o,s,c=0,l;for(o=r-1;o>=0;--o)l=c*a+e[o],s=g(l/t),c=l-s*t,i[o]=s|0;return[i,c|0]}function A(e,t){var r,i=I(t);if(o)return[new u(e.value/i.value),new u(e.value%i.value)];var a=e.value,d=i.value,m;if(d===0)throw Error(`Cannot divide by zero`);if(e.isSmall)return i.isSmall?[new l(g(a/d)),new l(a%d)]:[s[0],e];if(i.isSmall){if(d===1)return[e,s[0]];if(d==-1)return[e.negate(),s[0]];var h=Math.abs(d);if(ht.length?1:-1;for(var n=e.length-1;n>=0;n--)if(e[n]!==t[n])return e[n]>t[n]?1:-1;return 0}c.prototype.compareAbs=function(e){var t=I(e),n=this.value,r=t.value;return t.isSmall?1:re(n,r)},l.prototype.compareAbs=function(e){var t=I(e),n=Math.abs(this.value),r=t.value;return t.isSmall?(r=Math.abs(r),n===r?0:n>r?1:-1):-1},u.prototype.compareAbs=function(e){var t=this.value,n=I(e).value;return t=t>=0?t:-t,n=n>=0?n:-n,t===n?0:t>n?1:-1},c.prototype.compare=function(e){if(e===1/0)return-1;if(e===-1/0)return 1;var t=I(e),n=this.value,r=t.value;return this.sign===t.sign?t.isSmall?this.sign?-1:1:re(n,r)*(this.sign?-1:1):t.sign?1:-1},c.prototype.compareTo=c.prototype.compare,l.prototype.compare=function(e){if(e===1/0)return-1;if(e===-1/0)return 1;var t=I(e),n=this.value,r=t.value;return t.isSmall?n==r?0:n>r?1:-1:n<0===t.sign?n<0?1:-1:n<0?-1:1},l.prototype.compareTo=l.prototype.compare,u.prototype.compare=function(e){if(e===1/0)return-1;if(e===-1/0)return 1;var t=this.value,n=I(e).value;return t===n?0:t>n?1:-1},u.prototype.compareTo=u.prototype.compare,c.prototype.equals=function(e){return this.compare(e)===0},u.prototype.eq=u.prototype.equals=l.prototype.eq=l.prototype.equals=c.prototype.eq=c.prototype.equals,c.prototype.notEquals=function(e){return this.compare(e)!==0},u.prototype.neq=u.prototype.notEquals=l.prototype.neq=l.prototype.notEquals=c.prototype.neq=c.prototype.notEquals,c.prototype.greater=function(e){return this.compare(e)>0},u.prototype.gt=u.prototype.greater=l.prototype.gt=l.prototype.greater=c.prototype.gt=c.prototype.greater,c.prototype.lesser=function(e){return this.compare(e)<0},u.prototype.lt=u.prototype.lesser=l.prototype.lt=l.prototype.lesser=c.prototype.lt=c.prototype.lesser,c.prototype.greaterOrEquals=function(e){return this.compare(e)>=0},u.prototype.geq=u.prototype.greaterOrEquals=l.prototype.geq=l.prototype.greaterOrEquals=c.prototype.geq=c.prototype.greaterOrEquals,c.prototype.lesserOrEquals=function(e){return this.compare(e)<=0},u.prototype.leq=u.prototype.lesserOrEquals=l.prototype.leq=l.prototype.lesserOrEquals=c.prototype.leq=c.prototype.lesserOrEquals,c.prototype.isEven=function(){return(this.value[0]&1)==0},l.prototype.isEven=function(){return(this.value&1)==0},u.prototype.isEven=function(){return(this.value&BigInt(1))===BigInt(0)},c.prototype.isOdd=function(){return(this.value[0]&1)==1},l.prototype.isOdd=function(){return(this.value&1)==1},u.prototype.isOdd=function(){return(this.value&BigInt(1))===BigInt(1)},c.prototype.isPositive=function(){return!this.sign},l.prototype.isPositive=function(){return this.value>0},u.prototype.isPositive=l.prototype.isPositive,c.prototype.isNegative=function(){return this.sign},l.prototype.isNegative=function(){return this.value<0},u.prototype.isNegative=l.prototype.isNegative,c.prototype.isUnit=function(){return!1},l.prototype.isUnit=function(){return Math.abs(this.value)===1},u.prototype.isUnit=function(){return this.abs().value===BigInt(1)},c.prototype.isZero=function(){return!1},l.prototype.isZero=function(){return this.value===0},u.prototype.isZero=function(){return this.value===BigInt(0)},c.prototype.isDivisibleBy=function(e){var t=I(e);return t.isZero()?!1:t.isUnit()?!0:t.compareAbs(2)===0?this.isEven():this.mod(t).isZero()},u.prototype.isDivisibleBy=l.prototype.isDivisibleBy=c.prototype.isDivisibleBy;function ie(e){var t=e.abs();if(t.isUnit())return!1;if(t.equals(2)||t.equals(3)||t.equals(5))return!0;if(t.isEven()||t.isDivisibleBy(3)||t.isDivisibleBy(5))return!1;if(t.lesser(49))return!0}function ae(e,n){for(var r=e.prev(),i=r,a=0,o,s,c;i.isEven();)i=i.divide(2),a++;next:for(s=0;s-r?new l(e-1):new c(i,!0)},u.prototype.prev=function(){return new u(this.value-BigInt(1))};for(var oe=[1];2*oe[oe.length-1]<=n;)oe.push(2*oe[oe.length-1]);var j=oe.length,se=oe[j-1];function ce(e){return Math.abs(e)<=n}c.prototype.shiftLeft=function(e){var t=I(e).toJSNumber();if(!ce(t))throw Error(String(t)+` is too large for shifting.`);if(t<0)return this.shiftRight(-t);var n=this;if(n.isZero())return n;for(;t>=j;)n=n.multiply(se),t-=j-1;return n.multiply(oe[t])},u.prototype.shiftLeft=l.prototype.shiftLeft=c.prototype.shiftLeft,c.prototype.shiftRight=function(e){var t,n=I(e).toJSNumber();if(!ce(n))throw Error(String(n)+` is too large for shifting.`);if(n<0)return this.shiftLeft(-n);for(var r=this;n>=j;){if(r.isZero()||r.isNegative()&&r.isUnit())return r;t=A(r,se),r=t[1].isNegative()?t[0].prev():t[0],n-=j-1}return t=A(r,oe[n]),t[1].isNegative()?t[0].prev():t[0]},u.prototype.shiftRight=l.prototype.shiftRight=c.prototype.shiftRight;function le(e,n,r){n=I(n);for(var i=e.isNegative(),a=n.isNegative(),o=i?e.not():e,s=a?n.not():n,c=0,l=0,u=null,d=null,f=[];!o.isZero()||!s.isZero();)u=A(o,se),c=u[1].toJSNumber(),i&&(c=se-1-c),d=A(s,se),l=d[1].toJSNumber(),a&&(l=se-1-l),o=u[0],s=d[0],f.push(r(c,l));for(var p=r(+!!i,+!!a)===0?t(0):t(-1),m=f.length-1;m>=0;--m)p=p.multiply(se).add(t(f[m]));return p}c.prototype.not=function(){return this.negate().prev()},u.prototype.not=l.prototype.not=c.prototype.not,c.prototype.and=function(e){return le(this,e,function(e,t){return e&t})},u.prototype.and=l.prototype.and=c.prototype.and,c.prototype.or=function(e){return le(this,e,function(e,t){return e|t})},u.prototype.or=l.prototype.or=c.prototype.or,c.prototype.xor=function(e){return le(this,e,function(e,t){return e^t})},u.prototype.xor=l.prototype.xor=c.prototype.xor;var ue=1<<30,M=(n&-n)*(n&-n)|ue;function N(e){var t=e.value,r=typeof t==`number`?t|ue:typeof t==`bigint`?t|BigInt(ue):t[0]+t[1]*n|M;return r&-r}function de(e,n){if(n.compareTo(e)<=0){var r=de(e,n.square(n)),i=r.p,a=r.e,o=i.multiply(n);return o.compareTo(e)<=0?{p:o,e:a*2+1}:{p:i,e:a*2}}return{p:t(1),e:0}}c.prototype.bitLength=function(){var e=this;return e.compareTo(t(0))<0&&(e=e.negate().subtract(t(1))),e.compareTo(t(0))===0?t(0):t(de(e,t(2)).e).add(t(1))},u.prototype.bitLength=l.prototype.bitLength=c.prototype.bitLength;function fe(e,t){return e=I(e),t=I(t),e.greater(t)?e:t}function pe(e,t){return e=I(e),t=I(t),e.lesser(t)?e:t}function me(e,t){if(e=I(e).abs(),t=I(t).abs(),e.equals(t))return e;if(e.isZero())return t;if(t.isZero())return e;for(var n=s[1],r,i;e.isEven()&&t.isEven();)r=pe(N(e),N(t)),e=e.divide(r),t=t.divide(r),n=n.multiply(r);for(;e.isEven();)e=e.divide(N(e));do{for(;t.isEven();)t=t.divide(N(t));e.greater(t)&&(i=t,t=e,e=i),t=t.subtract(e)}while(!t.isZero());return n.isUnit()?e:e.multiply(n)}function P(e,t){return e=I(e).abs(),t=I(t).abs(),e.divide(me(e,t)).multiply(t)}function F(e,t,r){e=I(e),t=I(t);var i=r||Math.random,a=pe(e,t),o=fe(e,t).subtract(a).add(1);if(o.isSmall)return a.add(Math.floor(i()*o));for(var c=ve(o,n).value,l=[],u=!0,d=0;d=s){if(l===`1`&&s===1)continue;throw Error(l+` is not a valid digit in base `+t+`.`)}}t=I(t);var u=[],d=e[0]===`-`;for(o=+!!d;o`&&o=0;a--)r=r.add(e[a].times(i)),i=i.times(t);return n?r.negate():r}function _e(e,t){return t||=a,e`}function ve(e,n){if(n=t(n),n.isZero()){if(e.isZero())return{value:[0],isNegative:!1};throw Error(`Cannot convert nonzero numbers to base 0.`)}if(n.equals(-1)){if(e.isZero())return{value:[0],isNegative:!1};if(e.isNegative())return{value:[].concat.apply([],Array.apply(null,Array(-e.toJSNumber())).map(Array.prototype.valueOf,[1,0])),isNegative:!1};var r=Array.apply(null,Array(e.toJSNumber()-1)).map(Array.prototype.valueOf,[0,1]);return r.unshift([1]),{value:[].concat.apply([],r),isNegative:!1}}var i=!1;if(e.isNegative()&&n.isPositive()&&(i=!0,e=e.abs()),n.isUnit())return e.isZero()?{value:[0],isNegative:!1}:{value:Array.apply(null,Array(e.toJSNumber())).map(Number.prototype.valueOf,1),isNegative:i};for(var a=[],o=e,s;o.isNegative()||o.compareAbs(n)>=0;){s=o.divmod(n),o=s.quotient;var c=s.remainder;c.isNegative()&&(c=n.minus(c).abs(),o=o.next()),a.push(c.toJSNumber())}return a.push(o.toJSNumber()),{value:a.reverse(),isNegative:i}}function ye(e,t,n){var r=ve(e,t);return(r.isNegative?`-`:``)+r.value.map(function(e){return _e(e,n)}).join(``)}c.prototype.toArray=function(e){return ve(this,e)},l.prototype.toArray=function(e){return ve(this,e)},u.prototype.toArray=function(e){return ve(this,e)},c.prototype.toString=function(t,n){if(t===e&&(t=10),t!==10||n)return ye(this,t,n);for(var r=this.value,i=r.length,a=String(r[--i]),o=`0000000`,s;--i>=0;)s=String(r[i]),a+=o.slice(s.length)+s;return(this.sign?`-`:``)+a},l.prototype.toString=function(t,n){return t===e&&(t=10),t!=10||n?ye(this,t,n):String(this.value)},u.prototype.toString=l.prototype.toString,u.prototype.toJSON=c.prototype.toJSON=l.prototype.toJSON=function(){return this.toString()},c.prototype.valueOf=function(){return parseInt(this.toString(),10)},c.prototype.toJSNumber=c.prototype.valueOf,l.prototype.valueOf=function(){return this.value},l.prototype.toJSNumber=l.prototype.valueOf,u.prototype.valueOf=u.prototype.toJSNumber=function(){return parseInt(this.toString(),10)};function be(e){if(d(+e)){var t=+e;if(t===g(t))return o?new u(BigInt(t)):new l(t);throw Error(`Invalid integer: `+e)}var n=e[0]===`-`;n&&(e=e.slice(1));var r=e.split(/e/i);if(r.length>2)throw Error(`Invalid integer: `+r.join(`e`));if(r.length===2){var i=r[1];if(i[0]===`+`&&(i=i.slice(1)),i=+i,i!==g(i)||!d(i))throw Error(`Invalid integer: `+i+` is not a valid exponent.`);var a=r[0],s=a.indexOf(`.`);if(s>=0&&(i-=a.length-s-1,a=a.slice(0,s)+a.slice(s+1)),i<0)throw Error(`Cannot include negative exponent part for integers`);a+=Array(i+1).join(`0`),e=a}if(!/^([0-9][0-9]*)$/.test(e))throw Error(`Invalid integer: `+e);if(o)return new u(BigInt(n?`-`+e:e));for(var f=[],p=e.length,h=7,_=p-h;p>0;)f.push(+e.slice(_,p)),_-=h,_<0&&(_=0),p-=h;return m(f),new c(f,n)}function xe(e){if(o)return new u(BigInt(e));if(d(e)){if(e!==g(e))throw Error(e+` is not an integer.`);return new l(e)}return be(e.toString())}function I(e){return typeof e==`number`?xe(e):typeof e==`string`?be(e):typeof e==`bigint`?new u(e):e}for(var Se=0;Se<1e3;Se++)s[Se]=I(Se),Se>0&&(s[-Se]=I(-Se));return s.one=s[1],s.zero=s[0],s.minusOne=s[-1],s.max=fe,s.min=pe,s.gcd=me,s.lcm=P,s.isInstance=function(e){return e instanceof c||e instanceof l||e instanceof u},s.randBetween=F,s.fromArray=function(e,t,n){return ge(e.map(I),I(t||10),n)},s})();e.hasOwnProperty(`exports`)&&(e.exports=t)})(ha),ha.exports)}var va=Jn(_a()),ya=64,ba=16,xa=ya/ba;function Sa(){try{return!0}catch{return!1}}function Ca(e,t,n){let r=0;for(let i=0;ie&t[n]))}or({parts:t}){return new e(this.parts.map((e,n)=>e|t[n]))}xor({parts:t}){return new e(this.parts.map((e,n)=>e^t[n]))}not(){return new e(this.parts.map(e=>~e))}equals({parts:e}){return this.parts.every((t,n)=>t===e[n])}toString(){if(this.str!=null)return this.str;let e=Array(ya/4);return this.parts.forEach((t,n)=>{let r=wa(t.toString());for(let t=0;t<4;t++)e[t+n*4]=r[3-t]||0}),this.str=va.fromArray(e,16).toString()}toJSON(){return this.toString()}},Da=Sa();Da&&BigInt.prototype.toJSON==null&&(BigInt.prototype.toJSON=function(){return this.toString()});var Oa={},ka=Da?function(e){return BigInt(e)}:function(e){return e instanceof Ea?e:(typeof e==`number`&&(e=e.toString()),Oa[e]??(Oa[e]=Ea.fromString(e)),Oa[e])},Aa=ka(0),ja=Da?function(e=Aa,t=Aa){return e&t}:function(e=Aa,t=Aa){return e.and(t)},Ma=Da?function(e=Aa,t=Aa){return e|t}:function(e=Aa,t=Aa){return e.or(t)},Na=Da?function(e=Aa,t=Aa){return e^t}:function(e=Aa,t=Aa){return e.xor(t)},Pa=Da?function(e=Aa){return~e}:function(e=Aa){return e.not()},Fa=Da?function(e,t){return e===t}:function(e,t){return e==null||t==null?e==t:e.equals(t)};function Ia(...e){let t=e[0];for(let n=1;n{let[n]=Object.entries(e).find(([,e])=>e===t)??[];return t!=null&&n===void 0?e.UNHANDLED:t},V().or(Li()))}function qa(e){let t=Ni().transform(t=>{let n=e.safeParse(t);return n.success?n.data:e._def.defaultValue()});return t.overlayType=e,t}var Ja=U.object({image_url:U.string()}).describe(`Response for "INITIATE_IMAGE_UPLOAD" Command`),Ya=U.object({mediaUrl:U.string().max(1024)}).describe(`Request for "OPEN_SHARE_MOMENT_DIALOG" Command`),Xa=U.object({access_token:U.union([U.string(),U.null()]).optional()}).describe(`Request for "AUTHENTICATE" Command`),Za=U.object({access_token:U.string(),user:U.object({username:U.string(),discriminator:U.string(),id:U.string(),avatar:U.union([U.string(),U.null()]).optional(),public_flags:U.number(),global_name:U.union([U.string(),U.null()]).optional()}),scopes:U.array(qa(U.enum(`identify,identify.premium,email,connections,guilds,guilds.join,guilds.members.read,guilds.channels.read,gdm.join,bot,rpc,rpc.notifications.read,rpc.voice.read,rpc.voice.write,rpc.video.read,rpc.video.write,rpc.screenshare.read,rpc.screenshare.write,rpc.activities.write,webhook.incoming,messages.read,applications.builds.upload,applications.builds.read,applications.commands,applications.commands.permissions.update,applications.commands.update,applications.store.update,applications.entitlements,activities.read,activities.write,activities.invites.write,relationships.read,relationships.write,voice,dm_channels.read,role_connections.write,presences.read,presences.write,openid,dm_channels.messages.read,dm_channels.messages.write,gateway.connect,account.global_name.update,payment_sources.country_code,sdk.social_layer_presence,sdk.social_layer,lobbies.write,application_identities.write`.split(`,`)).or(U.literal(-1)).default(-1))),expires:U.string(),application:U.object({description:U.string(),icon:U.union([U.string(),U.null()]).optional(),id:U.string(),rpc_origins:U.array(U.string()).optional(),name:U.string()})}).describe(`Response for "AUTHENTICATE" Command`),Qa=U.object({participants:U.array(U.object({id:U.string(),username:U.string(),global_name:U.union([U.string(),U.null()]).optional(),discriminator:U.string(),avatar:U.union([U.string(),U.null()]).optional(),flags:U.number(),bot:U.boolean(),avatar_decoration_data:U.union([U.object({asset:U.union([U.string(),U.null()]).optional(),skuId:U.string().optional(),expiresAt:U.number().optional()}),U.null()]).optional(),premium_type:U.union([U.number(),U.null()]).optional(),nickname:U.string().optional()}))}).describe(`Response for "GET_ACTIVITY_INSTANCE_CONNECTED_PARTICIPANTS" Command`),$a=U.object({command:U.string(),options:U.array(U.object({name:U.string(),value:U.string()})).optional(),content:U.string().max(2e3).optional(),require_launch_channel:U.boolean().optional(),preview_image:U.object({height:U.number(),url:U.string(),width:U.number()}).optional(),components:U.array(U.object({type:U.literal(1),components:U.array(U.object({type:U.literal(2),style:U.number().gte(1).lte(5),label:U.string().max(80).optional(),custom_id:U.string().max(100).describe(`Developer-defined identifier for the button; max 100 characters`).optional()})).max(5).optional()})).optional(),pid:U.number().optional()}).describe(`Request for "SHARE_INTERACTION" Command`),eo=U.object({success:U.boolean()}).describe(`Response for "SHARE_INTERACTION" Command`),to=U.object({custom_id:U.string().max(64).optional(),message:U.string().max(1e3),link_id:U.string().max(64).optional()}).describe(`Request for "SHARE_LINK" Command`),no=U.object({success:U.boolean(),didCopyLink:U.boolean(),didSendMessage:U.boolean()}).describe(`Response for "SHARE_LINK" Command`),ro=U.object({relationships:U.array(U.object({type:U.number(),user:U.object({id:U.string(),username:U.string(),global_name:U.union([U.string(),U.null()]).optional(),discriminator:U.string(),avatar:U.union([U.string(),U.null()]).optional(),flags:U.number(),bot:U.boolean(),avatar_decoration_data:U.union([U.object({asset:U.union([U.string(),U.null()]).optional(),skuId:U.string().optional(),expiresAt:U.number().optional()}),U.null()]).optional(),premium_type:U.union([U.number(),U.null()]).optional()}),presence:U.object({status:U.string(),activity:U.union([U.object({session_id:U.string().optional(),type:U.number().optional(),name:U.string(),url:U.union([U.string(),U.null()]).optional(),application_id:U.string().optional(),status_display_type:U.number().optional(),state:U.string().optional(),state_url:U.string().optional(),details:U.string().optional(),details_url:U.string().optional(),emoji:U.union([U.object({name:U.string(),id:U.union([U.string(),U.null()]).optional(),animated:U.union([U.boolean(),U.null()]).optional()}),U.null()]).optional(),assets:U.object({large_image:U.string().optional(),large_text:U.string().optional(),large_url:U.string().optional(),small_image:U.string().optional(),small_text:U.string().optional(),small_url:U.string().optional()}).optional(),timestamps:U.object({start:U.number().optional(),end:U.number().optional()}).optional(),party:U.object({id:U.string().optional(),size:U.array(U.number()).min(2).max(2).optional(),privacy:U.number().optional()}).optional(),secrets:U.object({match:U.string().optional(),join:U.string().optional()}).optional(),sync_id:U.string().optional(),created_at:U.number().optional(),instance:U.boolean().optional(),flags:U.number().optional(),metadata:U.object({}).optional(),platform:U.string().optional(),supported_platforms:U.array(U.string()).optional(),buttons:U.array(U.string()).optional(),hangStatus:U.string().optional()}),U.null()]).optional()}).optional()}))}).describe(`Response for "GET_RELATIONSHIPS" Command`),io=U.object({user_id:U.string(),content:U.string().min(0).max(1024).optional()}).describe(`Request for "INVITE_USER_EMBEDDED" Command`),ao=U.object({id:U.string().max(64)}).describe(`Request for "GET_USER" Command`),oo=U.union([U.object({id:U.string(),username:U.string(),global_name:U.union([U.string(),U.null()]).optional(),discriminator:U.string(),avatar:U.union([U.string(),U.null()]).optional(),flags:U.number(),bot:U.boolean(),avatar_decoration_data:U.union([U.object({asset:U.union([U.string(),U.null()]).optional(),skuId:U.string().optional(),expiresAt:U.number().optional()}),U.null()]).optional(),premium_type:U.union([U.number(),U.null()]).optional()}),U.null()]),so=U.object({quest_id:U.string()}).describe(`Request for "GET_QUEST_ENROLLMENT_STATUS" Command`),co=U.object({quest_id:U.string(),is_enrolled:U.boolean(),enrolled_at:U.union([U.string(),U.null()]).optional()}).describe(`Response for "GET_QUEST_ENROLLMENT_STATUS" Command`),lo=U.object({quest_id:U.string()}).describe(`Request for "QUEST_START_TIMER" Command`),uo=U.object({success:U.boolean()}).describe(`Response for "QUEST_START_TIMER" Command`),fo=U.object({quest_id:U.string(),enrolled_at:U.union([U.string(),U.null()]).optional(),completed_at:U.union([U.string(),U.null()]).optional(),external_cta_url:U.string()}).describe(`Response for "GET_QUEST" Command`),po=U.object({ticket:U.string()}).describe(`Response for "REQUEST_PROXY_TICKET_REFRESH" Command`),mo;(function(e){e.INITIATE_IMAGE_UPLOAD=`INITIATE_IMAGE_UPLOAD`,e.OPEN_SHARE_MOMENT_DIALOG=`OPEN_SHARE_MOMENT_DIALOG`,e.AUTHENTICATE=`AUTHENTICATE`,e.GET_ACTIVITY_INSTANCE_CONNECTED_PARTICIPANTS=`GET_ACTIVITY_INSTANCE_CONNECTED_PARTICIPANTS`,e.SHARE_INTERACTION=`SHARE_INTERACTION`,e.SHARE_LINK=`SHARE_LINK`,e.GET_RELATIONSHIPS=`GET_RELATIONSHIPS`,e.INVITE_USER_EMBEDDED=`INVITE_USER_EMBEDDED`,e.GET_USER=`GET_USER`,e.GET_QUEST_ENROLLMENT_STATUS=`GET_QUEST_ENROLLMENT_STATUS`,e.QUEST_START_TIMER=`QUEST_START_TIMER`,e.GET_QUEST=`GET_QUEST`,e.REQUEST_PROXY_TICKET_REFRESH=`REQUEST_PROXY_TICKET_REFRESH`})(mo||={});var ho=U.object({}).optional().nullable(),go=U.void(),_o={[mo.INITIATE_IMAGE_UPLOAD]:{request:go,response:Ja},[mo.OPEN_SHARE_MOMENT_DIALOG]:{request:Ya,response:ho},[mo.AUTHENTICATE]:{request:Xa,response:Za},[mo.GET_ACTIVITY_INSTANCE_CONNECTED_PARTICIPANTS]:{request:go,response:Qa},[mo.SHARE_INTERACTION]:{request:$a,response:eo},[mo.SHARE_LINK]:{request:to,response:no},[mo.GET_RELATIONSHIPS]:{request:go,response:ro},[mo.INVITE_USER_EMBEDDED]:{request:io,response:ho},[mo.GET_USER]:{request:ao,response:oo},[mo.GET_QUEST_ENROLLMENT_STATUS]:{request:so,response:co},[mo.QUEST_START_TIMER]:{request:lo,response:uo},[mo.GET_QUEST]:{request:go,response:fo},[mo.REQUEST_PROXY_TICKET_REFRESH]:{request:go,response:po}},vo=t({Activity:()=>No,Attachment:()=>Ho,CertifiedDevice:()=>cs,CertifiedDeviceTypeObject:()=>ss,Channel:()=>Lo,ChannelMention:()=>Vo,ChannelTypesObject:()=>Io,Commands:()=>bo,DISPATCH:()=>yo,Embed:()=>Yo,EmbedAuthor:()=>qo,EmbedField:()=>Jo,EmbedFooter:()=>Uo,EmbedProvider:()=>Ko,Emoji:()=>Oo,Entitlement:()=>fs,EntitlementTypesObject:()=>ds,Guild:()=>Bo,GuildMember:()=>Eo,GuildMemberRPC:()=>Do,Image:()=>Wo,KeyTypesObject:()=>ns,LayoutMode:()=>bs,LayoutModeTypeObject:()=>ys,Message:()=>es,MessageActivity:()=>Zo,MessageApplication:()=>Qo,MessageReference:()=>$o,Orientation:()=>vs,OrientationLockState:()=>ms,OrientationLockStateTypeObject:()=>ps,OrientationTypeObject:()=>_s,PermissionOverwrite:()=>Fo,PermissionOverwriteTypeObject:()=>Po,PresenceUpdate:()=>Ro,Reaction:()=>Xo,ReceiveFramePayload:()=>xo,Relationship:()=>wo,Role:()=>zo,Scopes:()=>Co,ScopesObject:()=>So,ShortcutKey:()=>rs,Sku:()=>us,SkuTypeObject:()=>ls,Status:()=>Mo,StatusObject:()=>jo,ThermalState:()=>gs,ThermalStateTypeObject:()=>hs,User:()=>To,UserVoiceState:()=>Ao,Video:()=>Go,VoiceDevice:()=>ts,VoiceSettingModeTypeObject:()=>is,VoiceSettingsIO:()=>os,VoiceSettingsMode:()=>as,VoiceState:()=>ko}),yo=`DISPATCH`,bo;(function(e){e.AUTHORIZE=`AUTHORIZE`,e.GET_GUILDS=`GET_GUILDS`,e.GET_GUILD=`GET_GUILD`,e.GET_CHANNEL=`GET_CHANNEL`,e.GET_CHANNELS=`GET_CHANNELS`,e.SELECT_VOICE_CHANNEL=`SELECT_VOICE_CHANNEL`,e.SELECT_TEXT_CHANNEL=`SELECT_TEXT_CHANNEL`,e.SUBSCRIBE=`SUBSCRIBE`,e.UNSUBSCRIBE=`UNSUBSCRIBE`,e.CAPTURE_SHORTCUT=`CAPTURE_SHORTCUT`,e.SET_CERTIFIED_DEVICES=`SET_CERTIFIED_DEVICES`,e.SET_ACTIVITY=`SET_ACTIVITY`,e.GET_SKUS=`GET_SKUS`,e.GET_ENTITLEMENTS=`GET_ENTITLEMENTS`,e.GET_SKUS_EMBEDDED=`GET_SKUS_EMBEDDED`,e.GET_ENTITLEMENTS_EMBEDDED=`GET_ENTITLEMENTS_EMBEDDED`,e.START_PURCHASE=`START_PURCHASE`,e.SET_CONFIG=`SET_CONFIG`,e.SEND_ANALYTICS_EVENT=`SEND_ANALYTICS_EVENT`,e.USER_SETTINGS_GET_LOCALE=`USER_SETTINGS_GET_LOCALE`,e.OPEN_EXTERNAL_LINK=`OPEN_EXTERNAL_LINK`,e.ENCOURAGE_HW_ACCELERATION=`ENCOURAGE_HW_ACCELERATION`,e.CAPTURE_LOG=`CAPTURE_LOG`,e.SET_ORIENTATION_LOCK_STATE=`SET_ORIENTATION_LOCK_STATE`,e.OPEN_INVITE_DIALOG=`OPEN_INVITE_DIALOG`,e.GET_PLATFORM_BEHAVIORS=`GET_PLATFORM_BEHAVIORS`,e.GET_CHANNEL_PERMISSIONS=`GET_CHANNEL_PERMISSIONS`,e.AUTHENTICATE=`AUTHENTICATE`,e.GET_ACTIVITY_INSTANCE_CONNECTED_PARTICIPANTS=`GET_ACTIVITY_INSTANCE_CONNECTED_PARTICIPANTS`,e.GET_QUEST=`GET_QUEST`,e.GET_QUEST_ENROLLMENT_STATUS=`GET_QUEST_ENROLLMENT_STATUS`,e.GET_RELATIONSHIPS=`GET_RELATIONSHIPS`,e.GET_USER=`GET_USER`,e.INITIATE_IMAGE_UPLOAD=`INITIATE_IMAGE_UPLOAD`,e.INVITE_USER_EMBEDDED=`INVITE_USER_EMBEDDED`,e.OPEN_SHARE_MOMENT_DIALOG=`OPEN_SHARE_MOMENT_DIALOG`,e.QUEST_START_TIMER=`QUEST_START_TIMER`,e.REQUEST_PROXY_TICKET_REFRESH=`REQUEST_PROXY_TICKET_REFRESH`,e.SHARE_INTERACTION=`SHARE_INTERACTION`,e.SHARE_LINK=`SHARE_LINK`})(bo||={});var xo=H({cmd:V(),data:Ki(),evt:Wi(),nonce:V()}).passthrough(),So=Object.assign(Object.assign({},Za.shape.scopes.element.overlayType._def.innerType.options[0].Values),{UNHANDLED:-1}),Co=Ka(So),wo=ro.shape.relationships.element,To=H({id:V(),username:V(),discriminator:V(),global_name:V().optional().nullable(),avatar:V().optional().nullable(),avatar_decoration_data:H({asset:V(),sku_id:V().optional()}).nullable(),bot:Bi(),flags:Li().optional().nullable(),premium_type:Li().optional().nullable()}),Eo=H({user:To,nick:V().optional().nullable(),roles:Yi(V()),joined_at:V(),deaf:Bi(),mute:Bi()}),Do=H({user_id:V(),nick:V().optional().nullable(),guild_id:V(),avatar:V().optional().nullable(),avatar_decoration_data:H({asset:V(),sku_id:V().optional().nullable()}).optional().nullable(),color_string:V().optional().nullable()}),Oo=H({id:V(),name:V().optional().nullable(),roles:Yi(V()).optional().nullable(),user:To.optional().nullable(),require_colons:Bi().optional().nullable(),managed:Bi().optional().nullable(),animated:Bi().optional().nullable(),available:Bi().optional().nullable()}),ko=H({mute:Bi(),deaf:Bi(),self_mute:Bi(),self_deaf:Bi(),suppress:Bi()}),Ao=H({mute:Bi(),nick:V(),user:To,voice_state:ko,volume:Li()}),jo={UNHANDLED:-1,IDLE:`idle`,DND:`dnd`,ONLINE:`online`,OFFLINE:`offline`},Mo=Ka(jo),No=H({name:V(),type:Li(),url:V().optional().nullable(),created_at:Li().optional().nullable(),timestamps:H({start:Li(),end:Li()}).partial().optional().nullable(),application_id:V().optional().nullable(),details:V().optional().nullable(),details_url:V().url().optional().nullable(),state:V().optional().nullable(),state_url:V().url().optional().nullable(),emoji:Oo.optional().nullable(),party:H({id:V().optional().nullable(),size:Yi(Li()).optional().nullable()}).optional().nullable(),assets:H({large_image:V().nullable(),large_text:V().nullable(),large_url:V().url().optional().nullable(),small_image:V().nullable(),small_text:V().nullable(),small_url:V().url().optional().nullable()}).partial().optional().nullable(),secrets:H({join:V(),match:V()}).partial().optional().nullable(),instance:Bi().optional().nullable(),flags:Li().optional().nullable()}),Po={UNHANDLED:-1,ROLE:0,MEMBER:1},Fo=H({id:V(),type:Ka(Po),allow:V(),deny:V()}),Io={UNHANDLED:-1,DM:1,GROUP_DM:3,GUILD_TEXT:0,GUILD_VOICE:2,GUILD_CATEGORY:4,GUILD_ANNOUNCEMENT:5,GUILD_STORE:6,ANNOUNCEMENT_THREAD:10,PUBLIC_THREAD:11,PRIVATE_THREAD:12,GUILD_STAGE_VOICE:13,GUILD_DIRECTORY:14,GUILD_FORUM:15},Lo=H({id:V(),type:Ka(Io),guild_id:V().optional().nullable(),position:Li().optional().nullable(),permission_overwrites:Yi(Fo).optional().nullable(),name:V().optional().nullable(),topic:V().optional().nullable(),nsfw:Bi().optional().nullable(),last_message_id:V().optional().nullable(),bitrate:Li().optional().nullable(),user_limit:Li().optional().nullable(),rate_limit_per_user:Li().optional().nullable(),recipients:Yi(To).optional().nullable(),icon:V().optional().nullable(),owner_id:V().optional().nullable(),application_id:V().optional().nullable(),parent_id:V().optional().nullable(),last_pin_timestamp:V().optional().nullable()}),Ro=H({user:To,guild_id:V(),status:Mo,activities:Yi(No),client_status:H({desktop:Mo,mobile:Mo,web:Mo}).partial()}),zo=H({id:V(),name:V(),color:Li(),hoist:Bi(),position:Li(),permissions:V(),managed:Bi(),mentionable:Bi()}),Bo=H({id:V(),name:V(),owner_id:V(),icon:V().nullable(),icon_hash:V().optional().nullable(),splash:V().nullable(),discovery_splash:V().nullable(),owner:Bi().optional().nullable(),permissions:V().optional().nullable(),region:V(),afk_channel_id:V().nullable(),afk_timeout:Li(),widget_enabled:Bi().optional().nullable(),widget_channel_id:V().optional().nullable(),verification_level:Li(),default_message_notifications:Li(),explicit_content_filter:Li(),roles:Yi(zo),emojis:Yi(Oo),features:Yi(V()),mfa_level:Li(),application_id:V().nullable(),system_channel_id:V().nullable(),system_channel_flags:Li(),rules_channel_id:V().nullable(),joined_at:V().optional().nullable(),large:Bi().optional().nullable(),unavailable:Bi().optional().nullable(),member_count:Li().optional().nullable(),voice_states:Yi(ko).optional().nullable(),members:Yi(Eo).optional().nullable(),channels:Yi(Lo).optional().nullable(),presences:Yi(Ro).optional().nullable(),max_presences:Li().optional().nullable(),max_members:Li().optional().nullable(),vanity_url_code:V().nullable(),description:V().nullable(),banner:V().nullable(),premium_tier:Li(),premium_subscription_count:Li().optional().nullable(),preferred_locale:V(),public_updates_channel_id:V().nullable(),max_video_channel_users:Li().optional().nullable(),approximate_member_count:Li().optional().nullable(),approximate_presence_count:Li().optional().nullable()}),Vo=H({id:V(),guild_id:V(),type:Li(),name:V()}),Ho=H({id:V(),filename:V(),size:Li(),url:V(),proxy_url:V(),height:Li().optional().nullable(),width:Li().optional().nullable()}),Uo=H({text:V(),icon_url:V().optional().nullable(),proxy_icon_url:V().optional().nullable()}),Wo=H({url:V().optional().nullable(),proxy_url:V().optional().nullable(),height:Li().optional().nullable(),width:Li().optional().nullable()}),Go=Wo.omit({proxy_url:!0}),Ko=H({name:V().optional().nullable(),url:V().optional().nullable()}),qo=H({name:V().optional().nullable(),url:V().optional().nullable(),icon_url:V().optional().nullable(),proxy_icon_url:V().optional().nullable()}),Jo=H({name:V(),value:V(),inline:Bi()}),Yo=H({title:V().optional().nullable(),type:V().optional().nullable(),description:V().optional().nullable(),url:V().optional().nullable(),timestamp:V().optional().nullable(),color:Li().optional().nullable(),footer:Uo.optional().nullable(),image:Wo.optional().nullable(),thumbnail:Wo.optional().nullable(),video:Go.optional().nullable(),provider:Ko.optional().nullable(),author:qo.optional().nullable(),fields:Yi(Jo).optional().nullable()}),Xo=H({count:Li(),me:Bi(),emoji:Oo}),Zo=H({type:Li(),party_id:V().optional().nullable()}),Qo=H({id:V(),cover_image:V().optional().nullable(),description:V(),icon:V().optional().nullable(),name:V()}),$o=H({message_id:V().optional().nullable(),channel_id:V().optional().nullable(),guild_id:V().optional().nullable()}),es=H({id:V(),channel_id:V(),guild_id:V().optional().nullable(),author:To.optional().nullable(),member:Eo.optional().nullable(),content:V(),timestamp:V(),edited_timestamp:V().optional().nullable(),tts:Bi(),mention_everyone:Bi(),mentions:Yi(To),mention_roles:Yi(V()),mention_channels:Yi(Vo),attachments:Yi(Ho),embeds:Yi(Yo),reactions:Yi(Xo).optional().nullable(),nonce:Zi([V(),Li()]).optional().nullable(),pinned:Bi(),webhook_id:V().optional().nullable(),type:Li(),activity:Zo.optional().nullable(),application:Qo.optional().nullable(),message_reference:$o.optional().nullable(),flags:Li().optional().nullable(),stickers:Yi(Ki()).optional().nullable(),referenced_message:Ki().optional().nullable()}),ts=H({id:V(),name:V()}),ns={UNHANDLED:-1,KEYBOARD_KEY:0,MOUSE_BUTTON:1,KEYBOARD_MODIFIER_KEY:2,GAMEPAD_BUTTON:3},rs=H({type:Ka(ns),code:Li(),name:V()}),is={UNHANDLED:-1,PUSH_TO_TALK:`PUSH_TO_TALK`,VOICE_ACTIVITY:`VOICE_ACTIVITY`},as=H({type:Ka(is),auto_threshold:Bi(),threshold:Li(),shortcut:Yi(rs),delay:Li()}),os=H({device_id:V(),volume:Li(),available_devices:Yi(ts)}),ss={UNHANDLED:-1,AUDIO_INPUT:`AUDIO_INPUT`,AUDIO_OUTPUT:`AUDIO_OUTPUT`,VIDEO_INPUT:`VIDEO_INPUT`},cs=H({type:Ka(ss),id:V(),vendor:H({name:V(),url:V()}),model:H({name:V(),url:V()}),related:Yi(V()),echo_cancellation:Bi().optional().nullable(),noise_suppression:Bi().optional().nullable(),automatic_gain_control:Bi().optional().nullable(),hardware_mute:Bi().optional().nullable()}),ls={UNHANDLED:-1,APPLICATION:1,DLC:2,CONSUMABLE:3,BUNDLE:4,SUBSCRIPTION:5},us=H({id:V(),name:V(),type:Ka(ls),price:H({amount:Li(),currency:V()}),application_id:V(),flags:Li(),release_date:V().nullable()}),ds={UNHANDLED:-1,PURCHASE:1,PREMIUM_SUBSCRIPTION:2,DEVELOPER_GIFT:3,TEST_MODE_PURCHASE:4,FREE_PURCHASE:5,USER_GIFT:6,PREMIUM_PURCHASE:7},fs=H({id:V(),sku_id:V(),application_id:V(),user_id:V(),gift_code_flags:Li(),type:Ka(ds),gifter_user_id:V().optional().nullable(),branches:Yi(V()).optional().nullable(),starts_at:V().optional().nullable(),ends_at:V().optional().nullable(),parent_id:V().optional().nullable(),consumed:Bi().optional().nullable(),deleted:Bi().optional().nullable(),gift_code_batch_id:V().optional().nullable()}),ps={UNHANDLED:-1,UNLOCKED:1,PORTRAIT:2,LANDSCAPE:3},ms=Ka(ps),hs={UNHANDLED:-1,NOMINAL:0,FAIR:1,SERIOUS:2,CRITICAL:3},gs=Ka(hs),_s={UNHANDLED:-1,PORTRAIT:0,LANDSCAPE:1},vs=Ka(_s),ys={UNHANDLED:-1,FOCUSED:0,PIP:1,GRID:2},bs=Ka(ys),xs=`ERROR`,Ss;(function(e){e.READY=`READY`,e.VOICE_STATE_UPDATE=`VOICE_STATE_UPDATE`,e.SPEAKING_START=`SPEAKING_START`,e.SPEAKING_STOP=`SPEAKING_STOP`,e.ACTIVITY_LAYOUT_MODE_UPDATE=`ACTIVITY_LAYOUT_MODE_UPDATE`,e.ORIENTATION_UPDATE=`ORIENTATION_UPDATE`,e.CURRENT_USER_UPDATE=`CURRENT_USER_UPDATE`,e.CURRENT_GUILD_MEMBER_UPDATE=`CURRENT_GUILD_MEMBER_UPDATE`,e.ENTITLEMENT_CREATE=`ENTITLEMENT_CREATE`,e.THERMAL_STATE_UPDATE=`THERMAL_STATE_UPDATE`,e.ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE=`ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE`,e.RELATIONSHIP_UPDATE=`RELATIONSHIP_UPDATE`,e.ACTIVITY_JOIN=`ACTIVITY_JOIN`,e.QUEST_ENROLLMENT_STATUS_UPDATE=`QUEST_ENROLLMENT_STATUS_UPDATE`})(Ss||={});var Cs=xo.extend({evt:ca(Ss),nonce:V().nullable(),cmd:oa(yo),data:H({}).passthrough()}),ws=xo.extend({evt:oa(xs),data:H({code:Li(),message:V().optional()}).passthrough(),cmd:ca(bo),nonce:V().nullable()}),Ts=Zi([Cs,Cs.extend({evt:V()}),ws]);function Es(e){let t=e.evt;if(!(t in Ss))throw Error(`Unrecognized event type ${e.evt}`);return Ds[t].payload.parse(e)}var Ds={[Ss.READY]:{payload:Cs.extend({evt:oa(Ss.READY),data:H({v:Li(),config:H({cdn_host:V().optional(),api_endpoint:V(),environment:V()}),user:H({id:V(),username:V(),discriminator:V(),avatar:V().optional()}).optional()})})},[Ss.VOICE_STATE_UPDATE]:{payload:Cs.extend({evt:oa(Ss.VOICE_STATE_UPDATE),data:Ao}),subscribeArgs:H({channel_id:V()})},[Ss.SPEAKING_START]:{payload:Cs.extend({evt:oa(Ss.SPEAKING_START),data:H({lobby_id:V().optional(),channel_id:V().optional(),user_id:V()})}),subscribeArgs:H({lobby_id:V().nullable().optional(),channel_id:V().nullable().optional()})},[Ss.SPEAKING_STOP]:{payload:Cs.extend({evt:oa(Ss.SPEAKING_STOP),data:H({lobby_id:V().optional(),channel_id:V().optional(),user_id:V()})}),subscribeArgs:H({lobby_id:V().nullable().optional(),channel_id:V().nullable().optional()})},[Ss.ACTIVITY_LAYOUT_MODE_UPDATE]:{payload:Cs.extend({evt:oa(Ss.ACTIVITY_LAYOUT_MODE_UPDATE),data:H({layout_mode:Ka(ys)})})},[Ss.ORIENTATION_UPDATE]:{payload:Cs.extend({evt:oa(Ss.ORIENTATION_UPDATE),data:H({screen_orientation:Ka(_s),orientation:ca(Wa)})})},[Ss.CURRENT_USER_UPDATE]:{payload:Cs.extend({evt:oa(Ss.CURRENT_USER_UPDATE),data:To})},[Ss.CURRENT_GUILD_MEMBER_UPDATE]:{payload:Cs.extend({evt:oa(Ss.CURRENT_GUILD_MEMBER_UPDATE),data:Do}),subscribeArgs:H({guild_id:V()})},[Ss.ENTITLEMENT_CREATE]:{payload:Cs.extend({evt:oa(Ss.ENTITLEMENT_CREATE),data:H({entitlement:fs})})},[Ss.THERMAL_STATE_UPDATE]:{payload:Cs.extend({evt:oa(Ss.THERMAL_STATE_UPDATE),data:H({thermal_state:gs})})},[Ss.ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE]:{payload:Cs.extend({evt:oa(Ss.ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE),data:H({participants:Qa.shape.participants})})},[Ss.RELATIONSHIP_UPDATE]:{payload:Cs.extend({evt:oa(Ss.RELATIONSHIP_UPDATE),data:wo})},[Ss.ACTIVITY_JOIN]:{payload:Cs.extend({evt:oa(Ss.ACTIVITY_JOIN),data:H({applicationId:V(),secret:V()})})},[Ss.QUEST_ENROLLMENT_STATUS_UPDATE]:{payload:Cs.extend({evt:oa(Ss.QUEST_ENROLLMENT_STATUS_UPDATE),data:H({quest_id:V(),is_enrolled:Bi(),enrolled_at:V().date()})})}};function Os(e,t){throw t}var ks=H({}).nullable(),As=H({code:V()}),js=H({guilds:Yi(H({id:V(),name:V()}))}),Ms=H({id:V(),name:V(),icon_url:V().optional(),members:Yi(Eo)}),Ns=H({id:V(),type:Ka(Io),guild_id:V().optional().nullable(),name:V().optional().nullable(),topic:V().optional().nullable(),bitrate:Li().optional().nullable(),user_limit:Li().optional().nullable(),position:Li().optional().nullable(),voice_states:Yi(Ao),messages:Yi(es)}),Ps=H({channels:Yi(Lo)});Ns.nullable();var Fs=Ns.nullable(),Is=Ns.nullable();H({input:os,output:os,mode:as,automatic_gain_control:Bi(),echo_cancellation:Bi(),noise_suppression:Bi(),qos:Bi(),silence_warning:Bi(),deaf:Bi(),mute:Bi()});var Ls=H({evt:V()}),Rs=H({shortcut:rs}),zs=No,Bs=H({skus:Yi(us)}),Vs=H({entitlements:Yi(fs)}),Hs=Yi(fs).nullable(),Us=H({use_interactive_pip:Bi()}),Ws=H({locale:V()}),Gs=H({enabled:Bi()}),Ks=H({permissions:zi().or(V())}),qs=qa(H({opened:Bi().or(Wi())}).default({opened:null})),Js=H({iosKeyboardResizesView:da(Bi())}),Ys=xo.extend({cmd:ca(bo),evt:Wi()});function Xs({cmd:e,data:t}){switch(e){case bo.AUTHORIZE:return As.parse(t);case bo.CAPTURE_SHORTCUT:return Rs.parse(t);case bo.ENCOURAGE_HW_ACCELERATION:return Gs.parse(t);case bo.GET_CHANNEL:return Ns.parse(t);case bo.GET_CHANNELS:return Ps.parse(t);case bo.GET_CHANNEL_PERMISSIONS:return Ks.parse(t);case bo.GET_GUILD:return Ms.parse(t);case bo.GET_GUILDS:return js.parse(t);case bo.GET_PLATFORM_BEHAVIORS:return Js.parse(t);case bo.GET_CHANNEL:return Ns.parse(t);case bo.SELECT_TEXT_CHANNEL:return Is.parse(t);case bo.SELECT_VOICE_CHANNEL:return Fs.parse(t);case bo.SET_ACTIVITY:return zs.parse(t);case bo.GET_SKUS_EMBEDDED:return Bs.parse(t);case bo.GET_ENTITLEMENTS_EMBEDDED:return Vs.parse(t);case bo.SET_CONFIG:return Us.parse(t);case bo.START_PURCHASE:return Hs.parse(t);case bo.SUBSCRIBE:case bo.UNSUBSCRIBE:return Ls.parse(t);case bo.USER_SETTINGS_GET_LOCALE:return Ws.parse(t);case bo.OPEN_EXTERNAL_LINK:return qs.parse(t);case bo.SET_ORIENTATION_LOCK_STATE:case bo.SET_CERTIFIED_DEVICES:case bo.SEND_ANALYTICS_EVENT:case bo.OPEN_INVITE_DIALOG:case bo.CAPTURE_LOG:case bo.GET_SKUS:case bo.GET_ENTITLEMENTS:return ks.parse(t);case bo.AUTHENTICATE:case bo.GET_ACTIVITY_INSTANCE_CONNECTED_PARTICIPANTS:case bo.GET_QUEST:case bo.GET_QUEST_ENROLLMENT_STATUS:case bo.GET_RELATIONSHIPS:case bo.GET_USER:case bo.INITIATE_IMAGE_UPLOAD:case bo.INVITE_USER_EMBEDDED:case bo.OPEN_SHARE_MOMENT_DIALOG:case bo.QUEST_START_TIMER:case bo.REQUEST_PROXY_TICKET_REFRESH:case bo.SHARE_INTERACTION:case bo.SHARE_LINK:let{response:n}=_o[e];return n.parse(t);default:Os(e,Error(`Unrecognized command ${e}`))}}function Zs(e){return Object.assign(Object.assign({},e),{data:Xs(e)})}H({frame_id:V(),platform:ca(Ga).optional().nullable()}),H({v:oa(1),encoding:oa(`json`).optional(),client_id:V(),frame_id:V()});var Qs=H({code:Li(),message:V().optional()}),$s=H({evt:V().nullable(),nonce:V().nullable(),data:Ki().nullable(),cmd:V()}).passthrough();function ec(e){let t=$s.parse(e);return t.evt==null?Zs(Ys.passthrough().parse(t)):t.evt===`ERROR`?ws.parse(t):Es(Ts.parse(t))}function tc(e,t,n,r=()=>void 0){let i=xo.extend({cmd:oa(t),data:n});return async n=>{let a=await e({cmd:t,args:n,transfer:r(n)});return i.parse(a).data}}function nc(e,t=()=>void 0){let n=_o[e].response,r=xo.extend({cmd:oa(e),data:n});return n=>async i=>{let a=await n({cmd:e,args:i,transfer:t(i)});return r.parse(a).data}}var rc=e=>tc(e,bo.AUTHORIZE,As),ic=e=>tc(e,bo.CAPTURE_LOG,ks),ac=e=>tc(e,bo.ENCOURAGE_HW_ACCELERATION,Gs),oc=e=>tc(e,bo.GET_CHANNEL,Ns),sc=e=>tc(e,bo.GET_ENTITLEMENTS_EMBEDDED,Vs),cc=e=>tc(e,bo.GET_SKUS_EMBEDDED,Bs),lc=e=>tc(e,bo.GET_CHANNEL_PERMISSIONS,Ks),uc=e=>tc(e,bo.GET_PLATFORM_BEHAVIORS,Js),dc=e=>tc(e,bo.OPEN_EXTERNAL_LINK,qs),fc=e=>tc(e,bo.OPEN_INVITE_DIALOG,ks);No.pick({state:!0,state_url:!0,details:!0,details_url:!0,timestamps:!0,assets:!0,party:!0,secrets:!0,instance:!0,type:!0}).extend({type:No.shape.type.optional(),instance:No.shape.instance.optional()}).nullable();var pc=e=>tc(e,bo.SET_ACTIVITY,zs),mc=e=>tc(e,bo.SET_CONFIG,Us);function hc({sendCommand:e,cmd:t,response:n,fallbackTransform:r,transferTransform:i=()=>void 0}){let a=xo.extend({cmd:oa(t),data:n});return async n=>{try{let r=await e({cmd:t,args:n,transfer:i(n)});return a.parse(r).data}catch(o){if(o.code===Ua.INVALID_PAYLOAD){let o=r(n),s=await e({cmd:t,args:o,transfer:i(o)});return a.parse(s).data}else throw o}}}var gc=e=>({lock_state:e.lock_state,picture_in_picture_lock_state:e.picture_in_picture_lock_state}),_c=e=>hc({sendCommand:e,cmd:bo.SET_ORIENTATION_LOCK_STATE,response:ks,fallbackTransform:gc}),vc=e=>tc(e,bo.START_PURCHASE,Hs),yc=e=>tc(e,bo.USER_SETTINGS_GET_LOCALE,Ws),bc=nc(mo.AUTHENTICATE),xc=nc(mo.GET_ACTIVITY_INSTANCE_CONNECTED_PARTICIPANTS),Sc=nc(mo.GET_QUEST),Cc=nc(mo.GET_QUEST_ENROLLMENT_STATUS),wc=nc(mo.GET_RELATIONSHIPS),Tc=nc(mo.GET_USER),Ec=nc(mo.INITIATE_IMAGE_UPLOAD),Dc=nc(mo.INVITE_USER_EMBEDDED),Oc=nc(mo.OPEN_SHARE_MOMENT_DIALOG),kc=nc(mo.QUEST_START_TIMER),Ac=nc(mo.REQUEST_PROXY_TICKET_REFRESH),jc=nc(mo.SHARE_INTERACTION),Mc=nc(mo.SHARE_LINK);function Nc(e){return{authorize:rc(e),captureLog:ic(e),encourageHardwareAcceleration:ac(e),getChannel:oc(e),getChannelPermissions:lc(e),getEntitlements:sc(e),getPlatformBehaviors:uc(e),getSkus:cc(e),openExternalLink:dc(e),openInviteDialog:fc(e),setActivity:pc(e),setConfig:mc(e),setOrientationLockState:_c(e),startPurchase:vc(e),userSettingsGetLocale:yc(e),getInstanceConnectedParticipants:xc(e),authenticate:bc(e),getActivityInstanceConnectedParticipants:xc(e),getQuest:Sc(e),getQuestEnrollmentStatus:Cc(e),getRelationships:wc(e),getUser:Tc(e),initiateImageUpload:Ec(e),inviteUserEmbedded:Dc(e),openShareMomentDialog:Oc(e),questStartTimer:kc(e),requestProxyTicketRefresh:Ac(e),shareInteraction:jc(e),shareLink:Mc(e)}}var Pc=class extends Error{constructor(e,t=``){super(t),this.code=e,this.message=t,this.name=`Discord SDK Error`}};function Fc(){return{disableConsoleLogOverride:!1}}var Ic=[`log`,`warn`,`debug`,`info`,`error`];function Lc(e,t,n){let r=e[t],i=e;r&&(e[t]=function(){let e=[].slice.call(arguments);n(t,``+e.join(` `)),r.apply(i,e)})}var Rc=`2.5.0`,zc={randomUUID:typeof crypto<`u`&&crypto.randomUUID&&crypto.randomUUID.bind(crypto)},Bc,Vc=new Uint8Array(16);function Hc(){if(!Bc){if(typeof crypto>`u`||!crypto.getRandomValues)throw Error(`crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported`);Bc=crypto.getRandomValues.bind(crypto)}return Bc(Vc)}var Uc=[];for(let e=0;e<256;++e)Uc.push((e+256).toString(16).slice(1));function Wc(e,t=0){return(Uc[e[t+0]]+Uc[e[t+1]]+Uc[e[t+2]]+Uc[e[t+3]]+`-`+Uc[e[t+4]]+Uc[e[t+5]]+`-`+Uc[e[t+6]]+Uc[e[t+7]]+`-`+Uc[e[t+8]]+Uc[e[t+9]]+`-`+Uc[e[t+10]]+Uc[e[t+11]]+Uc[e[t+12]]+Uc[e[t+13]]+Uc[e[t+14]]+Uc[e[t+15]]).toLowerCase()}function Gc(e,t,n){if(zc.randomUUID&&!t&&!e)return zc.randomUUID();e||={};let r=e.random??e.rng?.()??Hc();if(r.length<16)throw Error(`Random bytes length must be >= 16`);return r[6]=r[6]&15|64,r[8]=r[8]&63|128,Wc(r)}var Kc;(function(e){e[e.HANDSHAKE=0]=`HANDSHAKE`,e[e.FRAME=1]=`FRAME`,e[e.CLOSE=2]=`CLOSE`,e[e.HELLO=3]=`HELLO`})(Kc||={});var qc=new Set(Jc());function Jc(){return typeof window>`u`?[]:[window.location.origin,`https://discord.com`,`https://discordapp.com`,`https://ptb.discord.com`,`https://ptb.discordapp.com`,`https://canary.discord.com`,`https://canary.discordapp.com`,`https://staging.discord.co`,`http://localhost:3333`,`https://pax.discord.com`,`null`]}function Yc(){return[window.parent.opener??window.parent,document.referrer?document.referrer:`*`]}var Xc=class{getTransfer(e){switch(e.cmd){case bo.SUBSCRIBE:case bo.UNSUBSCRIBE:return;default:return e.transfer??void 0}}constructor(e,t){if(this.sdkVersion=Rc,this.mobileAppVersion=null,this.source=null,this.sourceOrigin=``,this.eventBus=new Qn,this.pendingCommands=new Map,this.sendCommand=e=>{var t;if(this.source==null)throw Error(`Attempting to send message before initialization`);let n=Gc();return(t=this.source)==null||t.postMessage([Kc.FRAME,Object.assign(Object.assign({},e),{nonce:n})],this.sourceOrigin,this.getTransfer(e)),new Promise((e,t)=>{this.pendingCommands.set(n,{resolve:e,reject:t})})},this.commands=Nc(this.sendCommand),this.handleMessage=e=>{if(!qc.has(e.origin))return;let t=e.data;if(!Array.isArray(t))return;let[n,r]=t;switch(n){case Kc.HELLO:return;case Kc.CLOSE:return this.handleClose(r);case Kc.HANDSHAKE:return this.handleHandshake();case Kc.FRAME:return this.handleFrame(r);default:throw Error(`Invalid message format`)}},this.isReady=!1,this.clientId=e,this.configuration=t??Fc(),typeof window<`u`&&window.addEventListener(`message`,this.handleMessage),typeof window>`u`){this.frameId=``,this.instanceId=``,this.customId=null,this.referrerId=null,this.platform=Ga.DESKTOP,this.guildId=null,this.channelId=null,this.locationId=null;return}let n=new URLSearchParams(this._getSearch()),r=n.get(`frame_id`);if(!r)throw Error(`frame_id query param is not defined`);this.frameId=r;let i=n.get(`instance_id`);if(!i)throw Error(`instance_id query param is not defined`);this.instanceId=i;let a=n.get(`platform`);if(!a)throw Error(`platform query param is not defined`);if(a!==Ga.DESKTOP&&a!==Ga.MOBILE)throw Error(`Invalid query param "platform" of "${a}". Valid values are "${Ga.DESKTOP}" or "${Ga.MOBILE}"`);this.platform=a,this.customId=n.get(`custom_id`),this.referrerId=n.get(`referrer_id`),this.guildId=n.get(`guild_id`),this.channelId=n.get(`channel_id`),this.locationId=n.get(`location_id`),this.mobileAppVersion=n.get(`mobile_app_version`),[this.source,this.sourceOrigin]=Yc(),this.addOnReadyListener(),this.handshake()}close(e,t){var n;window.removeEventListener(`message`,this.handleMessage);let r=Gc();(n=this.source)==null||n.postMessage([Kc.CLOSE,{code:e,message:t,nonce:r}],this.sourceOrigin)}async subscribe(e,t,...n){let[r]=n,i=this.eventBus.listenerCount(e),a=this.eventBus.on(e,t);return Object.values(Ss).includes(e)&&e!==Ss.READY&&i===0&&await this.sendCommand({cmd:bo.SUBSCRIBE,args:r,evt:e}),a}async unsubscribe(e,t,...n){let[r]=n;return e!==Ss.READY&&this.eventBus.listenerCount(e)===1&&await this.sendCommand({cmd:bo.UNSUBSCRIBE,evt:e,args:r}),this.eventBus.off(e,t)}async ready(){this.isReady||await new Promise(e=>{this.eventBus.once(Ss.READY,e)})}parseMajorMobileVersion(){if(this.mobileAppVersion&&this.mobileAppVersion.includes(`.`))try{return parseInt(this.mobileAppVersion.split(`.`)[0])}catch{return-1}return-1}handshake(){var e;let t={v:1,encoding:`json`,client_id:this.clientId,frame_id:this.frameId},n=this.parseMajorMobileVersion();(this.platform===Ga.DESKTOP||n>=250)&&(t.sdk_version=this.sdkVersion),(e=this.source)==null||e.postMessage([Kc.HANDSHAKE,t],this.sourceOrigin)}addOnReadyListener(){this.eventBus.once(Ss.READY,()=>{this.overrideConsoleLogging(),this.isReady=!0})}overrideConsoleLogging(){if(this.configuration.disableConsoleLogOverride)return;let e=(e,t)=>{this.commands.captureLog({level:e,message:t})};Ic.forEach(t=>{Lc(console,t,e)})}handleClose(e){Qs.parse(e)}handleHandshake(){}handleFrame(e){var t,n;let r;try{r=ec(e)}catch(t){console.error(`Failed to parse`,e),console.error(t);return}if(r.cmd===`DISPATCH`)this.eventBus.emit(r.evt,r.data);else{if(r.evt===`ERROR`){if(r.nonce!=null){(t=this.pendingCommands.get(r.nonce))==null||t.reject(r.data),this.pendingCommands.delete(r.nonce);return}this.eventBus.emit(`error`,new Pc(r.data.code,r.data.message))}if(r.nonce==null){console.error(`Missing nonce`,e);return}(n=this.pendingCommands.get(r.nonce))==null||n.resolve(r),this.pendingCommands.delete(r.nonce)}}_getSearch(){return typeof window>`u`?``:window.location.search}},Zc=1e9,Qc={precision:20,rounding:4,toExpNeg:-7,toExpPos:21,LN10:`2.302585092994045684017991454684364207601101488628772976033327900967572609677352480235997205089598298341967784042286`},$c=!0,el=`[DecimalError] `,tl=el+`Invalid argument: `,nl=el+`Exponent out of range: `,rl=Math.floor,il=Math.pow,al=/^(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i,ol,sl=1e7,cl=7,ll=9007199254740991,ul=rl(ll/cl),W={};W.absoluteValue=W.abs=function(){var e=new this.constructor(this);return e.s&&=1,e},W.comparedTo=W.cmp=function(e){var t,n,r,i,a=this;if(e=new a.constructor(e),a.s!==e.s)return a.s||-e.s;if(a.e!==e.e)return a.e>e.e^a.s<0?1:-1;for(r=a.d.length,i=e.d.length,t=0,n=re.d[t]^a.s<0?1:-1;return r===i?0:r>i^a.s<0?1:-1},W.decimalPlaces=W.dp=function(){var e=this,t=e.d.length-1,n=(t-e.e)*cl;if(t=e.d[t],t)for(;t%10==0;t/=10)n--;return n<0?0:n},W.dividedBy=W.div=function(e){return ml(this,new this.constructor(e))},W.dividedToIntegerBy=W.idiv=function(e){var t=this,n=t.constructor;return xl(ml(t,new n(e),0,1),n.precision)},W.equals=W.eq=function(e){return!this.cmp(e)},W.exponent=function(){return gl(this)},W.greaterThan=W.gt=function(e){return this.cmp(e)>0},W.greaterThanOrEqualTo=W.gte=function(e){return this.cmp(e)>=0},W.isInteger=W.isint=function(){return this.e>this.d.length-2},W.isNegative=W.isneg=function(){return this.s<0},W.isPositive=W.ispos=function(){return this.s>0},W.isZero=function(){return this.s===0},W.lessThan=W.lt=function(e){return this.cmp(e)<0},W.lessThanOrEqualTo=W.lte=function(e){return this.cmp(e)<1},W.logarithm=W.log=function(e){var t,n=this,r=n.constructor,i=r.precision,a=i+5;if(e===void 0)e=new r(10);else if(e=new r(e),e.s<1||e.eq(ol))throw Error(el+`NaN`);if(n.s<1)throw Error(el+(n.s?`NaN`:`-Infinity`));return n.eq(ol)?new r(0):($c=!1,t=ml(yl(n,a),yl(e,a),a),$c=!0,xl(t,i))},W.minus=W.sub=function(e){var t=this;return e=new t.constructor(e),t.s==e.s?Sl(t,e):dl(t,(e.s=-e.s,e))},W.modulo=W.mod=function(e){var t,n=this,r=n.constructor,i=r.precision;if(e=new r(e),!e.s)throw Error(el+`NaN`);return n.s?($c=!1,t=ml(n,e,0,1).times(e),$c=!0,n.minus(t)):xl(new r(n),i)},W.naturalExponential=W.exp=function(){return hl(this)},W.naturalLogarithm=W.ln=function(){return yl(this)},W.negated=W.neg=function(){var e=new this.constructor(this);return e.s=-e.s||0,e},W.plus=W.add=function(e){var t=this;return e=new t.constructor(e),t.s==e.s?dl(t,e):Sl(t,(e.s=-e.s,e))},W.precision=W.sd=function(e){var t,n,r,i=this;if(e!==void 0&&e!==!!e&&e!==1&&e!==0)throw Error(tl+e);if(t=gl(i)+1,r=i.d.length-1,n=r*cl+1,r=i.d[r],r){for(;r%10==0;r/=10)n--;for(r=i.d[0];r>=10;r/=10)n++}return e&&t>n?t:n},W.squareRoot=W.sqrt=function(){var e,t,n,r,i,a,o,s=this,c=s.constructor;if(s.s<1){if(!s.s)return new c(0);throw Error(el+`NaN`)}for(e=gl(s),$c=!1,i=Math.sqrt(+s),i==0||i==1/0?(t=pl(s.d),(t.length+e)%2==0&&(t+=`0`),i=Math.sqrt(t),e=rl((e+1)/2)-(e<0||e%2),i==1/0?t=`5e`+e:(t=i.toExponential(),t=t.slice(0,t.indexOf(`e`)+1)+e),r=new c(t)):r=new c(i.toString()),n=c.precision,i=o=n+3;;)if(a=r,r=a.plus(ml(s,a,o+2)).times(.5),pl(a.d).slice(0,o)===(t=pl(r.d)).slice(0,o)){if(t=t.slice(o-3,o+1),i==o&&t==`4999`){if(xl(a,n+1,0),a.times(a).eq(s)){r=a;break}}else if(t!=`9999`)break;o+=4}return $c=!0,xl(r,n)},W.times=W.mul=function(e){var t,n,r,i,a,o,s,c,l,u=this,d=u.constructor,f=u.d,p=(e=new d(e)).d;if(!u.s||!e.s)return new d(0);for(e.s*=u.s,n=u.e+e.e,c=f.length,l=p.length,c=0;){for(t=0,i=c+r;i>r;)s=a[i]+p[r]*f[i-r-1]+t,a[i--]=s%sl|0,t=s/sl|0;a[i]=(a[i]+t)%sl|0}for(;!a[--o];)a.pop();return t?++n:a.shift(),e.d=a,e.e=n,$c?xl(e,d.precision):e},W.toDecimalPlaces=W.todp=function(e,t){var n=this,r=n.constructor;return n=new r(n),e===void 0?n:(fl(e,0,Zc),t===void 0?t=r.rounding:fl(t,0,8),xl(n,e+gl(n)+1,t))},W.toExponential=function(e,t){var n,r=this,i=r.constructor;return e===void 0?n=Cl(r,!0):(fl(e,0,Zc),t===void 0?t=i.rounding:fl(t,0,8),r=xl(new i(r),e+1,t),n=Cl(r,!0,e+1)),n},W.toFixed=function(e,t){var n,r,i=this,a=i.constructor;return e===void 0?Cl(i):(fl(e,0,Zc),t===void 0?t=a.rounding:fl(t,0,8),r=xl(new a(i),e+gl(i)+1,t),n=Cl(r.abs(),!1,e+gl(r)+1),i.isneg()&&!i.isZero()?`-`+n:n)},W.toInteger=W.toint=function(){var e=this,t=e.constructor;return xl(new t(e),gl(e)+1,t.rounding)},W.toNumber=function(){return+this},W.toPower=W.pow=function(e){var t,n,r,i,a,o,s=this,c=s.constructor,l=12,u=+(e=new c(e));if(!e.s)return new c(ol);if(s=new c(s),!s.s){if(e.s<1)throw Error(el+`Infinity`);return s}if(s.eq(ol))return s;if(r=c.precision,e.eq(ol))return xl(s,r);if(t=e.e,n=e.d.length-1,o=t>=n,a=s.s,!o){if(a<0)throw Error(el+`NaN`)}else if((n=u<0?-u:u)<=ll){for(i=new c(ol),t=Math.ceil(r/cl+4),$c=!1;n%2&&(i=i.times(s),wl(i.d,t)),n=rl(n/2),n!==0;)s=s.times(s),wl(s.d,t);return $c=!0,e.s<0?new c(ol).div(i):xl(i,r)}return a=a<0&&e.d[Math.max(t,n)]&1?-1:1,s.s=1,$c=!1,i=e.times(yl(s,r+l)),$c=!0,i=hl(i),i.s=a,i},W.toPrecision=function(e,t){var n,r,i=this,a=i.constructor;return e===void 0?(n=gl(i),r=Cl(i,n<=a.toExpNeg||n>=a.toExpPos)):(fl(e,1,Zc),t===void 0?t=a.rounding:fl(t,0,8),i=xl(new a(i),e,t),n=gl(i),r=Cl(i,e<=n||n<=a.toExpNeg,e)),r},W.toSignificantDigits=W.tosd=function(e,t){var n=this,r=n.constructor;return e===void 0?(e=r.precision,t=r.rounding):(fl(e,1,Zc),t===void 0?t=r.rounding:fl(t,0,8)),xl(new r(n),e,t)},W.toString=W.valueOf=W.val=W.toJSON=W[Symbol.for(`nodejs.util.inspect.custom`)]=function(){var e=this,t=gl(e),n=e.constructor;return Cl(e,t<=n.toExpNeg||t>=n.toExpPos)};function dl(e,t){var n,r,i,a,o,s,c,l,u=e.constructor,d=u.precision;if(!e.s||!t.s)return t.s||(t=new u(e)),$c?xl(t,d):t;if(c=e.d,l=t.d,o=e.e,i=t.e,c=c.slice(),a=o-i,a){for(a<0?(r=c,a=-a,s=l.length):(r=l,i=o,s=c.length),o=Math.ceil(d/cl),s=o>s?o+1:s+1,a>s&&(a=s,r.length=1),r.reverse();a--;)r.push(0);r.reverse()}for(s=c.length,a=l.length,s-a<0&&(a=s,r=l,l=c,c=r),n=0;a;)n=(c[--a]=c[a]+l[a]+n)/sl|0,c[a]%=sl;for(n&&(c.unshift(n),++i),s=c.length;c[--s]==0;)c.pop();return t.d=c,t.e=i,$c?xl(t,d):t}function fl(e,t,n){if(e!==~~e||en)throw Error(tl+e)}function pl(e){var t,n,r,i=e.length-1,a=``,o=e[0];if(i>0){for(a+=o,t=1;tr?1:-1;else for(i=a=0;it[i]?1:-1;break}return a}function n(e,t,n){for(var r=0;n--;)e[n]-=r,r=+(e[n]1;)e.shift()}return function(r,i,a,o){var s,c,l,u,d,f,p,m,h,g,_,v,y,b,x,S,C,w,T=r.constructor,E=r.s==i.s?1:-1,D=r.d,O=i.d;if(!r.s)return new T(r);if(!i.s)throw Error(el+`Division by zero`);for(c=r.e-i.e,C=O.length,x=D.length,p=new T(E),m=p.d=[],l=0;O[l]==(D[l]||0);)++l;if(O[l]>(D[l]||0)&&--c,v=a==null?a=T.precision:o?a+(gl(r)-gl(i))+1:a,v<0)return new T(0);if(v=v/cl+2|0,l=0,C==1)for(u=0,O=O[0],v++;(l1&&(O=e(O,u),D=e(D,u),C=O.length,x=D.length),b=C,h=D.slice(0,C),g=h.length;g=sl/2&&++S;do u=0,s=t(O,h,C,g),s<0?(_=h[0],C!=g&&(_=_*sl+(h[1]||0)),u=_/S|0,u>1?(u>=sl&&(u=sl-1),d=e(O,u),f=d.length,g=h.length,s=t(d,h,f,g),s==1&&(u--,n(d,C16)throw Error(nl+gl(e));if(!e.s)return new u(ol);for(t==null?($c=!1,s=d):s=t,o=new u(.03125);e.abs().gte(.1);)e=e.times(o),l+=5;for(r=Math.log(il(2,l))/Math.LN10*2+5|0,s+=r,n=i=a=new u(ol),u.precision=s;;){if(i=xl(i.times(e),s),n=n.times(++c),o=a.plus(ml(i,n,s)),pl(o.d).slice(0,s)===pl(a.d).slice(0,s)){for(;l--;)a=xl(a.times(a),s);return u.precision=d,t==null?($c=!0,xl(a,d)):a}a=o}}function gl(e){for(var t=e.e*cl,n=e.d[0];n>=10;n/=10)t++;return t}function _l(e,t,n){if(t>e.LN10.sd())throw $c=!0,n&&(e.precision=n),Error(el+`LN10 precision limit exceeded`);return xl(new e(e.LN10),t)}function vl(e){for(var t=``;e--;)t+=`0`;return t}function yl(e,t){var n,r,i,a,o,s,c,l,u,d=1,f=10,p=e,m=p.d,h=p.constructor,g=h.precision;if(p.s<1)throw Error(el+(p.s?`NaN`:`-Infinity`));if(p.eq(ol))return new h(0);if(t==null?($c=!1,l=g):l=t,p.eq(10))return t??($c=!0),_l(h,l);if(l+=f,h.precision=l,n=pl(m),r=n.charAt(0),a=gl(p),Math.abs(a)<0x5543df729c000){for(;r<7&&r!=1||r==1&&n.charAt(1)>3;)p=p.times(e),n=pl(p.d),r=n.charAt(0),d++;a=gl(p),r>1?(p=new h(`0.`+n),a++):p=new h(r+`.`+n.slice(1))}else return c=_l(h,l+2,g).times(a+``),p=yl(new h(r+`.`+n.slice(1)),l-f).plus(c),h.precision=g,t==null?($c=!0,xl(p,g)):p;for(s=o=p=ml(p.minus(ol),p.plus(ol),l),u=xl(p.times(p),l),i=3;;){if(o=xl(o.times(u),l),c=s.plus(ml(o,new h(i),l)),pl(c.d).slice(0,l)===pl(s.d).slice(0,l))return s=s.times(2),a!==0&&(s=s.plus(_l(h,l+2,g).times(a+``))),s=ml(s,new h(d),l),h.precision=g,t==null?($c=!0,xl(s,g)):s;s=c,i+=2}}function bl(e,t){var n,r,i;for((n=t.indexOf(`.`))>-1&&(t=t.replace(`.`,``)),(r=t.search(/e/i))>0?(n<0&&(n=r),n+=+t.slice(r+1),t=t.substring(0,r)):n<0&&(n=t.length),r=0;t.charCodeAt(r)===48;)++r;for(i=t.length;t.charCodeAt(i-1)===48;)--i;if(t=t.slice(r,i),t){if(i-=r,n=n-r-1,e.e=rl(n/cl),e.d=[],r=(n+1)%cl,n<0&&(r+=cl),rul||e.e<-ul))throw Error(nl+n)}else e.s=0,e.e=0,e.d=[0];return e}function xl(e,t,n){var r,i,a,o,s,c,l,u,d=e.d;for(o=1,a=d[0];a>=10;a/=10)o++;if(r=t-o,r<0)r+=cl,i=t,l=d[u=0];else{if(u=Math.ceil((r+1)/cl),a=d.length,u>=a)return e;for(l=a=d[u],o=1;a>=10;a/=10)o++;r%=cl,i=r-cl+o}if(n!==void 0&&(a=il(10,o-i-1),s=l/a%10|0,c=t<0||d[u+1]!==void 0||l%a,c=n<4?(s||c)&&(n==0||n==(e.s<0?3:2)):s>5||s==5&&(n==4||c||n==6&&(r>0?i>0?l/il(10,o-i):0:d[u-1])%10&1||n==(e.s<0?8:7))),t<1||!d[0])return c?(a=gl(e),d.length=1,t=t-a-1,d[0]=il(10,(cl-t%cl)%cl),e.e=rl(-t/cl)||0):(d.length=1,d[0]=e.e=e.s=0),e;if(r==0?(d.length=u,a=1,u--):(d.length=u+1,a=il(10,cl-r),d[u]=i>0?(l/il(10,o-i)%il(10,i)|0)*a:0),c)for(;;)if(u==0){(d[0]+=a)==sl&&(d[0]=1,++e.e);break}else{if(d[u]+=a,d[u]!=sl)break;d[u--]=0,a=1}for(r=d.length;d[--r]===0;)d.pop();if($c&&(e.e>ul||e.e<-ul))throw Error(nl+gl(e));return e}function Sl(e,t){var n,r,i,a,o,s,c,l,u,d,f=e.constructor,p=f.precision;if(!e.s||!t.s)return t.s?t.s=-t.s:t=new f(e),$c?xl(t,p):t;if(c=e.d,d=t.d,r=t.e,l=e.e,c=c.slice(),o=l-r,o){for(u=o<0,u?(n=c,o=-o,s=d.length):(n=d,r=l,s=c.length),i=Math.max(Math.ceil(p/cl),s)+2,o>i&&(o=i,n.length=1),n.reverse(),i=o;i--;)n.push(0);n.reverse()}else{for(i=c.length,s=d.length,u=i0;--i)c[s++]=0;for(i=d.length;i>o;){if(c[--i]0?a=a.charAt(0)+`.`+a.slice(1)+vl(r):o>1&&(a=a.charAt(0)+`.`+a.slice(1)),a=a+(i<0?`e`:`e+`)+i):i<0?(a=`0.`+vl(-i-1)+a,n&&(r=n-o)>0&&(a+=vl(r))):i>=o?(a+=vl(i+1-o),n&&(r=n-i-1)>0&&(a=a+`.`+vl(r))):((r=i+1)0&&(i+1===o&&(a+=`.`),a+=vl(r))),e.s<0?`-`+a:a}function wl(e,t){if(e.length>t)return e.length=t,!0}function Tl(e){var t,n,r;function i(e){var t=this;if(!(t instanceof i))return new i(e);if(t.constructor=i,e instanceof i){t.s=e.s,t.e=e.e,t.d=(e=e.d)?e.slice():e;return}if(typeof e==`number`){if(e*0!=0)throw Error(tl+e);if(e>0)t.s=1;else if(e<0)e=-e,t.s=-1;else{t.s=0,t.e=0,t.d=[0];return}if(e===~~e&&e<1e7){t.e=0,t.d=[e];return}return bl(t,e.toString())}else if(typeof e!=`string`)throw Error(tl+e);if(e.charCodeAt(0)===45?(e=e.slice(1),t.s=-1):t.s=1,al.test(e))bl(t,e);else throw Error(tl+e)}if(i.prototype=W,i.ROUND_UP=0,i.ROUND_DOWN=1,i.ROUND_CEIL=2,i.ROUND_FLOOR=3,i.ROUND_HALF_UP=4,i.ROUND_HALF_DOWN=5,i.ROUND_HALF_EVEN=6,i.ROUND_HALF_CEIL=7,i.ROUND_HALF_FLOOR=8,i.clone=Tl,i.config=i.set=El,e===void 0&&(e={}),e)for(r=[`precision`,`rounding`,`toExpNeg`,`toExpPos`,`LN10`],t=0;t=i[t+1]&&r<=i[t+2])this[n]=r;else throw Error(tl+n+`: `+r);if((r=e[n=`LN10`])!==void 0)if(r==Math.LN10)this[n]=new this(r);else throw Error(tl+n+`: `+r);return this}ol=new(Tl(Qc))(1);var G;(function(e){e.AED=`aed`,e.AFN=`afn`,e.ALL=`all`,e.AMD=`amd`,e.ANG=`ang`,e.AOA=`aoa`,e.ARS=`ars`,e.AUD=`aud`,e.AWG=`awg`,e.AZN=`azn`,e.BAM=`bam`,e.BBD=`bbd`,e.BDT=`bdt`,e.BGN=`bgn`,e.BHD=`bhd`,e.BIF=`bif`,e.BMD=`bmd`,e.BND=`bnd`,e.BOB=`bob`,e.BOV=`bov`,e.BRL=`brl`,e.BSD=`bsd`,e.BTN=`btn`,e.BWP=`bwp`,e.BYN=`byn`,e.BYR=`byr`,e.BZD=`bzd`,e.CAD=`cad`,e.CDF=`cdf`,e.CHE=`che`,e.CHF=`chf`,e.CHW=`chw`,e.CLF=`clf`,e.CLP=`clp`,e.CNY=`cny`,e.COP=`cop`,e.COU=`cou`,e.CRC=`crc`,e.CUC=`cuc`,e.CUP=`cup`,e.CVE=`cve`,e.CZK=`czk`,e.DJF=`djf`,e.DKK=`dkk`,e.DOP=`dop`,e.DZD=`dzd`,e.EGP=`egp`,e.ERN=`ern`,e.ETB=`etb`,e.EUR=`eur`,e.FJD=`fjd`,e.FKP=`fkp`,e.GBP=`gbp`,e.GEL=`gel`,e.GHS=`ghs`,e.GIP=`gip`,e.GMD=`gmd`,e.GNF=`gnf`,e.GTQ=`gtq`,e.GYD=`gyd`,e.HKD=`hkd`,e.HNL=`hnl`,e.HRK=`hrk`,e.HTG=`htg`,e.HUF=`huf`,e.IDR=`idr`,e.ILS=`ils`,e.INR=`inr`,e.IQD=`iqd`,e.IRR=`irr`,e.ISK=`isk`,e.JMD=`jmd`,e.JOD=`jod`,e.JPY=`jpy`,e.KES=`kes`,e.KGS=`kgs`,e.KHR=`khr`,e.KMF=`kmf`,e.KPW=`kpw`,e.KRW=`krw`,e.KWD=`kwd`,e.KYD=`kyd`,e.KZT=`kzt`,e.LAK=`lak`,e.LBP=`lbp`,e.LKR=`lkr`,e.LRD=`lrd`,e.LSL=`lsl`,e.LTL=`ltl`,e.LVL=`lvl`,e.LYD=`lyd`,e.MAD=`mad`,e.MDL=`mdl`,e.MGA=`mga`,e.MKD=`mkd`,e.MMK=`mmk`,e.MNT=`mnt`,e.MOP=`mop`,e.MRO=`mro`,e.MUR=`mur`,e.MVR=`mvr`,e.MWK=`mwk`,e.MXN=`mxn`,e.MXV=`mxv`,e.MYR=`myr`,e.MZN=`mzn`,e.NAD=`nad`,e.NGN=`ngn`,e.NIO=`nio`,e.NOK=`nok`,e.NPR=`npr`,e.NZD=`nzd`,e.OMR=`omr`,e.PAB=`pab`,e.PEN=`pen`,e.PGK=`pgk`,e.PHP=`php`,e.PKR=`pkr`,e.PLN=`pln`,e.PYG=`pyg`,e.QAR=`qar`,e.RON=`ron`,e.RSD=`rsd`,e.RUB=`rub`,e.RWF=`rwf`,e.SAR=`sar`,e.SBD=`sbd`,e.SCR=`scr`,e.SDG=`sdg`,e.SEK=`sek`,e.SGD=`sgd`,e.SHP=`shp`,e.SLL=`sll`,e.SOS=`sos`,e.SRD=`srd`,e.SSP=`ssp`,e.STD=`std`,e.SVC=`svc`,e.SYP=`syp`,e.SZL=`szl`,e.THB=`thb`,e.TJS=`tjs`,e.TMT=`tmt`,e.TND=`tnd`,e.TOP=`top`,e.TRY=`try`,e.TTD=`ttd`,e.TWD=`twd`,e.TZS=`tzs`,e.UAH=`uah`,e.UGX=`ugx`,e.USD=`usd`,e.USN=`usn`,e.USS=`uss`,e.UYI=`uyi`,e.UYU=`uyu`,e.UZS=`uzs`,e.VEF=`vef`,e.VND=`vnd`,e.VUV=`vuv`,e.WST=`wst`,e.XAF=`xaf`,e.XAG=`xag`,e.XAU=`xau`,e.XBA=`xba`,e.XBB=`xbb`,e.XBC=`xbc`,e.XBD=`xbd`,e.XCD=`xcd`,e.XDR=`xdr`,e.XFU=`xfu`,e.XOF=`xof`,e.XPD=`xpd`,e.XPF=`xpf`,e.XPT=`xpt`,e.XSU=`xsu`,e.XTS=`xts`,e.XUA=`xua`,e.YER=`yer`,e.ZAR=`zar`,e.ZMW=`zmw`,e.ZWL=`zwl`})(G||={}),G.AED,G.AFN,G.ALL,G.AMD,G.ANG,G.AOA,G.ARS,G.AUD,G.AWG,G.AZN,G.BAM,G.BBD,G.BDT,G.BGN,G.BHD,G.BIF,G.BMD,G.BND,G.BOB,G.BOV,G.BRL,G.BSD,G.BTN,G.BWP,G.BYR,G.BYN,G.BZD,G.CAD,G.CDF,G.CHE,G.CHF,G.CHW,G.CLF,G.CLP,G.CNY,G.COP,G.COU,G.CRC,G.CUC,G.CUP,G.CVE,G.CZK,G.DJF,G.DKK,G.DOP,G.DZD,G.EGP,G.ERN,G.ETB,G.EUR,G.FJD,G.FKP,G.GBP,G.GEL,G.GHS,G.GIP,G.GMD,G.GNF,G.GTQ,G.GYD,G.HKD,G.HNL,G.HRK,G.HTG,G.HUF,G.IDR,G.ILS,G.INR,G.IQD,G.IRR,G.ISK,G.JMD,G.JOD,G.JPY,G.KES,G.KGS,G.KHR,G.KMF,G.KPW,G.KRW,G.KWD,G.KYD,G.KZT,G.LAK,G.LBP,G.LKR,G.LRD,G.LSL,G.LTL,G.LVL,G.LYD,G.MAD,G.MDL,G.MGA,G.MKD,G.MMK,G.MNT,G.MOP,G.MRO,G.MUR,G.MVR,G.MWK,G.MXN,G.MXV,G.MYR,G.MZN,G.NAD,G.NGN,G.NIO,G.NOK,G.NPR,G.NZD,G.OMR,G.PAB,G.PEN,G.PGK,G.PHP,G.PKR,G.PLN,G.PYG,G.QAR,G.RON,G.RSD,G.RUB,G.RWF,G.SAR,G.SBD,G.SCR,G.SDG,G.SEK,G.SGD,G.SHP,G.SLL,G.SOS,G.SRD,G.SSP,G.STD,G.SVC,G.SYP,G.SZL,G.THB,G.TJS,G.TMT,G.TND,G.TOP,G.TRY,G.TTD,G.TWD,G.TZS,G.UAH,G.UGX,G.USD,G.USN,G.USS,G.UYI,G.UYU,G.UZS,G.VEF,G.VND,G.VUV,G.WST,G.XAF,G.XAG,G.XAU,G.XBA,G.XBB,G.XBC,G.XBD,G.XCD,G.XDR,G.XFU,G.XOF,G.XPD,G.XPF,G.XPT,G.XSU,G.XTS,G.XUA,G.YER,G.ZAR,G.ZMW,G.ZWL;var Dl={exports:{}};Dl.exports;var Ol;function kl(){return Ol?Dl.exports:(Ol=1,(function(e,t){var n=`__lodash_hash_undefined__`,r=9007199254740991,i=`[object Arguments]`,a=`[object Array]`,o=`[object Boolean]`,s=`[object Date]`,c=`[object Error]`,l=`[object Function]`,u=`[object Map]`,d=`[object Number]`,f=`[object Object]`,p=`[object Promise]`,m=`[object RegExp]`,h=`[object Set]`,g=`[object String]`,_=`[object Symbol]`,v=`[object WeakMap]`,y=`[object ArrayBuffer]`,b=`[object DataView]`,x=`[object Float32Array]`,S=`[object Float64Array]`,C=`[object Int8Array]`,w=`[object Int16Array]`,T=`[object Int32Array]`,E=`[object Uint8Array]`,D=`[object Uint8ClampedArray]`,O=`[object Uint16Array]`,ee=`[object Uint32Array]`,te=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,k=/^\w*$/,ne=/^\./,A=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,re=/[\\^$.*+?()[\]{}|]/g,ie=/\\(\\)?/g,ae=/^\[object .+?Constructor\]$/,oe=/^(?:0|[1-9]\d*)$/,j={};j[x]=j[S]=j[C]=j[w]=j[T]=j[E]=j[D]=j[O]=j[ee]=!0,j[i]=j[a]=j[y]=j[o]=j[b]=j[s]=j[c]=j[l]=j[u]=j[d]=j[f]=j[m]=j[h]=j[g]=j[v]=!1;var se=typeof qn==`object`&&qn&&qn.Object===Object&&qn,ce=typeof self==`object`&&self&&self.Object===Object&&self,le=se||ce||globalThis,ue=t&&!t.nodeType&&t,M=ue&&e&&!e.nodeType&&e,N=M&&M.exports===ue&&se.process,de=function(){try{return N&&N.binding(`util`)}catch{}}(),fe=de&&de.isTypedArray;function pe(e,t){for(var n=-1,r=e?e.length:0;++n-1}function ot(e,t){var n=this.__data__,r=Ct(n,e);return r<0?n.push([e,t]):n[r][1]=t,this}tt.prototype.clear=nt,tt.prototype.delete=rt,tt.prototype.get=it,tt.prototype.has=at,tt.prototype.set=ot;function st(e){var t=-1,n=e?e.length:0;for(this.clear();++ts))return!1;var l=a.get(e);if(l&&a.get(t))return l==t;var u=-1,d=!0,f=i&1?new pt:void 0;for(a.set(e,t),a.set(t,e);++u-1&&e%1==0&&e-1&&e%1==0&&e<=r}function gn(e){var t=typeof e;return!!e&&(t==`object`||t==`function`)}function _n(e){return!!e&&typeof e==`object`}function vn(e){return typeof e==`symbol`||_n(e)&&Ee.call(e)==_}var yn=fe?he(fe):Pt;function bn(e){return e==null?``:Bt(e)}function xn(e,t,n){var r=e==null?void 0:Dt(e,t);return r===void 0?n:r}function Sn(e,t){return e!=null&&Xt(e,t,kt)}function Cn(e){return fn(e)?St(e):It(e)}function wn(e,t,n){var r=dn(e)||yn(e);if(t=Ft(t),n==null)if(r||gn(e)){var i=e.constructor;n=r?dn(e)?new i:[]:mn(i)?wt(Ae(e)):{}}else n={};return(r?pe:Et)(e,function(e,r,i){return t(n,e,r,i)}),n}function Tn(e){return e}function En(e){return Qt(e)?P(on(e)):zt(e)}e.exports=wn})(Dl,Dl.exports),Dl.exports)}kl();var{Commands:Al}=vo,jl=[`identify`,`guilds.members.read`];function Ml(e,t){if(e instanceof Bn)return e;if(typeof e==`object`&&e&&`code`in e){let n=e.code,r=Object.values(Ua).filter(e=>typeof e==`number`);if(typeof n==`number`&&r.includes(n))return new Bn(`${t}_${n}`)}return new Bn(t)}var Nl=15e3,Pl=12e4,Fl=15e3;function Il(e=window.location.search){let t=new URLSearchParams(e),n=t.getAll(`frame_id`),r=t.getAll(`platform`);if(n.length!==1||n[0]===void 0||n[0].length===0||n[0].length>255||r.length!==1||![`desktop`,`mobile`].includes(r[0]??``))throw new Bn(`invalid_launch`)}async function Ll(e,t,n,r,i){let a=null,o=new Promise((e,n)=>{t!==void 0&&(a=()=>n(new Bn(`cancelled`)),t.addEventListener(`abort`,a,{once:!0}),t.aborted&&a())}),s=null,c=new Promise((e,t)=>{s=setTimeout(()=>t(new Bn(r)),i)}),l=Promise.resolve().then(()=>{if(t?.aborted)throw new Bn(`cancelled`);return e()}).catch(e=>{throw Ml(e,n)});try{return await Promise.race([l,o,c])}finally{s!==null&&clearTimeout(s),t!==void 0&&a!==null&&t.removeEventListener(`abort`,a)}}var Rl=class{makeSDK;sdk=null;authenticated=!1;closed=!1;constructor(e=e=>new Xc(e)){this.makeSDK=e}async authorize(e){let t=Hn();Il();let n=await Wn(t,e),r=this.open(n.client_id),i=()=>{if(e?.aborted)throw new Bn(`cancelled`)};try{if(i(),await Ll(()=>r.ready(),e,`sdk_ready_failed`,`sdk_ready_timeout`,Nl),i(),r.instanceId!==t)throw new Bn(`instance_mismatch`);let a=await Ll(()=>r.commands.authorize({client_id:n.client_id,response_type:`code`,state:n.state,prompt:`none`,scope:[...jl]}),e,`authorize_failed`,`authorize_timeout`,Pl);i();let o=await Gn(a.code,n.state,e);if(i(),!this.authenticated){if(await Ll(()=>r.commands.authenticate({access_token:o.access_token}),e,`sdk_authenticate_failed`,`sdk_authenticate_timeout`,Fl)==null)throw new Bn(`sdk_authenticate_failed`);this.authenticated=!0,i()}let s=o.ticket;return{sdk:r,bootstrap:n,player:o.player,takeTicket:()=>{let e=s;return s=null,e},destroy:()=>{s=null}}}catch(t){throw e?.aborted?new Bn(`cancelled`):new Bn(Ml(t,`authorization_failed`).code,!0)}}close(){if(!this.closed&&(this.closed=!0,this.sdk!==null))try{this.sdk.close(Ha.CLOSE_NORMAL,`Hands session closed`)}catch{}}open(e){if(this.closed)throw new Bn(`cancelled`);if(this.sdk!==null)return this.sdk;try{this.sdk=this.makeSDK(e)}catch(e){throw new Bn(Ml(e,`sdk_initialization_failed`).code,!0)}return this.sdk}},zl={Digit1:`corner_cut`,Numpad1:`corner_cut`,Digit2:`corner_swelling`,Numpad2:`corner_swelling`,Digit3:`corner_breath`,Numpad3:`corner_breath`},Bl={2:`corner_cut`,3:`corner_swelling`,1:`corner_breath`},Vl=[{kind:`corner_cut`,choice:`cut`,key:`1`,title:`Close the cut`},{kind:`corner_swelling`,choice:`swelling`,key:`2`,title:`Bring down the swelling`},{kind:`corner_breath`,choice:`breath`,key:`3`,title:`Catch your breath`}],Hl={cut:`Your corner is closing the cut.`,swelling:`Your corner is bringing the swelling down.`,breath:`Your corner is getting your breath back.`,balanced:`Your corner did a little of everything.`},Ul=1500,Wl=1e3;function Gl(e){return e<150?`small`:e<350?`nasty`:e<550?`deep`:`dangerous`}var Kl=(e,t)=>Math.round(Math.max(0,Math.min(1,e/t))*100);function ql(e){let{trauma:t}=e,n=Math.max(t.left_cut,t.right_cut),r=Math.max(0,n-p.cut.worse_cut),i=Math.max(t.left_eye,t.right_eye),a=Math.max(0,i-p.swelling.eyes),o=i>=d?a0,swelling:Math.max(t.left_eye,t.right_eye)>0||t.swelling>0,breath:!0,balanced:!1}}function Yl(){try{return[...navigator.getGamepads?.()??[]].find(e=>e!==null&&e.connected&&e.mapping===`standard`)??null}catch{return null}}var Xl=class{pick;now;element;buttons=new Map;details=new Map;status;visible=!1;choice=null;pending=null;shown=null;frozen=null;available=null;padFrame=0;padPrevious=new Set;measuredTop=null;resize=()=>{this.measuredTop=null};keydown=e=>{let t=zl[e.code];t===void 0||!this.visible||e.repeat||(e.preventDefault(),this.choose(t))};constructor(e,t,n=()=>performance.now()){this.pick=t,this.now=n;let r=document.createElement(`section`);r.className=`corner`,r.dataset.corner=``,r.hidden=!0,r.setAttribute(`aria-label`,`Instructions for your corner`);let i=document.createElement(`p`);i.className=`corner-title`,i.textContent=`YOUR CORNER`;let a=document.createElement(`div`);a.className=`corner-choices`;for(let{kind:e,key:t,title:n}of Vl){let r=document.createElement(`button`);r.type=`button`,r.dataset.cornerPick=e;let i=document.createElement(`kbd`);i.textContent=t;let o=document.createElement(`strong`);o.textContent=n;let s=document.createElement(`span`);r.append(i,o,s),r.addEventListener(`click`,()=>this.choose(e)),a.append(r),this.buttons.set(e,r),this.details.set(e,s)}this.status=document.createElement(`p`),this.status.className=`corner-status`,this.status.setAttribute(`role`,`status`),r.append(i,a,this.status),e.append(r),this.element=r,window.addEventListener(`keydown`,this.keydown),window.addEventListener(`resize`,this.resize)}update(e,t,n){let r=e?.fighters.find(e=>e.player_id===t);if(!(n&&e!==null&&e.phase===`rest`&&r!==void 0)||r===void 0){this.setVisible(!1),this.choice=null,this.pending=null,this.shown=null,this.frozen=null;return}this.setVisible(!0),this.choice=r.corner_choice,(this.choice!==null||this.pending!==null&&this.now()-this.pending.at>Ul)&&(this.pending=null);let i=this.choice!==null||this.pending!==null;i||(this.frozen=null);let a=i&&this.frozen!==null?this.frozen:ql(r);this.shown=a;let o=Jl(r);this.available=o;for(let{kind:e,choice:t}of Vl){let n=this.buttons.get(e),r=this.details.get(e);r.textContent!==a[t]&&(r.textContent=a[t]),n.disabled=i||!o[t],n.toggleAttribute(`data-picked`,this.choice===t||this.pending?.kind===e)}let s=this.choice===null?this.pending===null?`Tell your corner what to work on before the bell.`:`Calling it to your corner…`:Hl[this.choice];this.status.textContent!==s&&(this.status.textContent=s)}choose(e){if(!this.visible||this.choice!==null||this.pending!==null)return;let t=Vl.find(t=>t.kind===e)?.choice;if(!(t===void 0||this.available?.[t]===!1)&&this.pick(e)){this.frozen=this.shown,this.pending={kind:e,at:this.now()};for(let[t,n]of this.buttons)n.disabled=!0,n.toggleAttribute(`data-picked`,t===e);this.status.textContent=`Calling it to your corner…`}}setVisible(e){if(e!==this.visible)if(this.visible=e,this.element.hidden=!e,this.measuredTop=null,this.padPrevious.clear(),e){let e=Yl();for(let t of Object.keys(Bl))e?.buttons[Number(t)]?.pressed===!0&&this.padPrevious.add(Number(t));this.padFrame=requestAnimationFrame(()=>this.pollPad())}else cancelAnimationFrame(this.padFrame)}top(){if(!this.visible)return null;if(this.measuredTop===null){let e=this.element.getBoundingClientRect();if(e.height<=0)return null;this.measuredTop=e.top-(this.element.parentElement?.getBoundingClientRect().top??0)}return this.measuredTop}pollPad(){if(!this.visible)return;let e=Yl();if(e!==null)for(let[t,n]of Object.entries(Bl)){let r=Number(t),i=e.buttons[r]?.pressed===!0;i&&!this.padPrevious.has(r)&&this.choose(n),i?this.padPrevious.add(r):this.padPrevious.delete(r)}this.padFrame=requestAnimationFrame(()=>this.pollPad())}destroy(){this.setVisible(!1),window.removeEventListener(`keydown`,this.keydown),window.removeEventListener(`resize`,this.resize),this.element.remove()}},Zl={invalid_launch:`open Hands from a Discord voice channel with Play now`,cancelled:`the session was cancelled`,authorization_failed:`Discord did not authorize the session`,authorize_failed:`Discord did not authorize the session`,authorize_timeout:`Discord took too long to authorize the session`,sdk_ready_failed:`the Discord client did not respond`,sdk_ready_timeout:`the Discord client took too long to respond`,sdk_authenticate_failed:`Discord did not confirm your identity`,sdk_authenticate_timeout:`Discord took too long to confirm your identity`,sdk_initialization_failed:`the Discord client could not start the activity`,instance_mismatch:`this window belongs to a different activity instance`,bootstrap_failed:`the Hands server could not be reached`,token_failed:`the Hands server rejected the sign-in`,invalid_bootstrap:`the Hands server sent an unexpected reply`,invalid_token_response:`the Hands server sent an unexpected reply`,ticket_unavailable:`the session ticket was already used`,protocol_error:`the server and client disagree on the protocol`,unexpected_error:`something unexpected went wrong`,authentication_timeout:`the connection took too long to authenticate`,authentication_required:`the connection was not authenticated`,invalid_ticket:`the session ticket was rejected`,invalid_request:`the server rejected the request`,invalid_input:`the server rejected an input frame`,input_queue_full:`the server could not keep up with the inputs`,rate_limited:`too many requests were sent; wait a moment`,service_busy:`the server is busy right now`,service_unavailable:`the server is unavailable right now`,upstream_unavailable:`Discord is unavailable right now`,room_closed:`the bout has already ended; use Play now to start another`,room_full:`both seats in this bout are taken`,already_in_room:`you are already in a bout in another channel`,match_not_started:`the bout has not started yet`,connection_replaced:`this seat was taken over by a newer connection`,spectator_read_only:`spectators cannot send input`,server_shutting_down:`the server is restarting`,internal_error:`the server hit an internal error`,not_in_activity:`you are not a participant of this activity`,invalid_activity:`this activity instance is not valid`,invalid_state:`the sign-in state expired; retry`,invalid_guild:`this activity belongs to another server`,persistence_failed:`the result could not be saved`,rematch_unavailable:`the ring could not be cleared for a rematch; use Play now to start another bout`,network_unavailable:`the connection to the Hands server could not be opened`,match_abandoned:`both fighters lost their connection and the bout was called off`};function Ql(e){if(e===`client_outdated`)return`Hands was updated. Reload to continue (client_outdated).`;let t=Zl[e]??Zl[e.replace(/_\d+$/u,``)];return t===void 0?`Unable to continue (${e}).`:`Unable to continue: ${t} (${e}).`}function $l(){try{if(window.matchMedia?.(`(pointer: coarse)`).matches)return!0}catch{}return(navigator.maxTouchPoints??0)>0&&new URLSearchParams(window.location.search).get(`platform`)===`mobile`}var eu=[{label:`JAB`,punchClass:`jab`},{label:`STRAIGHT`,punchClass:`straight`},{label:`HOOK`,punchClass:`hook`},{label:`UPPER`,punchClass:`uppercut`}],tu=[{label:`◀ SLIP`,name:`Slip left`,kind:`slip_left`},{label:`WEAVE`,name:`Weave`,kind:`weave`},{label:`SLIP ▶`,name:`Slip right`,kind:`slip_right`},{label:`PULL`,name:`Pull`,kind:`pull`},{label:`CLINCH`,name:`Clinch`,kind:`clinch`}],nu=class{sharedActions;root;stickZone;stickBase;stickKnob;pads=[];moves=[];stickPointer=null;stickOrigin={x:0,y:0};moveX=0;moveY=0;body=!1;power=!1;guard=`none`;knockdown=!1;enabled=!0;heldPointers=new Map;constructor(e,t){this.sharedActions=t,this.root=document.createElement(`div`),this.root.className=`touch-controls`,this.root.setAttribute(`aria-label`,`Touch controls`),this.root.innerHTML=`
-
${Os.map(e=>``).join(``)}
+
${tu.map(e=>``).join(``)}
-
${Ds.map(e=>``).join(``)}
-
`,e.append(this.root),this.stickZone=this.root.querySelector(`[data-stick]`),this.stickBase=this.root.querySelector(`[data-stick-base]`),this.stickKnob=this.root.querySelector(`[data-stick-knob]`),this.pads.push(...this.root.querySelectorAll(`[data-punch]`)),this.moves.push(...this.root.querySelectorAll(`[data-move]`)),this.bind()}bind(){let e=this.stickZone;e.addEventListener(`pointerdown`,t=>{if(this.stickPointer!==null)return;t.preventDefault(),this.stickPointer=t.pointerId,this.stickOrigin={x:t.clientX,y:t.clientY},e.setPointerCapture(t.pointerId);let n=e.getBoundingClientRect();this.stickBase.style.left=`${t.clientX-n.left}px`,this.stickBase.style.top=`${t.clientY-n.top}px`,this.stickBase.classList.add(`active`),this.updateStick(t.clientX,t.clientY)}),e.addEventListener(`pointermove`,e=>{e.pointerId===this.stickPointer&&(e.preventDefault(),this.updateStick(e.clientX,e.clientY))});let t=e=>{e.pointerId===this.stickPointer&&(this.stickPointer=null,this.moveX=0,this.moveY=0,this.stickKnob.style.transform=`translate(-50%, -50%)`,this.stickBase.classList.remove(`active`))};e.addEventListener(`pointerup`,t),e.addEventListener(`pointercancel`,t);for(let e of this.pads)e.addEventListener(`pointerdown`,t=>{if(!this.enabled)return;t.preventDefault();let n=e.getBoundingClientRect(),r=t.clientX-n.lefte.classList.remove(`pressed`),120)});for(let e of this.moves)e.addEventListener(`pointerdown`,t=>{!this.enabled||this.knockdown||(t.preventDefault(),this.sharedActions.push(`touch`,{kind:e.dataset.move}),e.classList.add(`pressed`),window.setTimeout(()=>e.classList.remove(`pressed`),120))});for(let e of this.root.querySelectorAll(`[data-mod], [data-guard]`)){let t=e.dataset.mod??e.dataset.guard??``,n=n=>{n.preventDefault(),e.setPointerCapture(n.pointerId),this.heldPointers.set(n.pointerId,t),(t===`guard_high`||t===`guard_low`)&&this.sharedActions.clear(),this.syncHeld(),e.classList.add(`pressed`)},r=n=>{this.heldPointers.delete(n.pointerId)&&(this.syncHeld(),e.classList.toggle(`pressed`,[...this.heldPointers.values()].includes(t)))};e.addEventListener(`pointerdown`,n),e.addEventListener(`pointerup`,r),e.addEventListener(`pointercancel`,r)}}syncHeld(){let e=[...this.heldPointers.values()];this.body=e.includes(`body`),this.power=e.includes(`power`),this.guard=e.findLast(e=>e===`guard_high`||e===`guard_low`)??`none`}updateStick(e,t){let n=e-this.stickOrigin.x,r=t-this.stickOrigin.y,i=Math.hypot(n,r);i>56&&(n=n/i*56,r=r/i*56),this.stickKnob.style.transform=`translate(calc(-50% + ${n}px), calc(-50% + ${r}px))`;let a=Math.min(1,i/56),o=a<.15?0:(a-.15)/.85,s=i>0?n/Math.max(i,1e-6):0,c=i>0?r/Math.max(i,1e-6):0;this.moveX=Math.round(s*o*1e3)||0,this.moveY=Math.round(-c*o*1e3)||0}setKnockdown(e){if(this.knockdown!==e){this.knockdown=e,this.root.classList.toggle(`knockdown`,e);for(let[t,n]of this.pads.entries()){let r=n.querySelector(`.touch-pad-label`);r!==null&&(r.textContent=e?t%2==0?`GET UP`:`RHYTHM`:Ds[t].label)}}}setEnabled(e,t=e){this.enabled=e,this.root.classList.toggle(`disabled`,!t),this.root.classList.toggle(`resting`,t&&!e),t?e||this.sharedActions.clearSource(`touch`):this.reset()}frame(){return this.enabled?{moveX:this.moveX,moveY:this.moveY,defense:this.guard,actions:[]}:{moveX:0,moveY:0,defense:`none`,actions:[]}}reset(){this.stickPointer=null,this.moveX=0,this.moveY=0,this.body=!1,this.power=!1,this.guard=`none`,this.heldPointers.clear(),this.stickBase.classList.remove(`active`);for(let e of this.root.querySelectorAll(`.pressed`))e.classList.remove(`pressed`);this.sharedActions.clearSource(`touch`)}destroy(){this.reset(),this.root.remove()}},As={block:[55,.12,.22],hit:[85,.3,.42],counter_hit:[105,.38,.5],stun:[125,.42,.55],knockdown:[180,.62,.65]},js=class{settings;constructor(e){this.settings=e}event(e){if(!this.settings().haptics)return;let t=As[e.kind];if(t===void 0)return;let n=this.padActuator();if(n===null){this.vibrate(t[0]);return}try{Promise.resolve(n.playEffect(`dual-rumble`,{duration:Math.min(180,Math.max(0,t[0])),strongMagnitude:Math.min(.65,Math.max(0,t[1])),weakMagnitude:Math.min(.65,Math.max(0,t[2]))})).catch(()=>void 0)}catch{}}padActuator(){if(typeof navigator.getGamepads!=`function`)return null;let e;try{e=[...navigator.getGamepads.call(navigator)]}catch{return null}let t=e.find(e=>e!==null&&e.connected&&`vibrationActuator`in e)?.vibrationActuator;return t==null||typeof t.playEffect!=`function`?null:t}vibrate(e){if(!(!Es()||typeof navigator.vibrate!=`function`))try{navigator.vibrate(Math.min(180,Math.max(0,e)))}catch{}}},Ms={KeyF:[`left`,`jab`],KeyJ:[`right`,`jab`],KeyR:[`left`,`straight`],KeyU:[`right`,`straight`],KeyG:[`left`,`hook`],KeyH:[`right`,`hook`],KeyT:[`left`,`uppercut`],KeyY:[`right`,`uppercut`]},Ns={KeyZ:{kind:`slip_left`},KeyX:{kind:`slip_right`},KeyC:{kind:`weave`},KeyV:{kind:`pull`},KeyB:{kind:`clinch`},KeyN:{kind:`switch_stance`},KeyM:{kind:`taunt`},Digit1:{kind:`foul`,foul:`low_blow`},Digit2:{kind:`foul`,foul:`headbutt`},ArrowLeft:{kind:`get_up_left`},ArrowRight:{kind:`get_up_right`}},Ps=new Set([...Object.keys(Ms),...Object.keys(Ns),`KeyW`,`KeyA`,`KeyS`,`KeyD`,`KeyQ`,`KeyE`,`ShiftLeft`,`ShiftRight`,`AltLeft`,`AltRight`]),Fs=[`Move: W up · S down · A left · D right`,`High / low guard: Q / E`,`Left/right jab: F / J`,`Left/right straight: R / U`,`Left/right hook: G / H`,`Left/right uppercut: T / Y`,`Body: Shift · Power: Alt`,`Slip: Z / X · Weave: C · Pull: V`,`Clinch: B · Stance: N · Taunt: M · Fouls: 1 / 2`,`Get-up rhythm: ← / →`,`Controller move: left stick. High / low guard: left / right shoulder (independent of punches).`,`Controller face classes: bottom jab · right straight · left hook · top uppercut.`,`Controller face hand: hold D-pad left for left hand or D-pad right for right hand, then press a face punch; otherwise punches use the right hand. A direction used for a punch is consumed and does not evade.`,`Controller modifiers: left trigger body · right trigger power.`,`Controller actions: left stick press clinch · right stick press switch stance · hold right trigger + right stick press to taunt · D-pad up weave · D-pad down pull · tap and release D-pad left/right to slip; while down, D-pad left/right performs the private get-up rhythm immediately.`,`Controller fouls: View/Back low blow · Menu/Start headbutt.`,`Right-stick gesture: horizontal 0–22.5° hook · 22.5–45° jab · 45–70° straight · 70–90° uppercut; left/right direction selects hand.`,`Touch: drag the left side to move · tap a punch pad's left or right half for that hand · hold BODY, POWER, GUARD or LOW · tap ◀ SLIP, SLIP ▶, WEAVE, PULL or CLINCH.`];function Is(e,t){return e.kind===t.kind?e.kind===`punch`&&t.kind===`punch`?e.hand===t.hand&&e.class===t.class&&e.target===t.target&&e.power===t.power:e.kind===`foul`&&t.kind===`foul`?e.foul===t.foul:!0:!1}function Ls(e,t,n){if(n<1||e.length>0&&Is(e.at(-1),t))return;let r=e.length-n+1;r>0&&e.splice(0,r),e.push(t)}var Rs=class{maximum;queue=[];counter=0;listener=null;constructor(e=1){this.maximum=e}setListener(e){this.listener=e}push(e,t){if(this.maximum<1)return;let n=this.queue.at(-1);if(n!==void 0&&Is(n.action,t)){n.source=e;return}let r={...t,id:`c${this.counter+=1}`},i=this.queue.length-this.maximum+1;i>0&&this.queue.splice(0,i),this.queue.push({action:r,source:e}),this.listener?.(r)}clear(){this.queue.length=0}clearSource(e){let t=this.queue.filter(t=>t.source!==e);this.queue.splice(0,this.queue.length,...t)}drain(e){return this.queue.splice(0,e).map(e=>e.action)}};function zs(e,t,n=.18){let r=Math.hypot(e,t);if(!Number.isFinite(r)||r<=n)return{x:0,y:0};let i=(Math.min(1,r)-n)/(1-n);return{x:e/r*i,y:t/r*i}}var Bs=(e,t)=>{let n=Math.atan2(Math.abs(t),Math.abs(e))*180/Math.PI;return n<22.5?`hook`:n<45?`jab`:n<70?`straight`:`uppercut`},Vs=class{active=!1;hand=`right`;target=`head`;power=`normal`;peak={x:0,y:0};peakMagnitude=0;update(e,t,n,r){let i=Math.hypot(e,t);if(!this.active&&i>=.55&&Math.abs(e)>=.12)return this.active=!0,this.hand=e<0?`left`:`right`,this.target=n?`body`:`head`,this.power=r?`power`:`normal`,this.peak={x:e,y:t},this.peakMagnitude=i,null;if(!this.active)return null;if(i>this.peakMagnitude&&(this.peak={x:e,y:t},this.peakMagnitude=i),i<=.25){let e={kind:`punch`,hand:this.hand,class:Bs(this.peak.x,this.peak.y),target:this.target,power:this.power};return this.reset(),e}return null}reset(){this.active=!1,this.peak={x:0,y:0},this.peakMagnitude=0}},Hs=[`jab`,`straight`,`hook`,`uppercut`],Us=(e,t)=>e.buttons[t]?.pressed===!0;function Ws(){let e=navigator.getGamepads;if(typeof e!=`function`)return null;try{return[...e.call(navigator)].find(e=>e!==null&&e.connected&&e.mapping===`standard`)??null}catch{return null}}var Gs=class{maximumQueue;sharedActions;gesture=new Vs;queue=[];previous=new Set;selectorConsumed=new Map;raf=0;currentId=null;moveX=0;moveY=0;defense=`none`;knockdown=!1;enabled=!0;disconnect=e=>{e.gamepad.id===this.currentId&&this.reset()};constructor(e=1,t){this.maximumQueue=e,this.sharedActions=t,window.addEventListener(`gamepaddisconnected`,this.disconnect),this.raf=requestAnimationFrame(()=>this.poll())}push(e){this.sharedActions===void 0?Ls(this.queue,e,this.maximumQueue):this.sharedActions.push(`gamepad`,e)}clearActions(){this.queue.length=0,this.sharedActions?.clear()}poll(){if(!this.enabled){this.reset(),this.schedulePoll();return}let e=Ws();if(e===null){this.currentId!==null&&this.reset(),this.schedulePoll();return}this.currentId!==null&&this.currentId!==e.id&&this.reset(),this.currentId=e.id;let t=zs(e.axes[0]??0,e.axes[1]??0);this.moveX=Math.round(t.x*1e3),this.moveY=Math.round(-t.y*1e3);let n=Us(e,4)?`guard_high`:Us(e,5)?`guard_low`:`none`;n!==`none`&&n!==this.defense&&this.clearActions(),this.defense=n;let r=this.gesture.update(e.axes[2]??0,e.axes[3]??0,Us(e,6),Us(e,7));r!==null&&this.push(r);let i=Hs.some((t,n)=>Us(e,n)&&!this.previous.has(n));for(let t of[14,15]){let n=Us(e,t),r=this.previous.has(t);this.knockdown?(n&&!r&&this.push({kind:t===14?`get_up_left`:`get_up_right`}),this.selectorConsumed.delete(t)):n?(r||this.selectorConsumed.set(t,!1),i&&this.selectorConsumed.set(t,!0)):r&&(this.selectorConsumed.get(t)===!1&&this.push({kind:t===14?`slip_left`:`slip_right`}),this.selectorConsumed.delete(t))}for(let t=0;tthis.poll())}setKnockdown(e){this.knockdown=e}setEnabled(e){this.enabled=e,e||this.reset()}frame(e=4){let t=this.sharedActions===void 0?this.queue.splice(0,e):[];return{moveX:this.moveX,moveY:this.moveY,defense:this.defense,actions:t}}reset(){this.currentId=null,this.moveX=this.moveY=0,this.defense=`none`,this.queue.length=0,this.sharedActions?.clearSource(`gamepad`),this.previous.clear(),this.selectorConsumed.clear(),this.gesture.reset()}destroy(){cancelAnimationFrame(this.raf),window.removeEventListener(`gamepaddisconnected`,this.disconnect),this.reset()}},Ks=class{target;maximumQueue;sharedActions;held=new Set;queue=[];enabled=!0;keydown=e=>{if(!Ps.has(e.code))return;if(!this.enabled){this.held.add(e.code);return}if(e.preventDefault(),e.repeat){this.held.add(e.code);return}this.held.add(e.code),(e.code===`KeyQ`||e.code===`KeyE`)&&this.clearActions();let t=Ms[e.code];if(t!==void 0)this.push({kind:`punch`,hand:t[0],class:t[1],target:this.hasShift()?`body`:`head`,power:this.hasAlt()?`power`:`normal`});else{let t=Ns[e.code];t!==void 0&&this.push(t)}};keyup=e=>{Ps.has(e.code)&&(this.enabled&&e.preventDefault(),this.held.delete(e.code))};visibility=()=>{document.hidden&&this.reset()};blur=()=>this.reset();constructor(e=window,t=1,n){this.target=e,this.maximumQueue=t,this.sharedActions=n,e.addEventListener(`keydown`,this.keydown),e.addEventListener(`keyup`,this.keyup),e.addEventListener(`blur`,this.blur),document.addEventListener(`visibilitychange`,this.visibility)}hasShift(){return this.held.has(`ShiftLeft`)||this.held.has(`ShiftRight`)}hasAlt(){return this.held.has(`AltLeft`)||this.held.has(`AltRight`)}push(e){this.sharedActions===void 0?Ls(this.queue,e,this.maximumQueue):this.sharedActions.push(`keyboard`,e)}clearActions(){this.queue.length=0,this.sharedActions?.clear()}setEnabled(e){this.enabled=e,e||(this.queue.length=0,this.sharedActions?.clearSource(`keyboard`))}frame(e=4){if(!this.enabled)return{moveX:0,moveY:0,defense:`none`,actions:[]};let t=(this.held.has(`KeyD`)?1e3:0)-(this.held.has(`KeyA`)?1e3:0),n=(this.held.has(`KeyW`)?1e3:0)-(this.held.has(`KeyS`)?1e3:0);t!==0&&n!==0&&(t=Math.sign(t)*707,n=Math.sign(n)*707);let r=this.held.has(`KeyQ`)?`guard_high`:this.held.has(`KeyE`)?`guard_low`:`none`;return{moveX:t,moveY:n,defense:r,actions:this.sharedActions===void 0?this.queue.splice(0,e):[]}}reset(){this.held.clear(),this.queue.length=0,this.sharedActions?.clearSource(`keyboard`)}destroy(){this.reset(),this.target.removeEventListener(`keydown`,this.keydown),this.target.removeEventListener(`keyup`,this.keyup),this.target.removeEventListener(`blur`,this.blur),document.removeEventListener(`visibilitychange`,this.visibility)}},qs=class{actions=new Rs(1);keyboard;gamepad;touch=null;reset=()=>{this.keyboard.reset(),this.gamepad.reset(),this.touch?.reset(),this.actions.clear(),this.gamepad.setKnockdown(!1)};constructor(){this.keyboard=new Ks(window,1,this.actions),this.gamepad=new Gs(1,this.actions),window.addEventListener(`blur`,this.reset),document.addEventListener(`visibilitychange`,this.visibility)}visibility=()=>{document.hidden&&this.reset()};attachTouch(e,t=Es()){this.touch!==null||!t||(this.touch=new ks(e,this.actions))}setActive(e,t=e){this.keyboard.setEnabled(e),this.gamepad.setEnabled(e),this.touch?.setEnabled(e,t)}setKnockdown(e){this.gamepad.setKnockdown(e),this.touch?.setKnockdown(e)}onAction(e){this.actions.setListener(e)}held(){let e=this.keyboard.frame(0),t=this.gamepad.frame(0),n=this.touch?.frame()??null;return{moveX:t.moveX===0?n!==null&&n.moveX!==0?n.moveX:e.moveX:t.moveX,moveY:t.moveY===0?n!==null&&n.moveY!==0?n.moveY:e.moveY:t.moveY,defense:t.defense===`none`?n!==null&&n.defense!==`none`?n.defense:e.defense:t.defense}}frame(){return{...this.held(),actions:this.actions.drain(4)}}destroy(){window.removeEventListener(`blur`,this.reset),document.removeEventListener(`visibilitychange`,this.visibility),this.keyboard.destroy(),this.gamepad.destroy(),this.touch?.destroy()}},Js=(e,t,n)=>e+(t-e)*n,Ys=(e,t,n)=>({...t,x:Js(e.x,t.x,n),y:Js(e.y,t.y,n),facing:Js(e.facing,t.facing,n),facing_x:Js(e.facing_x,t.facing_x,n),facing_y:Js(e.facing_y,t.facing_y,n),velocity_x:Js(e.velocity_x,t.velocity_x,n),velocity_y:Js(e.velocity_y,t.velocity_y,n),stamina:Js(e.stamina,t.stamina,n),maximum_stamina:Js(e.maximum_stamina,t.maximum_stamina,n),conditioning:Js(e.conditioning,t.conditioning,n),guard:Js(e.guard,t.guard,n),poise:Js(e.poise,t.poise,n)}),Xs=1.5,Zs=6,Qs=.5,$s=2500,ec=1,tc=.04,nc=100,rc=.01,ic=4,ac=class{maximum;tickRate;initialDelayTicks;snapshots=[];offsetTicks=null;delayTicks;lastStarvationMs=-1/0;lastFrameMs=null;behindPushes=0;needs=[];constructor(e=8,t=30,n=2){this.maximum=e,this.tickRate=t,this.initialDelayTicks=n,this.delayTicks=n}get interpolationDelayTicks(){return this.delayTicks}push(e,t){let n=this.snapshots.at(-1);if(n!==void 0&&e.tick<=n.tick)return!1;if(this.snapshots.push(e),this.snapshots.length>this.maximum&&this.snapshots.shift(),t!==void 0&&Number.isFinite(t)){let n=e.tick-t*this.tickRate/1e3;this.offsetTicks!==null&&n=ic&&this.restartClock(),this.offsetTicks===null?(this.offsetTicks=n,this.need(t,this.initialDelayTicks)):this.offsetTicks=Math.max(n,this.offsetTicks-rc),this.need(t,this.offsetTicks-n+ec)}return!0}resync(){this.restartClock()}restartClock(){this.offsetTicks=null,this.delayTicks=this.initialDelayTicks,this.lastStarvationMs=-1/0,this.behindPushes=0,this.needs.length=0}need(e,t){for(;this.needs.length>0&&this.needs.at(-1).ticks<=t;)this.needs.pop();this.needs.push({atMs:e,ticks:t})}recentNeed(e){for(;this.needs.length>0&&this.needs[0].atMs<=e-$s;)this.needs.shift();return this.needs[0]?.ticks??0}latest(){return this.snapshots.at(-1)??null}renderTick(e){let t=this.latest();if(t===null)return 0;let n=this.snapshots[0].tick;if(this.offsetTicks===null)return Math.max(n,t.tick-1);let r=this.lastFrameMs===null?0:Math.min(nc,Math.max(0,e-this.lastFrameMs));this.lastFrameMs=e;let i=e*this.tickRate/1e3+this.offsetTicks-this.delayTicks;if(i>t.tick+.25)e-this.lastStarvationMs>250&&(this.lastStarvationMs=e,this.delayTicks=Math.min(Zs,this.delayTicks+Qs),this.need(e,this.delayTicks));else{let t=Math.max(Xs,this.recentNeed(e));this.delayTicks>t&&(this.delayTicks=Math.max(t,this.delayTicks-tc*r*this.tickRate/1e3))}return Math.min(t.tick,Math.max(n,i))}sample(e){let t=this.snapshots.findIndex(t=>t.tick>=e);if(t<=0)return this.snapshots[Math.max(0,t)]??this.latest();let n=this.snapshots[t-1],r=this.snapshots[t];if(n.phase!==r.phase||n.fighters.some((e,t)=>e.knockdowns!==r.fighters[t]?.knockdowns))return r;let i=Math.max(0,Math.min(1,(e-n.tick)/Math.max(1,r.tick-n.tick)));return{...r,fighters:[Ys(n.fighters[0],r.fighters[0],i),Ys(n.fighters[1],r.fighters[1],i)]}}clear(){this.snapshots.length=0,this.offsetTicks=null}},oc=class{highest=-1;accept(e){let t=new Set,n=e.filter(e=>e.event_id<=this.highest||t.has(e.event_id)?!1:(t.add(e.event_id),!0)).sort((e,t)=>e.event_id-t.event_id);for(let e of n)this.highest=Math.max(this.highest,e.event_id);return n}reset(){this.highest=-1}};function sc(e=window.location){let t=new URL(`/api/hands/ws`,e.origin);if(e.protocol===`https:`)t.protocol=`wss:`;else if(e.protocol===`http:`&&[`localhost`,`127.0.0.1`,`::1`].includes(e.hostname))t.protocol=`ws:`;else throw Error(`insecure_websocket_origin`);return t.search=``,t.hash=``,t.toString()}var cc=1,lc={moveX:0,moveY:0,defense:`none`,actions:[]},uc=33,dc=8,fc=50,pc=20,mc=2048;function hc(e){try{return Se(e)}catch(t){let n;try{let t=A(e);n=typeof t==`object`&&t&&`version`in t?t.version:void 0}catch{}throw typeof n==`number`&&n!==3?new Ee(`client_outdated`,!0):t}}var gc=class{getInput;callbacks;makeSocket;now;socket=null;reconnectTicket;reconnectTimer=null;opponentPauseTimer=null;inputTimer=null;reconnectDeadline=0;opponentPauseDeadline=0;opponentPaused=!1;attempts=0;nextSequence=0;playerId=null;sentAt=new Map;actionSequences=new Map;latencyMs=null;serverTick=0;role=null;active=!1;boutStarted=!1;disposed=!1;terminal=!1;inputSuppressed=!1;pointerFocus=!1;listenersBound=!1;lastInputSentAt=-1/0;sendTimes=[];edgeSendTimes=[];constructor(e,t,n,r=e=>new WebSocket(e),i=()=>performance.now()){this.getInput=t,this.callbacks=n,this.makeSocket=r,this.now=i,this.reconnectTicket=e}start(){this.disposed||this.listenersBound||(this.listenersBound=!0,window.addEventListener(`blur`,this.onInputLoss),window.addEventListener(`focus`,this.onInputRegain),window.addEventListener(`pointerdown`,this.onPagePointer,!0),document.addEventListener(`visibilitychange`,this.onVisibilityChange),this.connect(),!this.disposed&&!this.terminal&&(this.inputTimer=window.setInterval(()=>this.flushInput(),uc)))}setActive(e){this.setInputActive(e)}setInputActive(e){let t=this.active;this.active=e,e?this.boutStarted=!0:t&&this.sendInput(lc,!0)}notifyAction(){this.now()-(this.edgeSendTimes.at(-1)??-1/0)=pc||this.flushInput()&&this.edgeSendTimes.push(this.lastInputSentAt)}sendsInLastSecond(){return this.withinLastSecond(this.sendTimes)}withinLastSecond(e){let t=this.now()-1e3;for(;e.length>0&&e[0]<=t;)e.shift();return e.length}onInputLoss=()=>{this.inputSuppressed||(this.sendInput(lc,this.boutStarted),this.inputSuppressed=!0,this.pointerFocus=!1)};onInputRegain=()=>{!document.hidden&&document.hasFocus()&&(this.inputSuppressed=!1)};onPagePointer=()=>{if(!document.hidden&&(this.pointerFocus=!0,this.inputSuppressed=!1,!document.hasFocus()))try{window.focus()}catch{}};onVisibilityChange=()=>{document.hidden?this.onInputLoss():this.onInputRegain()};connect(){if(this.disposed||this.terminal)return;let e=this.reconnectTicket;if(e===null){this.terminal=!0,this.stopInputLifecycle(),this.callbacks.onFreshAuth();return}let t;try{t=this.makeSocket(sc())}catch{this.terminal=!0,this.stopInputLifecycle(),this.callbacks.onFatal(`network_unavailable`);return}this.socket=t,t.binaryType=`arraybuffer`,t.onopen=()=>{if(!(this.socket!==t||this.disposed))try{t.send(JSON.stringify({version:3,type:`authenticate`,ticket:e})),this.reconnectTicket=null}catch{this.handleClose(t)}},t.onmessage=e=>this.handleMessage(t,e),t.onerror=()=>void 0,t.onclose=()=>this.handleClose(t)}handleMessage(e,t){if(!(this.socket!==e||this.disposed||this.terminal))try{if(typeof t.data!=`string`&&!(t.data instanceof ArrayBuffer))throw Error(`unsupported_frame`);let n=hc(t.data);if(this.applyMessage(n),n.type===`ticket`){try{e.send(JSON.stringify({version:3,type:`ticket_ack`,refresh_id:n.refresh_id}))}catch{this.handleClose(e)}return}this.callbacks.onMessage(n),n.type===`error`?this.terminate(n.code,e):n.type===`final`&&(this.terminal=!0,this.clearTransportReconnect(!0),this.clearOpponentPause(!0),this.stopInputLifecycle(),this.socket===e&&(this.socket=null),e.onopen=e.onmessage=e.onclose=e.onerror=null,e.close(1e3,`complete`))}catch(e){this.callbacks.onFatal(Me(e)),this.dispose()}}applyMessage(e){if(e.type===`welcome`){if(e.reconnect_ticket===void 0)throw Error(`missing_reconnect_ticket`);this.reconnectTicket=e.reconnect_ticket,this.serverTick=e.server_tick,this.role=e.role,this.playerId=e.role===`fighter`?e.player_id:null,this.sentAt.clear(),e.role===`fighter`?this.nextSequence=Math.max(this.nextSequence,e.next_sequence):this.stopInputLifecycle(),this.attempts=0,this.clearTransportReconnect(!0)}else e.type===`ticket`?this.reconnectTicket=e.reconnect_ticket:e.type===`snapshot`?(this.serverTick=Math.max(this.serverTick,e.payload.tick),this.setInputActive([`countdown`,`fight`,`knockdown`,`foul_recovery`].includes(e.payload.phase)),this.observeAcknowledgement(e.payload)):e.type===`paused`?(this.setInputActive(!1),this.startOpponentPause(e.grace_ms)):e.type===`resumed`?(this.setInputActive(!0),this.attempts=0,this.clearOpponentPause(!0)):e.type===`ready`?this.setInputActive(!0):e.type===`waiting`?this.setInputActive(!1):(e.type===`final`||e.type===`error`)&&(this.active=!1,this.clearOpponentPause(!0))}terminate(e,t){this.terminal=!0,this.active=!1,this.clearTransportReconnect(!0),this.clearOpponentPause(!0),this.stopInputLifecycle(),this.callbacks.onFatal(e),this.socket===t&&(this.socket=null,t.onopen=t.onmessage=t.onclose=t.onerror=null,t.close(1e3,`terminal`))}handleClose(e){if(this.socket!==e||(this.socket=null,this.active=!1,this.disposed||this.terminal))return;if(this.reconnectTicket===null){this.terminal=!0,this.stopInputLifecycle(),this.callbacks.onFreshAuth();return}this.clearOpponentPause(!1),this.reconnectDeadline===0&&(this.reconnectDeadline=this.now()+2e4);let t=Math.max(0,this.reconnectDeadline-this.now());if(this.callbacks.onReconnect(t),t<=0){this.terminal=!0,this.stopInputLifecycle(),this.callbacks.onFreshAuth();return}let n=Math.min(3e3,250*2**Math.min(this.attempts,4),t);this.attempts+=1,this.reconnectTimer=window.setTimeout(()=>{this.reconnectTimer=null,this.callbacks.onReconnect(Math.max(0,this.reconnectDeadline-this.now())),this.connect()},n)}get inputLatencyMs(){return this.latencyMs}sequenceOf(e){return this.actionSequences.get(e)??null}observeAcknowledgement(e){if(this.playerId===null)return;let t=e.fighters.find(e=>e.player_id===this.playerId);if(t===void 0||t.last_input_sequence<0)return;let n=this.sentAt.get(t.last_input_sequence);if(n!==void 0){let e=Math.max(0,this.now()-n);this.latencyMs=this.latencyMs===null?e:this.latencyMs*.8+e*.2}for(let e of this.sentAt.keys())e<=t.last_input_sequence&&this.sentAt.delete(e)}sendInput(e,t=!1){let n=this.socket;if(this.role!==`fighter`||!this.active&&!t||this.disposed||this.terminal||n?.readyState!==cc)return!1;try{let t=e.actions.slice(0,4);n.send(Ce(this.nextSequence,this.serverTick,{...e,actions:t})),this.sentAt.set(this.nextSequence,this.now()),this.sentAt.size>128&&this.sentAt.delete(this.sentAt.keys().next().value);for(let e of t)e.id!==void 0&&this.actionSequences.set(e.id,this.nextSequence);for(;this.actionSequences.size>64;)this.actionSequences.delete(this.actionSequences.keys().next().value);return this.nextSequence+=1,this.lastInputSentAt=this.now(),this.sendTimes.push(this.lastInputSentAt),!0}catch{return this.handleClose(n),!1}}flushInput(){return this.inputSuppressed||document.hidden||!(this.pointerFocus||document.hasFocus())||this.sendsInLastSecond()>=fc||(this.socket?.bufferedAmount??0)>mc?!1:this.sendInput(this.getInput())}startOpponentPause(e){this.clearOpponentPause(!1),this.opponentPaused=!0,this.opponentPauseDeadline=this.now()+e,this.callbacks.onReconnect(e),e>0&&this.scheduleOpponentPauseTick()}scheduleOpponentPauseTick(){let e=Math.max(0,this.opponentPauseDeadline-this.now());if(e<=0){this.opponentPauseDeadline=0,this.callbacks.onReconnect(0);return}this.opponentPauseTimer=window.setTimeout(()=>{this.opponentPauseTimer=null;let e=Math.max(0,this.opponentPauseDeadline-this.now());this.callbacks.onReconnect(e),e>0?this.scheduleOpponentPauseTick():this.opponentPauseDeadline=0},Math.min(250,e))}clearOpponentPause(e){let t=this.opponentPaused;this.opponentPaused=!1,this.opponentPauseDeadline=0,this.opponentPauseTimer!==null&&(clearTimeout(this.opponentPauseTimer),this.opponentPauseTimer=null),e&&t&&this.callbacks.onReconnect(0)}cancelReconnect(){this.reconnectTimer!==null&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null)}clearTransportReconnect(e){let t=this.reconnectDeadline!==0;this.reconnectDeadline=0,this.cancelReconnect(),e&&t&&this.callbacks.onReconnect(0)}stopInputLifecycle(){this.inputTimer!==null&&(clearInterval(this.inputTimer),this.inputTimer=null),this.listenersBound&&=(window.removeEventListener(`blur`,this.onInputLoss),window.removeEventListener(`focus`,this.onInputRegain),window.removeEventListener(`pointerdown`,this.onPagePointer,!0),document.removeEventListener(`visibilitychange`,this.onVisibilityChange),!1)}dispose(){if(this.disposed)return;this.disposed=!0,this.active=!1,this.reconnectTicket=null,this.clearTransportReconnect(!0),this.clearOpponentPause(!0),this.stopInputLifecycle();let e=this.socket;this.socket=null,e!==null&&(e.onopen=e.onmessage=e.onclose=e.onerror=null,e.close(1e3,`teardown`))}},_c=`attached`,vc=1e3,yc=1001,bc=1002,xc=1003,Sc=1004,Cc=1005,wc=1006,Tc=1007,Ec=1008,Dc=1009,Oc=1010,kc=1011,Ac=1012,jc=1013,Mc=1014,Nc=1015,Pc=1016,Fc=1017,Ic=1018,Lc=1020,Rc=35902,zc=35899,Bc=1021,Vc=1022,Hc=1023,Uc=1026,Wc=1027,Gc=1028,Kc=1029,qc=1030,Jc=1031,Yc=1033,Xc=33776,Zc=33777,Qc=33778,$c=33779,el=35840,tl=35841,nl=35842,rl=35843,il=36196,al=37492,ol=37496,sl=37488,cl=37489,ll=37490,ul=37491,dl=37808,fl=37809,pl=37810,ml=37811,hl=37812,gl=37813,_l=37814,vl=37815,yl=37816,bl=37817,xl=37818,Sl=37819,Cl=37820,wl=37821,Tl=36492,El=36494,Dl=36495,Ol=36283,kl=36284,Al=36285,jl=36286,Ml=2300,Nl=2301,Pl=2302,Fl=2303,Il=2400,Ll=2401,Rl=2402,zl=2500,Bl=3200,Vl=`srgb`,Hl=`srgb-linear`,Ul=`linear`,Wl=`srgb`,Gl=7680,Kl=35044,ql=35048,Jl=2e3;function Yl(e){for(let t=e.length-1;t>=0;--t)if(e[t]>=65535)return!0;return!1}function Xl(e){return ArrayBuffer.isView(e)&&!(e instanceof DataView)}function Zl(e){return document.createElementNS(`http://www.w3.org/1999/xhtml`,e)}function Ql(){let e=Zl(`canvas`);return e.style.display=`block`,e}var $l={};function eu(...e){let t=`THREE.`+e.shift();console.log(t,...e)}function tu(e){let t=e[0];if(typeof t==`string`&&t.startsWith(`TSL:`)){let t=e[1];t&&t.isStackTrace?e[0]+=` `+t.getLocation():e[1]=`Stack trace not available. Enable "THREE.Node.captureStackTrace" to capture stack traces.`}return e}function nu(...e){e=tu(e);let t=`THREE.`+e.shift();{let n=e[0];n&&n.isStackTrace?console.warn(n.getError(t)):console.warn(t,...e)}}function ru(...e){e=tu(e);let t=`THREE.`+e.shift();{let n=e[0];n&&n.isStackTrace?console.error(n.getError(t)):console.error(t,...e)}}function iu(...e){let t=e.join(` `);t in $l||($l[t]=!0,nu(...e))}function au(e,t,n){return new Promise(function(r,i){function a(){switch(e.clientWaitSync(t,e.SYNC_FLUSH_COMMANDS_BIT,0)){case e.WAIT_FAILED:i();break;case e.TIMEOUT_EXPIRED:setTimeout(a,n);break;default:r()}}setTimeout(a,n)})}var ou={0:1,2:6,4:7,3:5,1:0,6:2,7:4,5:3},su=class{addEventListener(e,t){this._listeners===void 0&&(this._listeners={});let n=this._listeners;n[e]===void 0&&(n[e]=[]),n[e].indexOf(t)===-1&&n[e].push(t)}hasEventListener(e,t){let n=this._listeners;return n!==void 0&&n[e]!==void 0&&n[e].indexOf(t)!==-1}removeEventListener(e,t){let n=this._listeners;if(n===void 0)return;let r=n[e];if(r!==void 0){let e=r.indexOf(t);e!==-1&&r.splice(e,1)}}dispatchEvent(e){let t=this._listeners;if(t===void 0)return;let n=t[e.type];if(n!==void 0){e.target=this;let t=n.slice(0);for(let n=0,r=t.length;n>8&255]+cu[e>>16&255]+cu[e>>24&255]+`-`+cu[t&255]+cu[t>>8&255]+`-`+cu[t>>16&15|64]+cu[t>>24&255]+`-`+cu[n&63|128]+cu[n>>8&255]+`-`+cu[n>>16&255]+cu[n>>24&255]+cu[r&255]+cu[r>>8&255]+cu[r>>16&255]+cu[r>>24&255]).toLowerCase()}function pu(e,t,n){return Math.max(t,Math.min(n,e))}function mu(e,t){return(e%t+t)%t}function hu(e,t,n,r,i){return r+(e-t)*(i-r)/(n-t)}function gu(e,t,n){return e===t?0:(n-e)/(t-e)}function _u(e,t,n){return(1-n)*e+n*t}function vu(e,t,n,r){return _u(e,t,1-Math.exp(-n*r))}function yu(e,t=1){return t-Math.abs(mu(e,t*2)-t)}function bu(e,t,n){return e<=t?0:e>=n?1:(e=(e-t)/(n-t),e*e*(3-2*e))}function xu(e,t,n){return e<=t?0:e>=n?1:(e=(e-t)/(n-t),e*e*e*(e*(e*6-15)+10))}function Su(e,t){return e+Math.floor(Math.random()*(t-e+1))}function Cu(e,t){return e+Math.random()*(t-e)}function wu(e){return e*(.5-Math.random())}function Tu(e){e!==void 0&&(lu=e);let t=lu+=1831565813;return t=Math.imul(t^t>>>15,t|1),t^=t+Math.imul(t^t>>>7,t|61),((t^t>>>14)>>>0)/4294967296}function Eu(e){return e*uu}function Du(e){return e*du}function Ou(e){return e>0&&Number.isInteger(e)&&2**Math.round(Math.log2(e))===e}function ku(e){return 2**Math.ceil(Math.log(e)/Math.LN2)}function Au(e){return 2**Math.floor(Math.log(e)/Math.LN2)}function ju(e,t,n,r,i){let a=Math.cos,o=Math.sin,s=a(n/2),c=o(n/2),l=a((t+r)/2),u=o((t+r)/2),d=a((t-r)/2),f=o((t-r)/2),p=a((r-t)/2),m=o((r-t)/2);switch(i){case`XYX`:e.set(s*u,c*d,c*f,s*l);break;case`YZY`:e.set(c*f,s*u,c*d,s*l);break;case`ZXZ`:e.set(c*d,c*f,s*u,s*l);break;case`XZX`:e.set(s*u,c*m,c*p,s*l);break;case`YXY`:e.set(c*p,s*u,c*m,s*l);break;case`ZYZ`:e.set(c*m,c*p,s*u,s*l);break;default:nu(`MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: `+i)}}function Mu(e,t){switch(t.constructor){case Float32Array:return e;case Uint32Array:return e/4294967295;case Uint16Array:return e/65535;case Uint8Array:case Uint8ClampedArray:return e/255;case Int32Array:return Math.max(e/2147483647,-1);case Int16Array:return Math.max(e/32767,-1);case Int8Array:return Math.max(e/127,-1);default:throw Error(`THREE.MathUtils: Invalid component type.`)}}function Nu(e,t){switch(t.constructor){case Float32Array:return e;case Uint32Array:return Math.round(e*4294967295);case Uint16Array:return Math.round(e*65535);case Uint8Array:case Uint8ClampedArray:return Math.round(e*255);case Int32Array:return Math.round(e*2147483647);case Int16Array:return Math.round(e*32767);case Int8Array:return Math.round(e*127);default:throw Error(`THREE.MathUtils: Invalid component type.`)}}var Pu={DEG2RAD:uu,RAD2DEG:du,generateUUID:fu,clamp:pu,euclideanModulo:mu,mapLinear:hu,inverseLerp:gu,lerp:_u,damp:vu,pingpong:yu,smoothstep:bu,smootherstep:xu,randInt:Su,randFloat:Cu,randFloatSpread:wu,seededRandom:Tu,degToRad:Eu,radToDeg:Du,isPowerOfTwo:Ou,ceilPowerOfTwo:ku,floorPowerOfTwo:Au,setQuaternionFromProperEuler:ju,normalize:Nu,denormalize:Mu},X=class e{static{e.prototype.isVector2=!0}constructor(e=0,t=0){this.x=e,this.y=t}get width(){return this.x}set width(e){this.x=e}get height(){return this.y}set height(e){this.y=e}set(e,t){return this.x=e,this.y=t,this}setScalar(e){return this.x=e,this.y=e,this}setX(e){return this.x=e,this}setY(e){return this.y=e,this}setComponent(e,t){switch(e){case 0:this.x=t;break;case 1:this.y=t;break;default:throw Error(`THREE.Vector2: index is out of range: `+e)}return this}getComponent(e){switch(e){case 0:return this.x;case 1:return this.y;default:throw Error(`THREE.Vector2: index is out of range: `+e)}}clone(){return new this.constructor(this.x,this.y)}copy(e){return this.x=e.x,this.y=e.y,this}add(e){return this.x+=e.x,this.y+=e.y,this}addScalar(e){return this.x+=e,this.y+=e,this}addVectors(e,t){return this.x=e.x+t.x,this.y=e.y+t.y,this}addScaledVector(e,t){return this.x+=e.x*t,this.y+=e.y*t,this}sub(e){return this.x-=e.x,this.y-=e.y,this}subScalar(e){return this.x-=e,this.y-=e,this}subVectors(e,t){return this.x=e.x-t.x,this.y=e.y-t.y,this}multiply(e){return this.x*=e.x,this.y*=e.y,this}multiplyScalar(e){return this.x*=e,this.y*=e,this}divide(e){return this.x/=e.x,this.y/=e.y,this}divideScalar(e){return this.multiplyScalar(1/e)}applyMatrix3(e){let t=this.x,n=this.y,r=e.elements;return this.x=r[0]*t+r[3]*n+r[6],this.y=r[1]*t+r[4]*n+r[7],this}min(e){return this.x=Math.min(this.x,e.x),this.y=Math.min(this.y,e.y),this}max(e){return this.x=Math.max(this.x,e.x),this.y=Math.max(this.y,e.y),this}clamp(e,t){return this.x=pu(this.x,e.x,t.x),this.y=pu(this.y,e.y,t.y),this}clampScalar(e,t){return this.x=pu(this.x,e,t),this.y=pu(this.y,e,t),this}clampLength(e,t){let n=this.length();return this.divideScalar(n||1).multiplyScalar(pu(n,e,t))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this}negate(){return this.x=-this.x,this.y=-this.y,this}dot(e){return this.x*e.x+this.y*e.y}cross(e){return this.x*e.y-this.y*e.x}lengthSq(){return this.x*this.x+this.y*this.y}length(){return Math.sqrt(this.x*this.x+this.y*this.y)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)}normalize(){return this.divideScalar(this.length()||1)}angle(){return Math.atan2(-this.y,-this.x)+Math.PI}angleTo(e){let t=Math.sqrt(this.lengthSq()*e.lengthSq());if(t===0)return Math.PI/2;let n=this.dot(e)/t;return Math.acos(pu(n,-1,1))}distanceTo(e){return Math.sqrt(this.distanceToSquared(e))}distanceToSquared(e){let t=this.x-e.x,n=this.y-e.y;return t*t+n*n}manhattanDistanceTo(e){return Math.abs(this.x-e.x)+Math.abs(this.y-e.y)}setLength(e){return this.normalize().multiplyScalar(e)}lerp(e,t){return this.x+=(e.x-this.x)*t,this.y+=(e.y-this.y)*t,this}lerpVectors(e,t,n){return this.x=e.x+(t.x-e.x)*n,this.y=e.y+(t.y-e.y)*n,this}equals(e){return e.x===this.x&&e.y===this.y}fromArray(e,t=0){return this.x=e[t],this.y=e[t+1],this}toArray(e=[],t=0){return e[t]=this.x,e[t+1]=this.y,e}fromBufferAttribute(e,t){return this.x=e.getX(t),this.y=e.getY(t),this}rotateAround(e,t){let n=Math.cos(t),r=Math.sin(t),i=this.x-e.x,a=this.y-e.y;return this.x=i*n-a*r+e.x,this.y=i*r+a*n+e.y,this}random(){return this.x=Math.random(),this.y=Math.random(),this}*[Symbol.iterator](){yield this.x,yield this.y}},Fu=class{constructor(e=0,t=0,n=0,r=1){this.isQuaternion=!0,this._x=e,this._y=t,this._z=n,this._w=r}static slerpFlat(e,t,n,r,i,a,o){let s=n[r+0],c=n[r+1],l=n[r+2],u=n[r+3],d=i[a+0],f=i[a+1],p=i[a+2],m=i[a+3];if(u!==m||s!==d||c!==f||l!==p){let e=s*d+c*f+l*p+u*m;e<0&&(d=-d,f=-f,p=-p,m=-m,e=-e);let t=1-o;if(e<.9995){let n=Math.acos(e),r=Math.sin(n);t=Math.sin(t*n)/r,o=Math.sin(o*n)/r,s=s*t+d*o,c=c*t+f*o,l=l*t+p*o,u=u*t+m*o}else{s=s*t+d*o,c=c*t+f*o,l=l*t+p*o,u=u*t+m*o;let e=1/Math.sqrt(s*s+c*c+l*l+u*u);s*=e,c*=e,l*=e,u*=e}}e[t]=s,e[t+1]=c,e[t+2]=l,e[t+3]=u}static multiplyQuaternionsFlat(e,t,n,r,i,a){let o=n[r],s=n[r+1],c=n[r+2],l=n[r+3],u=i[a],d=i[a+1],f=i[a+2],p=i[a+3];return e[t]=o*p+l*u+s*f-c*d,e[t+1]=s*p+l*d+c*u-o*f,e[t+2]=c*p+l*f+o*d-s*u,e[t+3]=l*p-o*u-s*d-c*f,e}get x(){return this._x}set x(e){this._x=e,this._onChangeCallback()}get y(){return this._y}set y(e){this._y=e,this._onChangeCallback()}get z(){return this._z}set z(e){this._z=e,this._onChangeCallback()}get w(){return this._w}set w(e){this._w=e,this._onChangeCallback()}set(e,t,n,r){return this._x=e,this._y=t,this._z=n,this._w=r,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._w)}copy(e){return this._x=e.x,this._y=e.y,this._z=e.z,this._w=e.w,this._onChangeCallback(),this}setFromEuler(e,t=!0){let n=e._x,r=e._y,i=e._z,a=e._order,o=Math.cos,s=Math.sin,c=o(n/2),l=o(r/2),u=o(i/2),d=s(n/2),f=s(r/2),p=s(i/2);switch(a){case`XYZ`:this._x=d*l*u+c*f*p,this._y=c*f*u-d*l*p,this._z=c*l*p+d*f*u,this._w=c*l*u-d*f*p;break;case`YXZ`:this._x=d*l*u+c*f*p,this._y=c*f*u-d*l*p,this._z=c*l*p-d*f*u,this._w=c*l*u+d*f*p;break;case`ZXY`:this._x=d*l*u-c*f*p,this._y=c*f*u+d*l*p,this._z=c*l*p+d*f*u,this._w=c*l*u-d*f*p;break;case`ZYX`:this._x=d*l*u-c*f*p,this._y=c*f*u+d*l*p,this._z=c*l*p-d*f*u,this._w=c*l*u+d*f*p;break;case`YZX`:this._x=d*l*u+c*f*p,this._y=c*f*u+d*l*p,this._z=c*l*p-d*f*u,this._w=c*l*u-d*f*p;break;case`XZY`:this._x=d*l*u-c*f*p,this._y=c*f*u-d*l*p,this._z=c*l*p+d*f*u,this._w=c*l*u+d*f*p;break;default:nu(`Quaternion: .setFromEuler() encountered an unknown order: `+a)}return t===!0&&this._onChangeCallback(),this}setFromAxisAngle(e,t){let n=t/2,r=Math.sin(n);return this._x=e.x*r,this._y=e.y*r,this._z=e.z*r,this._w=Math.cos(n),this._onChangeCallback(),this}setFromRotationMatrix(e){let t=e.elements,n=t[0],r=t[4],i=t[8],a=t[1],o=t[5],s=t[9],c=t[2],l=t[6],u=t[10],d=n+o+u;if(d>0){let e=.5/Math.sqrt(d+1);this._w=.25/e,this._x=(l-s)*e,this._y=(i-c)*e,this._z=(a-r)*e}else if(n>o&&n>u){let e=2*Math.sqrt(1+n-o-u);this._w=(l-s)/e,this._x=.25*e,this._y=(r+a)/e,this._z=(i+c)/e}else if(o>u){let e=2*Math.sqrt(1+o-n-u);this._w=(i-c)/e,this._x=(r+a)/e,this._y=.25*e,this._z=(s+l)/e}else{let e=2*Math.sqrt(1+u-n-o);this._w=(a-r)/e,this._x=(i+c)/e,this._y=(s+l)/e,this._z=.25*e}return this._onChangeCallback(),this}setFromUnitVectors(e,t){let n=e.dot(t)+1;return n<1e-8?(n=0,Math.abs(e.x)>Math.abs(e.z)?(this._x=-e.y,this._y=e.x,this._z=0,this._w=n):(this._x=0,this._y=-e.z,this._z=e.y,this._w=n)):(this._x=e.y*t.z-e.z*t.y,this._y=e.z*t.x-e.x*t.z,this._z=e.x*t.y-e.y*t.x,this._w=n),this.normalize()}angleTo(e){return 2*Math.acos(Math.abs(pu(this.dot(e),-1,1)))}rotateTowards(e,t){let n=this.angleTo(e);if(n===0)return this;let r=Math.min(1,t/n);return this.slerp(e,r),this}identity(){return this.set(0,0,0,1)}invert(){return this.conjugate()}conjugate(){return this._x*=-1,this._y*=-1,this._z*=-1,this._onChangeCallback(),this}dot(e){return this._x*e._x+this._y*e._y+this._z*e._z+this._w*e._w}lengthSq(){return this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w}length(){return Math.sqrt(this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w)}normalize(){let e=this.length();return e===0?(this._x=0,this._y=0,this._z=0,this._w=1):(e=1/e,this._x*=e,this._y*=e,this._z*=e,this._w*=e),this._onChangeCallback(),this}multiply(e){return this.multiplyQuaternions(this,e)}premultiply(e){return this.multiplyQuaternions(e,this)}multiplyQuaternions(e,t){let n=e._x,r=e._y,i=e._z,a=e._w,o=t._x,s=t._y,c=t._z,l=t._w;return this._x=n*l+a*o+r*c-i*s,this._y=r*l+a*s+i*o-n*c,this._z=i*l+a*c+n*s-r*o,this._w=a*l-n*o-r*s-i*c,this._onChangeCallback(),this}slerp(e,t){let n=e._x,r=e._y,i=e._z,a=e._w,o=this.dot(e);o<0&&(n=-n,r=-r,i=-i,a=-a,o=-o);let s=1-t;if(o<.9995){let e=Math.acos(o),c=Math.sin(e);s=Math.sin(s*e)/c,t=Math.sin(t*e)/c,this._x=this._x*s+n*t,this._y=this._y*s+r*t,this._z=this._z*s+i*t,this._w=this._w*s+a*t,this._onChangeCallback()}else this._x=this._x*s+n*t,this._y=this._y*s+r*t,this._z=this._z*s+i*t,this._w=this._w*s+a*t,this.normalize();return this}slerpQuaternions(e,t,n){return this.copy(e).slerp(t,n)}random(){let e=2*Math.PI*Math.random(),t=2*Math.PI*Math.random(),n=Math.random(),r=Math.sqrt(1-n),i=Math.sqrt(n);return this.set(r*Math.sin(e),r*Math.cos(e),i*Math.sin(t),i*Math.cos(t))}equals(e){return e._x===this._x&&e._y===this._y&&e._z===this._z&&e._w===this._w}fromArray(e,t=0){return this._x=e[t],this._y=e[t+1],this._z=e[t+2],this._w=e[t+3],this._onChangeCallback(),this}toArray(e=[],t=0){return e[t]=this._x,e[t+1]=this._y,e[t+2]=this._z,e[t+3]=this._w,e}fromBufferAttribute(e,t){return this._x=e.getX(t),this._y=e.getY(t),this._z=e.getZ(t),this._w=e.getW(t),this._onChangeCallback(),this}toJSON(){return this.toArray()}_onChange(e){return this._onChangeCallback=e,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._w}},Z=class e{static{e.prototype.isVector3=!0}constructor(e=0,t=0,n=0){this.x=e,this.y=t,this.z=n}set(e,t,n){return n===void 0&&(n=this.z),this.x=e,this.y=t,this.z=n,this}setScalar(e){return this.x=e,this.y=e,this.z=e,this}setX(e){return this.x=e,this}setY(e){return this.y=e,this}setZ(e){return this.z=e,this}setComponent(e,t){switch(e){case 0:this.x=t;break;case 1:this.y=t;break;case 2:this.z=t;break;default:throw Error(`THREE.Vector3: index is out of range: `+e)}return this}getComponent(e){switch(e){case 0:return this.x;case 1:return this.y;case 2:return this.z;default:throw Error(`THREE.Vector3: index is out of range: `+e)}}clone(){return new this.constructor(this.x,this.y,this.z)}copy(e){return this.x=e.x,this.y=e.y,this.z=e.z,this}add(e){return this.x+=e.x,this.y+=e.y,this.z+=e.z,this}addScalar(e){return this.x+=e,this.y+=e,this.z+=e,this}addVectors(e,t){return this.x=e.x+t.x,this.y=e.y+t.y,this.z=e.z+t.z,this}addScaledVector(e,t){return this.x+=e.x*t,this.y+=e.y*t,this.z+=e.z*t,this}sub(e){return this.x-=e.x,this.y-=e.y,this.z-=e.z,this}subScalar(e){return this.x-=e,this.y-=e,this.z-=e,this}subVectors(e,t){return this.x=e.x-t.x,this.y=e.y-t.y,this.z=e.z-t.z,this}multiply(e){return this.x*=e.x,this.y*=e.y,this.z*=e.z,this}multiplyScalar(e){return this.x*=e,this.y*=e,this.z*=e,this}multiplyVectors(e,t){return this.x=e.x*t.x,this.y=e.y*t.y,this.z=e.z*t.z,this}applyEuler(e){return this.applyQuaternion(Lu.setFromEuler(e))}applyAxisAngle(e,t){return this.applyQuaternion(Lu.setFromAxisAngle(e,t))}applyMatrix3(e){let t=this.x,n=this.y,r=this.z,i=e.elements;return this.x=i[0]*t+i[3]*n+i[6]*r,this.y=i[1]*t+i[4]*n+i[7]*r,this.z=i[2]*t+i[5]*n+i[8]*r,this}applyNormalMatrix(e){return this.applyMatrix3(e).normalize()}applyMatrix4(e){let t=this.x,n=this.y,r=this.z,i=e.elements,a=1/(i[3]*t+i[7]*n+i[11]*r+i[15]);return this.x=(i[0]*t+i[4]*n+i[8]*r+i[12])*a,this.y=(i[1]*t+i[5]*n+i[9]*r+i[13])*a,this.z=(i[2]*t+i[6]*n+i[10]*r+i[14])*a,this}applyQuaternion(e){let t=this.x,n=this.y,r=this.z,i=e.x,a=e.y,o=e.z,s=e.w,c=2*(a*r-o*n),l=2*(o*t-i*r),u=2*(i*n-a*t);return this.x=t+s*c+a*u-o*l,this.y=n+s*l+o*c-i*u,this.z=r+s*u+i*l-a*c,this}project(e){return this.applyMatrix4(e.matrixWorldInverse).applyMatrix4(e.projectionMatrix)}unproject(e){return this.applyMatrix4(e.projectionMatrixInverse).applyMatrix4(e.matrixWorld)}transformDirection(e){let t=this.x,n=this.y,r=this.z,i=e.elements;return this.x=i[0]*t+i[4]*n+i[8]*r,this.y=i[1]*t+i[5]*n+i[9]*r,this.z=i[2]*t+i[6]*n+i[10]*r,this.normalize()}divide(e){return this.x/=e.x,this.y/=e.y,this.z/=e.z,this}divideScalar(e){return this.multiplyScalar(1/e)}min(e){return this.x=Math.min(this.x,e.x),this.y=Math.min(this.y,e.y),this.z=Math.min(this.z,e.z),this}max(e){return this.x=Math.max(this.x,e.x),this.y=Math.max(this.y,e.y),this.z=Math.max(this.z,e.z),this}clamp(e,t){return this.x=pu(this.x,e.x,t.x),this.y=pu(this.y,e.y,t.y),this.z=pu(this.z,e.z,t.z),this}clampScalar(e,t){return this.x=pu(this.x,e,t),this.y=pu(this.y,e,t),this.z=pu(this.z,e,t),this}clampLength(e,t){let n=this.length();return this.divideScalar(n||1).multiplyScalar(pu(n,e,t))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this.z=Math.trunc(this.z),this}negate(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this}dot(e){return this.x*e.x+this.y*e.y+this.z*e.z}lengthSq(){return this.x*this.x+this.y*this.y+this.z*this.z}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)}normalize(){return this.divideScalar(this.length()||1)}setLength(e){return this.normalize().multiplyScalar(e)}lerp(e,t){return this.x+=(e.x-this.x)*t,this.y+=(e.y-this.y)*t,this.z+=(e.z-this.z)*t,this}lerpVectors(e,t,n){return this.x=e.x+(t.x-e.x)*n,this.y=e.y+(t.y-e.y)*n,this.z=e.z+(t.z-e.z)*n,this}cross(e){return this.crossVectors(this,e)}crossVectors(e,t){let n=e.x,r=e.y,i=e.z,a=t.x,o=t.y,s=t.z;return this.x=r*s-i*o,this.y=i*a-n*s,this.z=n*o-r*a,this}projectOnVector(e){let t=e.lengthSq();if(t===0)return this.set(0,0,0);let n=e.dot(this)/t;return this.copy(e).multiplyScalar(n)}projectOnPlane(e){return Iu.copy(this).projectOnVector(e),this.sub(Iu)}reflect(e){return this.sub(Iu.copy(e).multiplyScalar(2*this.dot(e)))}angleTo(e){let t=Math.sqrt(this.lengthSq()*e.lengthSq());if(t===0)return Math.PI/2;let n=this.dot(e)/t;return Math.acos(pu(n,-1,1))}distanceTo(e){return Math.sqrt(this.distanceToSquared(e))}distanceToSquared(e){let t=this.x-e.x,n=this.y-e.y,r=this.z-e.z;return t*t+n*n+r*r}manhattanDistanceTo(e){return Math.abs(this.x-e.x)+Math.abs(this.y-e.y)+Math.abs(this.z-e.z)}setFromSpherical(e){return this.setFromSphericalCoords(e.radius,e.phi,e.theta)}setFromSphericalCoords(e,t,n){let r=Math.sin(t)*e;return this.x=r*Math.sin(n),this.y=Math.cos(t)*e,this.z=r*Math.cos(n),this}setFromCylindrical(e){return this.setFromCylindricalCoords(e.radius,e.theta,e.y)}setFromCylindricalCoords(e,t,n){return this.x=e*Math.sin(t),this.y=n,this.z=e*Math.cos(t),this}setFromMatrixPosition(e){let t=e.elements;return this.x=t[12],this.y=t[13],this.z=t[14],this}setFromMatrixScale(e){let t=this.setFromMatrixColumn(e,0).length(),n=this.setFromMatrixColumn(e,1).length(),r=this.setFromMatrixColumn(e,2).length();return this.x=t,this.y=n,this.z=r,this}setFromMatrixColumn(e,t){return this.fromArray(e.elements,t*4)}setFromMatrix3Column(e,t){return this.fromArray(e.elements,t*3)}setFromEuler(e){return this.x=e._x,this.y=e._y,this.z=e._z,this}setFromColor(e){return this.x=e.r,this.y=e.g,this.z=e.b,this}equals(e){return e.x===this.x&&e.y===this.y&&e.z===this.z}fromArray(e,t=0){return this.x=e[t],this.y=e[t+1],this.z=e[t+2],this}toArray(e=[],t=0){return e[t]=this.x,e[t+1]=this.y,e[t+2]=this.z,e}fromBufferAttribute(e,t){return this.x=e.getX(t),this.y=e.getY(t),this.z=e.getZ(t),this}random(){return this.x=Math.random(),this.y=Math.random(),this.z=Math.random(),this}randomDirection(){let e=Math.random()*Math.PI*2,t=Math.random()*2-1,n=Math.sqrt(1-t*t);return this.x=n*Math.cos(e),this.y=t,this.z=n*Math.sin(e),this}*[Symbol.iterator](){yield this.x,yield this.y,yield this.z}},Iu=new Z,Lu=new Fu,Ru=class e{static{e.prototype.isMatrix3=!0}constructor(e,t,n,r,i,a,o,s,c){this.elements=[1,0,0,0,1,0,0,0,1],e!==void 0&&this.set(e,t,n,r,i,a,o,s,c)}set(e,t,n,r,i,a,o,s,c){let l=this.elements;return l[0]=e,l[1]=r,l[2]=o,l[3]=t,l[4]=i,l[5]=s,l[6]=n,l[7]=a,l[8]=c,this}identity(){return this.set(1,0,0,0,1,0,0,0,1),this}copy(e){let t=this.elements,n=e.elements;return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t[4]=n[4],t[5]=n[5],t[6]=n[6],t[7]=n[7],t[8]=n[8],this}extractBasis(e,t,n){return e.setFromMatrix3Column(this,0),t.setFromMatrix3Column(this,1),n.setFromMatrix3Column(this,2),this}setFromMatrix4(e){let t=e.elements;return this.set(t[0],t[4],t[8],t[1],t[5],t[9],t[2],t[6],t[10]),this}multiply(e){return this.multiplyMatrices(this,e)}premultiply(e){return this.multiplyMatrices(e,this)}multiplyMatrices(e,t){let n=e.elements,r=t.elements,i=this.elements,a=n[0],o=n[3],s=n[6],c=n[1],l=n[4],u=n[7],d=n[2],f=n[5],p=n[8],m=r[0],h=r[3],g=r[6],_=r[1],v=r[4],y=r[7],b=r[2],x=r[5],S=r[8];return i[0]=a*m+o*_+s*b,i[3]=a*h+o*v+s*x,i[6]=a*g+o*y+s*S,i[1]=c*m+l*_+u*b,i[4]=c*h+l*v+u*x,i[7]=c*g+l*y+u*S,i[2]=d*m+f*_+p*b,i[5]=d*h+f*v+p*x,i[8]=d*g+f*y+p*S,this}multiplyScalar(e){let t=this.elements;return t[0]*=e,t[3]*=e,t[6]*=e,t[1]*=e,t[4]*=e,t[7]*=e,t[2]*=e,t[5]*=e,t[8]*=e,this}determinant(){let e=this.elements,t=e[0],n=e[1],r=e[2],i=e[3],a=e[4],o=e[5],s=e[6],c=e[7],l=e[8];return t*a*l-t*o*c-n*i*l+n*o*s+r*i*c-r*a*s}invert(){let e=this.elements,t=e[0],n=e[1],r=e[2],i=e[3],a=e[4],o=e[5],s=e[6],c=e[7],l=e[8],u=l*a-o*c,d=o*s-l*i,f=c*i-a*s,p=t*u+n*d+r*f;if(p===0)return this.set(0,0,0,0,0,0,0,0,0);let m=1/p;return e[0]=u*m,e[1]=(r*c-l*n)*m,e[2]=(o*n-r*a)*m,e[3]=d*m,e[4]=(l*t-r*s)*m,e[5]=(r*i-o*t)*m,e[6]=f*m,e[7]=(n*s-c*t)*m,e[8]=(a*t-n*i)*m,this}transpose(){let e,t=this.elements;return e=t[1],t[1]=t[3],t[3]=e,e=t[2],t[2]=t[6],t[6]=e,e=t[5],t[5]=t[7],t[7]=e,this}getNormalMatrix(e){return this.setFromMatrix4(e).invert().transpose()}transposeIntoArray(e){let t=this.elements;return e[0]=t[0],e[1]=t[3],e[2]=t[6],e[3]=t[1],e[4]=t[4],e[5]=t[7],e[6]=t[2],e[7]=t[5],e[8]=t[8],this}setUvTransform(e,t,n,r,i,a,o){let s=Math.cos(i),c=Math.sin(i);return this.set(n*s,n*c,-n*(s*a+c*o)+a+e,-r*c,r*s,-r*(-c*a+s*o)+o+t,0,0,1),this}scale(e,t){return iu(`Matrix3: .scale() is deprecated. Use .makeScale() instead.`),this.premultiply(zu.makeScale(e,t)),this}rotate(e){return iu(`Matrix3: .rotate() is deprecated. Use .makeRotation() instead.`),this.premultiply(zu.makeRotation(-e)),this}translate(e,t){return iu(`Matrix3: .translate() is deprecated. Use .makeTranslation() instead.`),this.premultiply(zu.makeTranslation(e,t)),this}makeTranslation(e,t){return e.isVector2?this.set(1,0,e.x,0,1,e.y,0,0,1):this.set(1,0,e,0,1,t,0,0,1),this}makeRotation(e){let t=Math.cos(e),n=Math.sin(e);return this.set(t,-n,0,n,t,0,0,0,1),this}makeScale(e,t){return this.set(e,0,0,0,t,0,0,0,1),this}equals(e){let t=this.elements,n=e.elements;for(let e=0;e<9;e++)if(t[e]!==n[e])return!1;return!0}fromArray(e,t=0){for(let n=0;n<9;n++)this.elements[n]=e[n+t];return this}toArray(e=[],t=0){let n=this.elements;return e[t]=n[0],e[t+1]=n[1],e[t+2]=n[2],e[t+3]=n[3],e[t+4]=n[4],e[t+5]=n[5],e[t+6]=n[6],e[t+7]=n[7],e[t+8]=n[8],e}clone(){return new this.constructor().fromArray(this.elements)}},zu=new Ru,Bu=new Ru().set(.4123908,.3575843,.1804808,.212639,.7151687,.0721923,.0193308,.1191948,.9505322),Vu=new Ru().set(3.2409699,-1.5373832,-.4986108,-.9692436,1.8759675,.0415551,.0556301,-.203977,1.0569715);function Hu(){let e={enabled:!0,workingColorSpace:Hl,spaces:{},convert:function(e,t,n){return this.enabled===!1||t===n||!t||!n?e:(this.spaces[t].transfer===`srgb`&&(e.r=Wu(e.r),e.g=Wu(e.g),e.b=Wu(e.b)),this.spaces[t].primaries!==this.spaces[n].primaries&&(e.applyMatrix3(this.spaces[t].toXYZ),e.applyMatrix3(this.spaces[n].fromXYZ)),this.spaces[n].transfer===`srgb`&&(e.r=Gu(e.r),e.g=Gu(e.g),e.b=Gu(e.b)),e)},workingToColorSpace:function(e,t){return this.convert(e,this.workingColorSpace,t)},colorSpaceToWorking:function(e,t){return this.convert(e,t,this.workingColorSpace)},getPrimaries:function(e){return this.spaces[e].primaries},getTransfer:function(e){return e===``?Ul:this.spaces[e].transfer},getToneMappingMode:function(e){return this.spaces[e].outputColorSpaceConfig.toneMappingMode||`standard`},getLuminanceCoefficients:function(e,t=this.workingColorSpace){return e.fromArray(this.spaces[t].luminanceCoefficients)},define:function(e){Object.assign(this.spaces,e)},_getMatrix:function(e,t,n){return e.copy(this.spaces[t].toXYZ).multiply(this.spaces[n].fromXYZ)},_getDrawingBufferColorSpace:function(e){return this.spaces[e].outputColorSpaceConfig.drawingBufferColorSpace},_getUnpackColorSpace:function(e=this.workingColorSpace){return this.spaces[e].workingColorSpaceConfig.unpackColorSpace},fromWorkingColorSpace:function(t,n){return iu(`ColorManagement: .fromWorkingColorSpace() has been renamed to .workingToColorSpace().`),e.workingToColorSpace(t,n)},toWorkingColorSpace:function(t,n){return iu(`ColorManagement: .toWorkingColorSpace() has been renamed to .colorSpaceToWorking().`),e.colorSpaceToWorking(t,n)}},t=[.64,.33,.3,.6,.15,.06],n=[.2126,.7152,.0722],r=[.3127,.329];return e.define({[Hl]:{primaries:t,whitePoint:r,transfer:Ul,toXYZ:Bu,fromXYZ:Vu,luminanceCoefficients:n,workingColorSpaceConfig:{unpackColorSpace:Vl},outputColorSpaceConfig:{drawingBufferColorSpace:Vl}},[Vl]:{primaries:t,whitePoint:r,transfer:Wl,toXYZ:Bu,fromXYZ:Vu,luminanceCoefficients:n,outputColorSpaceConfig:{drawingBufferColorSpace:Vl}}}),e}var Uu=Hu();function Wu(e){return e<.04045?e*.0773993808:(e*.9478672986+.0521327014)**2.4}function Gu(e){return e<.0031308?e*12.92:1.055*e**.41666-.055}var Ku,qu=class{static getDataURL(e,t=`image/png`){if(/^data:/i.test(e.src)||typeof HTMLCanvasElement>`u`)return e.src;let n;if(e instanceof HTMLCanvasElement)n=e;else{Ku===void 0&&(Ku=Zl(`canvas`)),Ku.width=e.width,Ku.height=e.height;let t=Ku.getContext(`2d`);e instanceof ImageData?t.putImageData(e,0,0):t.drawImage(e,0,0,e.width,e.height),n=Ku}return n.toDataURL(t)}static sRGBToLinear(e){if(typeof HTMLImageElement<`u`&&e instanceof HTMLImageElement||typeof HTMLCanvasElement<`u`&&e instanceof HTMLCanvasElement||typeof ImageBitmap<`u`&&e instanceof ImageBitmap){let t=Zl(`canvas`);t.width=e.width,t.height=e.height;let n=t.getContext(`2d`);n.drawImage(e,0,0,e.width,e.height);let r=n.getImageData(0,0,e.width,e.height),i=r.data;for(let e=0;e1),this.pmremVersion=0,this.normalized=!1}get width(){return this.source.getSize(Qu).x}get height(){return this.source.getSize(Qu).y}get depth(){return this.source.getSize(Qu).z}get image(){return this.source.data}set image(e){this.source.data=e}updateMatrix(){this.matrix.setUvTransform(this.offset.x,this.offset.y,this.repeat.x,this.repeat.y,this.rotation,this.center.x,this.center.y)}addUpdateRange(e,t){this.updateRanges.push({start:e,count:t})}clearUpdateRanges(){this.updateRanges.length=0}clone(){return new this.constructor().copy(this)}copy(e){return this.name=e.name,this.source=e.source,this.mipmaps=e.mipmaps.slice(0),this.mapping=e.mapping,this.channel=e.channel,this.wrapS=e.wrapS,this.wrapT=e.wrapT,this.magFilter=e.magFilter,this.minFilter=e.minFilter,this.anisotropy=e.anisotropy,this.format=e.format,this.internalFormat=e.internalFormat,this.type=e.type,this.normalized=e.normalized,this.offset.copy(e.offset),this.repeat.copy(e.repeat),this.center.copy(e.center),this.rotation=e.rotation,this.matrixAutoUpdate=e.matrixAutoUpdate,this.matrix.copy(e.matrix),this.generateMipmaps=e.generateMipmaps,this.premultiplyAlpha=e.premultiplyAlpha,this.flipY=e.flipY,this.unpackAlignment=e.unpackAlignment,this.colorSpace=e.colorSpace,this.renderTarget=e.renderTarget,this.isRenderTargetTexture=e.isRenderTargetTexture,this.isArrayTexture=e.isArrayTexture,this.userData=JSON.parse(JSON.stringify(e.userData)),this.needsUpdate=!0,this}setValues(e){for(let t in e){let n=e[t];if(n===void 0){nu(`Texture.setValues(): parameter '${t}' has value of undefined.`);continue}let r=this[t];if(r===void 0){nu(`Texture.setValues(): property '${t}' does not exist.`);continue}r&&n&&r.isVector2&&n.isVector2||r&&n&&r.isVector3&&n.isVector3||r&&n&&r.isMatrix3&&n.isMatrix3?r.copy(n):this[t]=n}}toJSON(e){let t=e===void 0||typeof e==`string`;if(!t&&e.textures[this.uuid]!==void 0)return e.textures[this.uuid];let n={metadata:{version:4.7,type:`Texture`,generator:`Texture.toJSON`},uuid:this.uuid,name:this.name,image:this.source.toJSON(e).uuid,mapping:this.mapping,channel:this.channel,repeat:[this.repeat.x,this.repeat.y],offset:[this.offset.x,this.offset.y],center:[this.center.x,this.center.y],rotation:this.rotation,wrap:[this.wrapS,this.wrapT],format:this.format,internalFormat:this.internalFormat,type:this.type,normalized:this.normalized,colorSpace:this.colorSpace,minFilter:this.minFilter,magFilter:this.magFilter,anisotropy:this.anisotropy,flipY:this.flipY,generateMipmaps:this.generateMipmaps,premultiplyAlpha:this.premultiplyAlpha,unpackAlignment:this.unpackAlignment};return Object.keys(this.userData).length>0&&(n.userData=this.userData),t||(e.textures[this.uuid]=n),n}dispose(){this.dispatchEvent({type:`dispose`})}transformUv(e){if(this.mapping!==300)return e;if(e.applyMatrix3(this.matrix),e.x<0||e.x>1)switch(this.wrapS){case vc:e.x-=Math.floor(e.x);break;case yc:e.x=e.x<0?0:1;break;case bc:Math.abs(Math.floor(e.x)%2)===1?e.x=Math.ceil(e.x)-e.x:e.x-=Math.floor(e.x);break}if(e.y<0||e.y>1)switch(this.wrapT){case vc:e.y-=Math.floor(e.y);break;case yc:e.y=e.y<0?0:1;break;case bc:Math.abs(Math.floor(e.y)%2)===1?e.y=Math.ceil(e.y)-e.y:e.y-=Math.floor(e.y);break}return this.flipY&&(e.y=1-e.y),e}set needsUpdate(e){e===!0&&(this.version++,this.source.needsUpdate=!0)}set needsPMREMUpdate(e){e===!0&&this.pmremVersion++}};$u.DEFAULT_IMAGE=null,$u.DEFAULT_MAPPING=300,$u.DEFAULT_ANISOTROPY=1;var ed=class e{static{e.prototype.isVector4=!0}constructor(e=0,t=0,n=0,r=1){this.x=e,this.y=t,this.z=n,this.w=r}get width(){return this.z}set width(e){this.z=e}get height(){return this.w}set height(e){this.w=e}set(e,t,n,r){return this.x=e,this.y=t,this.z=n,this.w=r,this}setScalar(e){return this.x=e,this.y=e,this.z=e,this.w=e,this}setX(e){return this.x=e,this}setY(e){return this.y=e,this}setZ(e){return this.z=e,this}setW(e){return this.w=e,this}setComponent(e,t){switch(e){case 0:this.x=t;break;case 1:this.y=t;break;case 2:this.z=t;break;case 3:this.w=t;break;default:throw Error(`THREE.Vector4: index is out of range: `+e)}return this}getComponent(e){switch(e){case 0:return this.x;case 1:return this.y;case 2:return this.z;case 3:return this.w;default:throw Error(`THREE.Vector4: index is out of range: `+e)}}clone(){return new this.constructor(this.x,this.y,this.z,this.w)}copy(e){return this.x=e.x,this.y=e.y,this.z=e.z,this.w=e.w===void 0?1:e.w,this}add(e){return this.x+=e.x,this.y+=e.y,this.z+=e.z,this.w+=e.w,this}addScalar(e){return this.x+=e,this.y+=e,this.z+=e,this.w+=e,this}addVectors(e,t){return this.x=e.x+t.x,this.y=e.y+t.y,this.z=e.z+t.z,this.w=e.w+t.w,this}addScaledVector(e,t){return this.x+=e.x*t,this.y+=e.y*t,this.z+=e.z*t,this.w+=e.w*t,this}sub(e){return this.x-=e.x,this.y-=e.y,this.z-=e.z,this.w-=e.w,this}subScalar(e){return this.x-=e,this.y-=e,this.z-=e,this.w-=e,this}subVectors(e,t){return this.x=e.x-t.x,this.y=e.y-t.y,this.z=e.z-t.z,this.w=e.w-t.w,this}multiply(e){return this.x*=e.x,this.y*=e.y,this.z*=e.z,this.w*=e.w,this}multiplyScalar(e){return this.x*=e,this.y*=e,this.z*=e,this.w*=e,this}applyMatrix4(e){let t=this.x,n=this.y,r=this.z,i=this.w,a=e.elements;return this.x=a[0]*t+a[4]*n+a[8]*r+a[12]*i,this.y=a[1]*t+a[5]*n+a[9]*r+a[13]*i,this.z=a[2]*t+a[6]*n+a[10]*r+a[14]*i,this.w=a[3]*t+a[7]*n+a[11]*r+a[15]*i,this}divide(e){return this.x/=e.x,this.y/=e.y,this.z/=e.z,this.w/=e.w,this}divideScalar(e){return this.multiplyScalar(1/e)}setAxisAngleFromQuaternion(e){this.w=2*Math.acos(e.w);let t=Math.sqrt(1-e.w*e.w);return t<1e-4?(this.x=1,this.y=0,this.z=0):(this.x=e.x/t,this.y=e.y/t,this.z=e.z/t),this}setAxisAngleFromRotationMatrix(e){let t,n,r,i,a=.01,o=.1,s=e.elements,c=s[0],l=s[4],u=s[8],d=s[1],f=s[5],p=s[9],m=s[2],h=s[6],g=s[10];if(Math.abs(l-d)s&&e>_?e_?s1);this.dispose()}this.viewport.set(0,0,e,t),this.scissor.set(0,0,e,t)}clone(){return new this.constructor().copy(this)}copy(e){this.width=e.width,this.height=e.height,this.depth=e.depth,this.scissor.copy(e.scissor),this.scissorTest=e.scissorTest,this.viewport.copy(e.viewport),this.textures.length=0;for(let t=0,n=e.textures.length;t>>0}enable(e){this.mask|=1<1){for(let e=0;e1){for(let e=0;e0&&(r.userData=this.userData),r.layers=this.layers.mask,r.matrix=this.matrix.toArray(),r.up=this.up.toArray(),this.pivot!==null&&(r.pivot=this.pivot.toArray()),this.morphTargetDictionary!==void 0&&(r.morphTargetDictionary=Object.assign({},this.morphTargetDictionary)),this.morphTargetInfluences!==void 0&&(r.morphTargetInfluences=this.morphTargetInfluences.slice()),this.isInstancedMesh&&(r.type=`InstancedMesh`,r.count=this.count,r.instanceMatrix=this.instanceMatrix.toJSON(),this.instanceColor!==null&&(r.instanceColor=this.instanceColor.toJSON())),this.isBatchedMesh&&(r.type=`BatchedMesh`,r.perObjectFrustumCulled=this.perObjectFrustumCulled,r.sortObjects=this.sortObjects,r.drawRanges=this._drawRanges,r.reservedRanges=this._reservedRanges,r.geometryInfo=this._geometryInfo.map(e=>({...e,boundingBox:e.boundingBox?e.boundingBox.toJSON():void 0,boundingSphere:e.boundingSphere?e.boundingSphere.toJSON():void 0})),r.instanceInfo=this._instanceInfo.map(e=>({...e})),r.availableInstanceIds=this._availableInstanceIds.slice(),r.availableGeometryIds=this._availableGeometryIds.slice(),r.nextIndexStart=this._nextIndexStart,r.nextVertexStart=this._nextVertexStart,r.geometryCount=this._geometryCount,r.maxInstanceCount=this._maxInstanceCount,r.maxVertexCount=this._maxVertexCount,r.maxIndexCount=this._maxIndexCount,r.geometryInitialized=this._geometryInitialized,r.matricesTexture=this._matricesTexture.toJSON(e),r.indirectTexture=this._indirectTexture.toJSON(e),this._colorsTexture!==null&&(r.colorsTexture=this._colorsTexture.toJSON(e)),this.boundingSphere!==null&&(r.boundingSphere=this.boundingSphere.toJSON()),this.boundingBox!==null&&(r.boundingBox=this.boundingBox.toJSON()));function i(t,n){return t[n.uuid]===void 0&&(t[n.uuid]=n.toJSON(e)),n.uuid}if(this.isScene)this.background&&(this.background.isColor?r.background=this.background.toJSON():this.background.isTexture&&(r.background=this.background.toJSON(e).uuid)),this.environment&&this.environment.isTexture&&this.environment.isRenderTargetTexture!==!0&&(r.environment=this.environment.toJSON(e).uuid);else if(this.isMesh||this.isLine||this.isPoints){r.geometry=i(e.geometries,this.geometry);let t=this.geometry.parameters;if(t!==void 0&&t.shapes!==void 0){let n=t.shapes;if(Array.isArray(n))for(let t=0,r=n.length;t0){r.children=[];for(let t=0;t0){r.animations=[];for(let t=0;t0&&(n.geometries=t),r.length>0&&(n.materials=r),i.length>0&&(n.textures=i),o.length>0&&(n.images=o),s.length>0&&(n.shapes=s),c.length>0&&(n.skeletons=c),l.length>0&&(n.animations=l),u.length>0&&(n.nodes=u)}return n.object=r,n;function a(e){let t=[];for(let n in e){let r=e[n];delete r.metadata,t.push(r)}return t}}clone(e){return new this.constructor().copy(this,e)}copy(e,t=!0){if(this.name=e.name,this.up.copy(e.up),this.position.copy(e.position),this.rotation.order=e.rotation.order,this.quaternion.copy(e.quaternion),this.scale.copy(e.scale),this.pivot=e.pivot===null?null:e.pivot.clone(),this.matrix.copy(e.matrix),this.matrixWorld.copy(e.matrixWorld),this.matrixAutoUpdate=e.matrixAutoUpdate,this.matrixWorldAutoUpdate=e.matrixWorldAutoUpdate,this.matrixWorldNeedsUpdate=e.matrixWorldNeedsUpdate,this.layers.mask=e.layers.mask,this.visible=e.visible,this.castShadow=e.castShadow,this.receiveShadow=e.receiveShadow,this.frustumCulled=e.frustumCulled,this.renderOrder=e.renderOrder,this.static=e.static,this.animations=e.animations.slice(),this.userData=JSON.parse(JSON.stringify(e.userData)),t===!0)for(let t=0;t.025?(c.inputState.pinching=!1,this.dispatchEvent({type:`pinchend`,handedness:e.handedness,target:this})):!c.inputState.pinching&&o<=.015&&(c.inputState.pinching=!0,this.dispatchEvent({type:`pinchstart`,handedness:e.handedness,target:this}))}else s!==null&&e.gripSpace&&(i=t.getPose(e.gripSpace,n),i!==null&&(s.matrix.fromArray(i.transform.matrix),s.matrix.decompose(s.position,s.rotation,s.scale),s.matrixWorldNeedsUpdate=!0,i.linearVelocity?(s.hasLinearVelocity=!0,s.linearVelocity.copy(i.linearVelocity)):s.hasLinearVelocity=!1,i.angularVelocity?(s.hasAngularVelocity=!0,s.angularVelocity.copy(i.angularVelocity)):s.hasAngularVelocity=!1,s.eventsEnabled&&s.dispatchEvent({type:`gripUpdated`,data:e,target:this})));o!==null&&(r=t.getPose(e.targetRaySpace,n),r===null&&i!==null&&(r=i),r!==null&&(o.matrix.fromArray(r.transform.matrix),o.matrix.decompose(o.position,o.rotation,o.scale),o.matrixWorldNeedsUpdate=!0,r.linearVelocity?(o.hasLinearVelocity=!0,o.linearVelocity.copy(r.linearVelocity)):o.hasLinearVelocity=!1,r.angularVelocity?(o.hasAngularVelocity=!0,o.angularVelocity.copy(r.angularVelocity)):o.hasAngularVelocity=!1,this.dispatchEvent(Pd)))}return o!==null&&(o.visible=r!==null),s!==null&&(s.visible=i!==null),c!==null&&(c.visible=a!==null),this}_getHandJoint(e,t){if(e.joints[t.jointName]===void 0){let n=new Nd;n.matrixAutoUpdate=!1,n.visible=!1,e.joints[t.jointName]=n,e.add(n)}return e.joints[t.jointName]}},Id={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074},Ld={h:0,s:0,l:0},Rd={h:0,s:0,l:0};function zd(e,t,n){return n<0&&(n+=1),n>1&&--n,n<1/6?e+(t-e)*6*n:n<1/2?t:n<2/3?e+(t-e)*6*(2/3-n):e}var Q=class{constructor(e,t,n){return this.isColor=!0,this.r=1,this.g=1,this.b=1,this.set(e,t,n)}set(e,t,n){if(t===void 0&&n===void 0){let t=e;t&&t.isColor?this.copy(t):typeof t==`number`?this.setHex(t):typeof t==`string`&&this.setStyle(t)}else this.setRGB(e,t,n);return this}setScalar(e){return this.r=e,this.g=e,this.b=e,this}setHex(e,t=Vl){return e=Math.floor(e),this.r=(e>>16&255)/255,this.g=(e>>8&255)/255,this.b=(e&255)/255,Uu.colorSpaceToWorking(this,t),this}setRGB(e,t,n,r=Uu.workingColorSpace){return this.r=e,this.g=t,this.b=n,Uu.colorSpaceToWorking(this,r),this}setHSL(e,t,n,r=Uu.workingColorSpace){if(e=mu(e,1),t=pu(t,0,1),n=pu(n,0,1),t===0)this.r=this.g=this.b=n;else{let r=n<=.5?n*(1+t):n+t-n*t,i=2*n-r;this.r=zd(i,r,e+1/3),this.g=zd(i,r,e),this.b=zd(i,r,e-1/3)}return Uu.colorSpaceToWorking(this,r),this}setStyle(e,t=Vl){function n(t){t!==void 0&&parseFloat(t)<1&&nu(`Color: Alpha component of `+e+` will be ignored.`)}let r;if(r=/^(\w+)\(([^\)]*)\)/.exec(e)){let i,a=r[1],o=r[2];switch(a){case`rgb`:case`rgba`:if(i=/^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(i[4]),this.setRGB(Math.min(255,parseInt(i[1],10))/255,Math.min(255,parseInt(i[2],10))/255,Math.min(255,parseInt(i[3],10))/255,t);if(i=/^\s*(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(i[4]),this.setRGB(Math.min(100,parseInt(i[1],10))/100,Math.min(100,parseInt(i[2],10))/100,Math.min(100,parseInt(i[3],10))/100,t);break;case`hsl`:case`hsla`:if(i=/^\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\%\s*,\s*(\d*\.?\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(i[4]),this.setHSL(parseFloat(i[1])/360,parseFloat(i[2])/100,parseFloat(i[3])/100,t);break;default:nu(`Color: Unknown color model `+e)}}else if(r=/^\#([A-Fa-f\d]+)$/.exec(e)){let n=r[1],i=n.length;if(i===3)return this.setRGB(parseInt(n.charAt(0),16)/15,parseInt(n.charAt(1),16)/15,parseInt(n.charAt(2),16)/15,t);if(i===6)return this.setHex(parseInt(n,16),t);nu(`Color: Invalid hex color `+e)}else if(e&&e.length>0)return this.setColorName(e,t);return this}setColorName(e,t=Vl){let n=Id[e.toLowerCase()];return n===void 0?nu(`Color: Unknown color `+e):this.setHex(n,t),this}clone(){return new this.constructor(this.r,this.g,this.b)}copy(e){return this.r=e.r,this.g=e.g,this.b=e.b,this}copySRGBToLinear(e){return this.r=Wu(e.r),this.g=Wu(e.g),this.b=Wu(e.b),this}copyLinearToSRGB(e){return this.r=Gu(e.r),this.g=Gu(e.g),this.b=Gu(e.b),this}convertSRGBToLinear(){return this.copySRGBToLinear(this),this}convertLinearToSRGB(){return this.copyLinearToSRGB(this),this}getHex(e=Vl){return Uu.workingToColorSpace(Bd.copy(this),e),Math.round(pu(Bd.r*255,0,255))*65536+Math.round(pu(Bd.g*255,0,255))*256+Math.round(pu(Bd.b*255,0,255))}getHexString(e=Vl){return(`000000`+this.getHex(e).toString(16)).slice(-6)}getHSL(e,t=Uu.workingColorSpace){Uu.workingToColorSpace(Bd.copy(this),t);let n=Bd.r,r=Bd.g,i=Bd.b,a=Math.max(n,r,i),o=Math.min(n,r,i),s,c,l=(o+a)/2;if(o===a)s=0,c=0;else{let e=a-o;switch(c=l<=.5?e/(a+o):e/(2-a-o),a){case n:s=(r-i)/e+(r0?r.multiplyScalar(1/Math.sqrt(i)):r.set(0,0,0)}static getBarycoord(e,t,n,r,i){Ud.subVectors(r,t),Wd.subVectors(n,t),Gd.subVectors(e,t);let a=Ud.dot(Ud),o=Ud.dot(Wd),s=Ud.dot(Gd),c=Wd.dot(Wd),l=Wd.dot(Gd),u=a*c-o*o;if(u===0)return i.set(0,0,0),null;let d=1/u,f=(c*s-o*l)*d,p=(a*l-o*s)*d;return i.set(1-f-p,p,f)}static containsPoint(e,t,n,r){return this.getBarycoord(e,t,n,r,Kd)!==null&&Kd.x>=0&&Kd.y>=0&&Kd.x+Kd.y<=1}static getInterpolation(e,t,n,r,i,a,o,s){return this.getBarycoord(e,t,n,r,Kd)===null?(s.x=0,s.y=0,`z`in s&&(s.z=0),`w`in s&&(s.w=0),null):(s.setScalar(0),s.addScaledVector(i,Kd.x),s.addScaledVector(a,Kd.y),s.addScaledVector(o,Kd.z),s)}static getInterpolatedAttribute(e,t,n,r,i,a){return $d.setScalar(0),ef.setScalar(0),tf.setScalar(0),$d.fromBufferAttribute(e,t),ef.fromBufferAttribute(e,n),tf.fromBufferAttribute(e,r),a.setScalar(0),a.addScaledVector($d,i.x),a.addScaledVector(ef,i.y),a.addScaledVector(tf,i.z),a}static isFrontFacing(e,t,n,r){return Ud.subVectors(n,t),Wd.subVectors(e,t),Ud.cross(Wd).dot(r)<0}set(e,t,n){return this.a.copy(e),this.b.copy(t),this.c.copy(n),this}setFromPointsAndIndices(e,t,n,r){return this.a.copy(e[t]),this.b.copy(e[n]),this.c.copy(e[r]),this}setFromAttributeAndIndices(e,t,n,r){return this.a.fromBufferAttribute(e,t),this.b.fromBufferAttribute(e,n),this.c.fromBufferAttribute(e,r),this}clone(){return new this.constructor().copy(this)}copy(e){return this.a.copy(e.a),this.b.copy(e.b),this.c.copy(e.c),this}getArea(){return Ud.subVectors(this.c,this.b),Wd.subVectors(this.a,this.b),Ud.cross(Wd).length()*.5}getMidpoint(e){return e.addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)}getNormal(t){return e.getNormal(this.a,this.b,this.c,t)}getPlane(e){return e.setFromCoplanarPoints(this.a,this.b,this.c)}getBarycoord(t,n){return e.getBarycoord(t,this.a,this.b,this.c,n)}getInterpolation(t,n,r,i,a){return e.getInterpolation(t,this.a,this.b,this.c,n,r,i,a)}containsPoint(t){return e.containsPoint(t,this.a,this.b,this.c)}isFrontFacing(t){return e.isFrontFacing(this.a,this.b,this.c,t)}intersectsBox(e){return e.intersectsTriangle(this)}closestPointToPoint(e,t){let n=this.a,r=this.b,i=this.c,a,o;qd.subVectors(r,n),Jd.subVectors(i,n),Xd.subVectors(e,n);let s=qd.dot(Xd),c=Jd.dot(Xd);if(s<=0&&c<=0)return t.copy(n);Zd.subVectors(e,r);let l=qd.dot(Zd),u=Jd.dot(Zd);if(l>=0&&u<=l)return t.copy(r);let d=s*u-l*c;if(d<=0&&s>=0&&l<=0)return a=s/(s-l),t.copy(n).addScaledVector(qd,a);Qd.subVectors(e,i);let f=qd.dot(Qd),p=Jd.dot(Qd);if(p>=0&&f<=p)return t.copy(i);let m=f*c-s*p;if(m<=0&&c>=0&&p<=0)return o=c/(c-p),t.copy(n).addScaledVector(Jd,o);let h=l*p-f*u;if(h<=0&&u-l>=0&&f-p>=0)return Yd.subVectors(i,r),o=(u-l)/(u-l+(f-p)),t.copy(r).addScaledVector(Yd,o);let g=1/(h+m+d);return a=m*g,o=d*g,t.copy(n).addScaledVector(qd,a).addScaledVector(Jd,o)}equals(e){return e.a.equals(this.a)&&e.b.equals(this.b)&&e.c.equals(this.c)}},rf=class{constructor(e=new Z(1/0,1/0,1/0),t=new Z(-1/0,-1/0,-1/0)){this.isBox3=!0,this.min=e,this.max=t}set(e,t){return this.min.copy(e),this.max.copy(t),this}setFromArray(e){this.makeEmpty();for(let t=0,n=e.length;t=this.min.x&&e.x<=this.max.x&&e.y>=this.min.y&&e.y<=this.max.y&&e.z>=this.min.z&&e.z<=this.max.z}containsBox(e){return this.min.x<=e.min.x&&e.max.x<=this.max.x&&this.min.y<=e.min.y&&e.max.y<=this.max.y&&this.min.z<=e.min.z&&e.max.z<=this.max.z}getParameter(e,t){return t.set((e.x-this.min.x)/(this.max.x-this.min.x),(e.y-this.min.y)/(this.max.y-this.min.y),(e.z-this.min.z)/(this.max.z-this.min.z))}intersectsBox(e){return e.max.x>=this.min.x&&e.min.x<=this.max.x&&e.max.y>=this.min.y&&e.min.y<=this.max.y&&e.max.z>=this.min.z&&e.min.z<=this.max.z}intersectsSphere(e){return this.clampPoint(e.center,of),of.distanceToSquared(e.center)<=e.radius*e.radius}intersectsPlane(e){let t,n;return e.normal.x>0?(t=e.normal.x*this.min.x,n=e.normal.x*this.max.x):(t=e.normal.x*this.max.x,n=e.normal.x*this.min.x),e.normal.y>0?(t+=e.normal.y*this.min.y,n+=e.normal.y*this.max.y):(t+=e.normal.y*this.max.y,n+=e.normal.y*this.min.y),e.normal.z>0?(t+=e.normal.z*this.min.z,n+=e.normal.z*this.max.z):(t+=e.normal.z*this.max.z,n+=e.normal.z*this.min.z),t<=-e.constant&&n>=-e.constant}intersectsTriangle(e){if(this.isEmpty())return!1;this.getCenter(mf),hf.subVectors(this.max,mf),cf.subVectors(e.a,mf),lf.subVectors(e.b,mf),uf.subVectors(e.c,mf),df.subVectors(lf,cf),ff.subVectors(uf,lf),pf.subVectors(cf,uf);let t=[0,-df.z,df.y,0,-ff.z,ff.y,0,-pf.z,pf.y,df.z,0,-df.x,ff.z,0,-ff.x,pf.z,0,-pf.x,-df.y,df.x,0,-ff.y,ff.x,0,-pf.y,pf.x,0];return!vf(t,cf,lf,uf,hf)||(t=[1,0,0,0,1,0,0,0,1],!vf(t,cf,lf,uf,hf))?!1:(gf.crossVectors(df,ff),t=[gf.x,gf.y,gf.z],vf(t,cf,lf,uf,hf))}clampPoint(e,t){return t.copy(e).clamp(this.min,this.max)}distanceToPoint(e){return this.clampPoint(e,of).distanceTo(e)}getBoundingSphere(e){return this.isEmpty()?e.makeEmpty():(this.getCenter(e.center),e.radius=this.getSize(of).length()*.5),e}intersect(e){return this.min.max(e.min),this.max.min(e.max),this.isEmpty()&&this.makeEmpty(),this}union(e){return this.min.min(e.min),this.max.max(e.max),this}applyMatrix4(e){return this.isEmpty()?this:(af[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(e),af[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(e),af[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(e),af[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(e),af[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(e),af[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(e),af[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(e),af[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(e),this.setFromPoints(af),this)}translate(e){return this.min.add(e),this.max.add(e),this}equals(e){return e.min.equals(this.min)&&e.max.equals(this.max)}toJSON(){return{min:this.min.toArray(),max:this.max.toArray()}}fromJSON(e){return this.min.fromArray(e.min),this.max.fromArray(e.max),this}},af=[new Z,new Z,new Z,new Z,new Z,new Z,new Z,new Z],of=new Z,sf=new rf,cf=new Z,lf=new Z,uf=new Z,df=new Z,ff=new Z,pf=new Z,mf=new Z,hf=new Z,gf=new Z,_f=new Z;function vf(e,t,n,r,i){for(let a=0,o=e.length-3;a<=o;a+=3){_f.fromArray(e,a);let o=i.x*Math.abs(_f.x)+i.y*Math.abs(_f.y)+i.z*Math.abs(_f.z),s=t.dot(_f),c=n.dot(_f),l=r.dot(_f);if(Math.max(-Math.max(s,c,l),Math.min(s,c,l))>o)return!1}return!0}var yf=new Z,bf=new X,xf=0,Sf=class extends su{constructor(e,t,n=!1){if(super(),Array.isArray(e))throw TypeError(`THREE.BufferAttribute: array should be a Typed Array.`);this.isBufferAttribute=!0,Object.defineProperty(this,"id",{value:xf++}),this.name=``,this.array=e,this.itemSize=t,this.count=e===void 0?0:e.length/t,this.normalized=n,this.usage=Kl,this.updateRanges=[],this.gpuType=Nc,this.version=0}onUploadCallback(){}set needsUpdate(e){e===!0&&this.version++}setUsage(e){return this.usage=e,this}addUpdateRange(e,t){this.updateRanges.push({start:e,count:t})}clearUpdateRanges(){this.updateRanges.length=0}copy(e){return this.name=e.name,this.array=new e.array.constructor(e.array),this.itemSize=e.itemSize,this.count=e.count,this.normalized=e.normalized,this.usage=e.usage,this.gpuType=e.gpuType,this}copyAt(e,t,n){e*=this.itemSize,n*=t.itemSize;for(let r=0,i=this.itemSize;rthis.radius*this.radius&&(t.sub(this.center).normalize(),t.multiplyScalar(this.radius).add(this.center)),t}getBoundingBox(e){return this.isEmpty()?(e.makeEmpty(),e):(e.set(this.center,this.center),e.expandByScalar(this.radius),e)}applyMatrix4(e){return this.center.applyMatrix4(e),this.radius*=e.getMaxScaleOnAxis(),this}translate(e){return this.center.add(e),this}expandByPoint(e){if(this.isEmpty())return this.center.copy(e),this.radius=0,this;Df.subVectors(e,this.center);let t=Df.lengthSq();if(t>this.radius*this.radius){let e=Math.sqrt(t),n=(e-this.radius)*.5;this.center.addScaledVector(Df,n/e),this.radius+=n}return this}union(e){return e.isEmpty()?this:this.isEmpty()?(this.copy(e),this):(this.center.equals(e.center)===!0?this.radius=Math.max(this.radius,e.radius):(Of.subVectors(e.center,this.center).setLength(e.radius),this.expandByPoint(Df.copy(e.center).add(Of)),this.expandByPoint(Df.copy(e.center).sub(Of))),this)}equals(e){return e.center.equals(this.center)&&e.radius===this.radius}clone(){return new this.constructor().copy(this)}toJSON(){return{radius:this.radius,center:this.center.toArray()}}fromJSON(e){return this.radius=e.radius,this.center.fromArray(e.center),this}},Af=0,jf=new ad,Mf=new Md,Nf=new Z,Pf=new rf,Ff=new rf,If=new Z,Lf=class e extends su{constructor(){super(),this.isBufferGeometry=!0,Object.defineProperty(this,"id",{value:Af++}),this.uuid=fu(),this.name=``,this.type=`BufferGeometry`,this.index=null,this.indirect=null,this.indirectOffset=0,this.attributes={},this.morphAttributes={},this.morphTargetsRelative=!1,this.groups=[],this.boundingBox=null,this.boundingSphere=null,this.drawRange={start:0,count:1/0},this.userData={},this._transformed=!1}getIndex(){return this.index}setIndex(e){return Array.isArray(e)?this.index=new(Yl(e)?wf:Cf)(e,1):this.index=e,this}setIndirect(e,t=0){return this.indirect=e,this.indirectOffset=t,this}getIndirect(){return this.indirect}getAttribute(e){return this.attributes[e]}setAttribute(e,t){return this.attributes[e]=t,this}deleteAttribute(e){return delete this.attributes[e],this}hasAttribute(e){return this.attributes[e]!==void 0}addGroup(e,t,n=0){this.groups.push({start:e,count:t,materialIndex:n})}clearGroups(){this.groups=[]}setDrawRange(e,t){this.drawRange.start=e,this.drawRange.count=t}applyMatrix4(e){let t=this.attributes.position;t!==void 0&&(t.applyMatrix4(e),t.needsUpdate=!0);let n=this.attributes.normal;if(n!==void 0){let t=new Ru().getNormalMatrix(e);n.applyNormalMatrix(t),n.needsUpdate=!0}let r=this.attributes.tangent;return r!==void 0&&(r.transformDirection(e),r.needsUpdate=!0),this.boundingBox!==null&&this.computeBoundingBox(),this.boundingSphere!==null&&this.computeBoundingSphere(),this._transformed=!0,this}applyQuaternion(e){return jf.makeRotationFromQuaternion(e),this.applyMatrix4(jf),this}rotateX(e){return jf.makeRotationX(e),this.applyMatrix4(jf),this}rotateY(e){return jf.makeRotationY(e),this.applyMatrix4(jf),this}rotateZ(e){return jf.makeRotationZ(e),this.applyMatrix4(jf),this}translate(e,t,n){return jf.makeTranslation(e,t,n),this.applyMatrix4(jf),this}scale(e,t,n){return jf.makeScale(e,t,n),this.applyMatrix4(jf),this}lookAt(e){return Mf.lookAt(e),Mf.updateMatrix(),this.applyMatrix4(Mf.matrix),this}center(){return this.computeBoundingBox(),this.boundingBox.getCenter(Nf).negate(),this.translate(Nf.x,Nf.y,Nf.z),this}setFromPoints(e){let t=this.getAttribute(`position`);if(t===void 0){let t=[];for(let n=0,r=e.length;nt.count&&nu(`BufferGeometry: Buffer size too small for points data. Use .dispose() and create a new geometry.`),t.needsUpdate=!0}return this}computeBoundingBox(){this.boundingBox===null&&(this.boundingBox=new rf);let e=this.attributes.position,t=this.morphAttributes.position;if(e&&e.isGLBufferAttribute){ru(`BufferGeometry.computeBoundingBox(): GLBufferAttribute requires a manual bounding box.`,this),this.boundingBox.set(new Z(-1/0,-1/0,-1/0),new Z(1/0,1/0,1/0));return}if(e!==void 0){if(this.boundingBox.setFromBufferAttribute(e),t)for(let e=0,n=t.length;e0&&(e.userData=this.userData),this.parameters!==void 0&&this._transformed!==!0){let t=this.parameters;for(let n in t)t[n]!==void 0&&(e[n]=t[n]);return e}e.data={attributes:{}};let t=this.index;t!==null&&(e.data.index={type:t.array.constructor.name,array:Array.prototype.slice.call(t.array)});let n=this.attributes;for(let t in n){let r=n[t];e.data.attributes[t]=r.toJSON(e.data)}let r={},i=!1;for(let t in this.morphAttributes){let n=this.morphAttributes[t],a=[];for(let t=0,r=n.length;t0&&(r[t]=a,i=!0)}i&&(e.data.morphAttributes=r,e.data.morphTargetsRelative=this.morphTargetsRelative);let a=this.groups;a.length>0&&(e.data.groups=JSON.parse(JSON.stringify(a)));let o=this.boundingSphere;return o!==null&&(e.data.boundingSphere=o.toJSON()),e}clone(){return new this.constructor().copy(this)}copy(e){this.index=null,this.attributes={},this.morphAttributes={},this.groups=[],this.boundingBox=null,this.boundingSphere=null;let t={};this.name=e.name;let n=e.index;n!==null&&this.setIndex(n.clone());let r=e.attributes;for(let e in r){let n=r[e];this.setAttribute(e,n.clone(t))}let i=e.morphAttributes;for(let e in i){let n=[],r=i[e];for(let e=0,i=r.length;e1)?null:t.copy(e.start).addScaledVector(r,a)}intersectsLine(e){let t=this.distanceToPoint(e.start),n=this.distanceToPoint(e.end);return t<0&&n>0||n<0&&t>0}intersectsBox(e){return e.intersectsPlane(this)}intersectsSphere(e){return e.intersectsPlane(this)}coplanarPoint(e){return e.copy(this.normal).multiplyScalar(-this.constant)}applyMatrix4(e,t){let n=t||Uf.getNormalMatrix(e),r=this.coplanarPoint(Vf).applyMatrix4(e),i=this.normal.applyMatrix3(n).normalize();return this.constant=-r.dot(i),this}translate(e){return this.constant-=e.dot(this.normal),this}equals(e){return e.normal.equals(this.normal)&&e.constant===this.constant}clone(){return new this.constructor().copy(this)}toJSON(){return{normal:this.normal.toArray(),constant:this.constant}}fromJSON(e){return this.normal.fromArray(e.normal),this.constant=e.constant,this}},Gf=0,Kf=class extends su{constructor(){super(),this.isMaterial=!0,Object.defineProperty(this,"id",{value:Gf++}),this.uuid=fu(),this.name=``,this.type=`Material`,this.blending=1,this.side=0,this.vertexColors=!1,this.opacity=1,this.transparent=!1,this.alphaHash=!1,this.blendSrc=204,this.blendDst=205,this.blendEquation=100,this.blendSrcAlpha=null,this.blendDstAlpha=null,this.blendEquationAlpha=null,this.blendColor=new Q(0,0,0),this.blendAlpha=0,this.depthFunc=3,this.depthTest=!0,this.depthWrite=!0,this.stencilWriteMask=255,this.stencilFunc=519,this.stencilRef=0,this.stencilFuncMask=255,this.stencilFail=Gl,this.stencilZFail=Gl,this.stencilZPass=Gl,this.stencilWrite=!1,this.clippingPlanes=null,this.clipIntersection=!1,this.clipShadows=!1,this.shadowSide=null,this.colorWrite=!0,this.precision=null,this.polygonOffset=!1,this.polygonOffsetFactor=0,this.polygonOffsetUnits=0,this.dithering=!1,this.alphaToCoverage=!1,this.premultipliedAlpha=!1,this.forceSinglePass=!1,this.allowOverride=!0,this.visible=!0,this.toneMapped=!0,this.userData={},this.version=0,this._alphaTest=0}get alphaTest(){return this._alphaTest}set alphaTest(e){this._alphaTest>0!=e>0&&this.version++,this._alphaTest=e}onBeforeRender(){}onBeforeCompile(){}customProgramCacheKey(){return this.onBeforeCompile.toString()}setValues(e){if(e!==void 0)for(let t in e){let n=e[t];if(n===void 0){nu(`Material: parameter '${t}' has value of undefined.`);continue}let r=this[t];if(r===void 0){nu(`Material: '${t}' is not a property of THREE.${this.type}.`);continue}r&&r.isColor?r.set(n):r&&r.isVector2&&n&&n.isVector2||r&&r.isEuler&&n&&n.isEuler||r&&r.isVector3&&n&&n.isVector3?r.copy(n):this[t]=n}}toJSON(e){let t=e===void 0||typeof e==`string`;t&&(e={textures:{},images:{}});let n={metadata:{version:4.7,type:`Material`,generator:`Material.toJSON`}};n.uuid=this.uuid,n.type=this.type,n.blending=this.blending,n.side=this.side,n.shadowSide=this.shadowSide,n.vertexColors=this.vertexColors,n.opacity=this.opacity,n.transparent=this.transparent,n.blendSrc=this.blendSrc,n.blendDst=this.blendDst,n.blendEquation=this.blendEquation,n.blendSrcAlpha=this.blendSrcAlpha,n.blendDstAlpha=this.blendDstAlpha,n.blendEquationAlpha=this.blendEquationAlpha,n.blendColor=this.blendColor.getHex(),n.blendAlpha=this.blendAlpha,n.depthFunc=this.depthFunc,n.depthTest=this.depthTest,n.depthWrite=this.depthWrite,n.colorWrite=this.colorWrite,n.clipIntersection=this.clipIntersection,n.clipShadows=this.clipShadows,n.stencilWriteMask=this.stencilWriteMask,n.stencilFunc=this.stencilFunc,n.stencilRef=this.stencilRef,n.stencilFuncMask=this.stencilFuncMask,n.stencilFail=this.stencilFail,n.stencilZFail=this.stencilZFail,n.stencilZPass=this.stencilZPass,n.stencilWrite=this.stencilWrite,n.polygonOffset=this.polygonOffset,n.polygonOffsetFactor=this.polygonOffsetFactor,n.polygonOffsetUnits=this.polygonOffsetUnits,n.dithering=this.dithering,n.alphaTest=this.alphaTest,n.alphaHash=this.alphaHash,n.alphaToCoverage=this.alphaToCoverage,n.premultipliedAlpha=this.premultipliedAlpha,n.forceSinglePass=this.forceSinglePass,n.allowOverride=this.allowOverride,n.visible=this.visible,n.toneMapped=this.toneMapped,n.name=this.name,this.color&&this.color.isColor&&(n.color=this.color.getHex()),this.roughness!==void 0&&(n.roughness=this.roughness),this.metalness!==void 0&&(n.metalness=this.metalness),this.sheen!==void 0&&(n.sheen=this.sheen),this.sheenColor&&this.sheenColor.isColor&&(n.sheenColor=this.sheenColor.getHex()),this.sheenRoughness!==void 0&&(n.sheenRoughness=this.sheenRoughness),this.emissive&&this.emissive.isColor&&(n.emissive=this.emissive.getHex()),this.emissiveIntensity!==void 0&&(n.emissiveIntensity=this.emissiveIntensity),this.specular&&this.specular.isColor&&(n.specular=this.specular.getHex()),this.specularIntensity!==void 0&&(n.specularIntensity=this.specularIntensity),this.specularColor&&this.specularColor.isColor&&(n.specularColor=this.specularColor.getHex()),this.shininess!==void 0&&(n.shininess=this.shininess),this.clearcoat!==void 0&&(n.clearcoat=this.clearcoat),this.clearcoatRoughness!==void 0&&(n.clearcoatRoughness=this.clearcoatRoughness),this.clearcoatMap&&this.clearcoatMap.isTexture&&(n.clearcoatMap=this.clearcoatMap.toJSON(e).uuid),this.clearcoatRoughnessMap&&this.clearcoatRoughnessMap.isTexture&&(n.clearcoatRoughnessMap=this.clearcoatRoughnessMap.toJSON(e).uuid),this.clearcoatNormalMap&&this.clearcoatNormalMap.isTexture&&(n.clearcoatNormalMap=this.clearcoatNormalMap.toJSON(e).uuid,n.clearcoatNormalScale=this.clearcoatNormalScale.toArray()),this.sheenColorMap&&this.sheenColorMap.isTexture&&(n.sheenColorMap=this.sheenColorMap.toJSON(e).uuid),this.sheenRoughnessMap&&this.sheenRoughnessMap.isTexture&&(n.sheenRoughnessMap=this.sheenRoughnessMap.toJSON(e).uuid),this.dispersion!==void 0&&(n.dispersion=this.dispersion),this.retroreflectivity!==void 0&&(n.retroreflectivity=this.retroreflectivity),this.iridescence!==void 0&&(n.iridescence=this.iridescence),this.iridescenceIOR!==void 0&&(n.iridescenceIOR=this.iridescenceIOR),this.iridescenceThicknessRange!==void 0&&(n.iridescenceThicknessRange=this.iridescenceThicknessRange),this.iridescenceMap&&this.iridescenceMap.isTexture&&(n.iridescenceMap=this.iridescenceMap.toJSON(e).uuid),this.iridescenceThicknessMap&&this.iridescenceThicknessMap.isTexture&&(n.iridescenceThicknessMap=this.iridescenceThicknessMap.toJSON(e).uuid),this.anisotropy!==void 0&&(n.anisotropy=this.anisotropy),this.anisotropyRotation!==void 0&&(n.anisotropyRotation=this.anisotropyRotation),this.anisotropyMap&&this.anisotropyMap.isTexture&&(n.anisotropyMap=this.anisotropyMap.toJSON(e).uuid),this.map&&this.map.isTexture&&(n.map=this.map.toJSON(e).uuid),this.matcap&&this.matcap.isTexture&&(n.matcap=this.matcap.toJSON(e).uuid),this.alphaMap&&this.alphaMap.isTexture&&(n.alphaMap=this.alphaMap.toJSON(e).uuid),this.lightMap&&this.lightMap.isTexture&&(n.lightMap=this.lightMap.toJSON(e).uuid,n.lightMapIntensity=this.lightMapIntensity),this.aoMap&&this.aoMap.isTexture&&(n.aoMap=this.aoMap.toJSON(e).uuid,n.aoMapIntensity=this.aoMapIntensity),this.bumpMap&&this.bumpMap.isTexture&&(n.bumpMap=this.bumpMap.toJSON(e).uuid,n.bumpScale=this.bumpScale),this.normalMap&&this.normalMap.isTexture&&(n.normalMap=this.normalMap.toJSON(e).uuid,n.normalMapType=this.normalMapType,n.normalScale=this.normalScale.toArray()),this.displacementMap&&this.displacementMap.isTexture&&(n.displacementMap=this.displacementMap.toJSON(e).uuid,n.displacementScale=this.displacementScale,n.displacementBias=this.displacementBias),this.roughnessMap&&this.roughnessMap.isTexture&&(n.roughnessMap=this.roughnessMap.toJSON(e).uuid),this.metalnessMap&&this.metalnessMap.isTexture&&(n.metalnessMap=this.metalnessMap.toJSON(e).uuid),this.emissiveMap&&this.emissiveMap.isTexture&&(n.emissiveMap=this.emissiveMap.toJSON(e).uuid),this.specularMap&&this.specularMap.isTexture&&(n.specularMap=this.specularMap.toJSON(e).uuid),this.specularIntensityMap&&this.specularIntensityMap.isTexture&&(n.specularIntensityMap=this.specularIntensityMap.toJSON(e).uuid),this.specularColorMap&&this.specularColorMap.isTexture&&(n.specularColorMap=this.specularColorMap.toJSON(e).uuid),this.envMap&&this.envMap.isTexture&&(n.envMap=this.envMap.toJSON(e).uuid,this.combine!==void 0&&(n.combine=this.combine)),this.envMapRotation!==void 0&&(n.envMapRotation=this.envMapRotation.toArray()),this.envMapIntensity!==void 0&&(n.envMapIntensity=this.envMapIntensity),this.reflectivity!==void 0&&(n.reflectivity=this.reflectivity),this.refractionRatio!==void 0&&(n.refractionRatio=this.refractionRatio),this.gradientMap&&this.gradientMap.isTexture&&(n.gradientMap=this.gradientMap.toJSON(e).uuid),this.transmission!==void 0&&(n.transmission=this.transmission),this.transmissionMap&&this.transmissionMap.isTexture&&(n.transmissionMap=this.transmissionMap.toJSON(e).uuid),this.thickness!==void 0&&(n.thickness=this.thickness),this.thicknessMap&&this.thicknessMap.isTexture&&(n.thicknessMap=this.thicknessMap.toJSON(e).uuid),this.attenuationDistance!==void 0&&(n.attenuationDistance=this.attenuationDistance),this.attenuationColor!==void 0&&(n.attenuationColor=this.attenuationColor.getHex()),this.size!==void 0&&(n.size=this.size),this.sizeAttenuation!==void 0&&(n.sizeAttenuation=this.sizeAttenuation),Array.isArray(this.clippingPlanes)&&this.clippingPlanes.length>0&&(n.clippingPlanes=this.clippingPlanes.map(e=>e.toJSON())),this.rotation!==void 0&&(n.rotation=this.rotation),this.depthPacking!==void 0&&(n.depthPacking=this.depthPacking),this.linewidth!==void 0&&(n.linewidth=this.linewidth),this.linecap!==void 0&&(n.linecap=this.linecap),this.linejoin!==void 0&&(n.linejoin=this.linejoin),this.dashSize!==void 0&&(n.dashSize=this.dashSize),this.gapSize!==void 0&&(n.gapSize=this.gapSize),this.scale!==void 0&&(n.scale=this.scale),this.wireframe!==void 0&&(n.wireframe=this.wireframe),this.wireframeLinewidth!==void 0&&(n.wireframeLinewidth=this.wireframeLinewidth),this.wireframeLinecap!==void 0&&(n.wireframeLinecap=this.wireframeLinecap),this.wireframeLinejoin!==void 0&&(n.wireframeLinejoin=this.wireframeLinejoin),this.flatShading!==void 0&&(n.flatShading=this.flatShading),this.fog!==void 0&&(n.fog=this.fog),Object.keys(this.userData).length>0&&(n.userData=this.userData);function r(e){let t=[];for(let n in e){let r=e[n];delete r.metadata,t.push(r)}return t}if(t){let t=r(e.textures),i=r(e.images);t.length>0&&(n.textures=t),i.length>0&&(n.images=i)}return n}fromJSON(e,t){if(e.uuid!==void 0&&(this.uuid=e.uuid),e.name!==void 0&&(this.name=e.name),e.color!==void 0&&this.color!==void 0&&this.color.setHex(e.color),e.roughness!==void 0&&(this.roughness=e.roughness),e.metalness!==void 0&&(this.metalness=e.metalness),e.sheen!==void 0&&(this.sheen=e.sheen),e.sheenColor!==void 0&&(this.sheenColor=new Q().setHex(e.sheenColor)),e.sheenRoughness!==void 0&&(this.sheenRoughness=e.sheenRoughness),e.emissive!==void 0&&this.emissive!==void 0&&this.emissive.setHex(e.emissive),e.specular!==void 0&&this.specular!==void 0&&this.specular.setHex(e.specular),e.specularIntensity!==void 0&&(this.specularIntensity=e.specularIntensity),e.specularColor!==void 0&&this.specularColor!==void 0&&this.specularColor.setHex(e.specularColor),e.shininess!==void 0&&(this.shininess=e.shininess),e.clearcoat!==void 0&&(this.clearcoat=e.clearcoat),e.clearcoatRoughness!==void 0&&(this.clearcoatRoughness=e.clearcoatRoughness),e.dispersion!==void 0&&(this.dispersion=e.dispersion),e.retroreflectivity!==void 0&&(this.retroreflectivity=e.retroreflectivity),e.iridescence!==void 0&&(this.iridescence=e.iridescence),e.iridescenceIOR!==void 0&&(this.iridescenceIOR=e.iridescenceIOR),e.iridescenceThicknessRange!==void 0&&(this.iridescenceThicknessRange=e.iridescenceThicknessRange),e.transmission!==void 0&&(this.transmission=e.transmission),e.thickness!==void 0&&(this.thickness=e.thickness),e.attenuationDistance!==void 0&&(this.attenuationDistance=e.attenuationDistance),e.attenuationColor!==void 0&&this.attenuationColor!==void 0&&this.attenuationColor.setHex(e.attenuationColor),e.anisotropy!==void 0&&(this.anisotropy=e.anisotropy),e.anisotropyRotation!==void 0&&(this.anisotropyRotation=e.anisotropyRotation),e.fog!==void 0&&(this.fog=e.fog),e.flatShading!==void 0&&(this.flatShading=e.flatShading),e.blending!==void 0&&(this.blending=e.blending),e.combine!==void 0&&(this.combine=e.combine),e.side!==void 0&&(this.side=e.side),e.shadowSide!==void 0&&(this.shadowSide=e.shadowSide),e.opacity!==void 0&&(this.opacity=e.opacity),e.transparent!==void 0&&(this.transparent=e.transparent),e.alphaTest!==void 0&&(this.alphaTest=e.alphaTest),e.alphaHash!==void 0&&(this.alphaHash=e.alphaHash),e.depthFunc!==void 0&&(this.depthFunc=e.depthFunc),e.depthTest!==void 0&&(this.depthTest=e.depthTest),e.depthWrite!==void 0&&(this.depthWrite=e.depthWrite),e.colorWrite!==void 0&&(this.colorWrite=e.colorWrite),e.clippingPlanes!==void 0&&(this.clippingPlanes=e.clippingPlanes.map(e=>new Wf().fromJSON(e))),e.clipIntersection!==void 0&&(this.clipIntersection=e.clipIntersection),e.clipShadows!==void 0&&(this.clipShadows=e.clipShadows),e.depthPacking!==void 0&&(this.depthPacking=e.depthPacking),e.blendSrc!==void 0&&(this.blendSrc=e.blendSrc),e.blendDst!==void 0&&(this.blendDst=e.blendDst),e.blendEquation!==void 0&&(this.blendEquation=e.blendEquation),e.blendSrcAlpha!==void 0&&(this.blendSrcAlpha=e.blendSrcAlpha),e.blendDstAlpha!==void 0&&(this.blendDstAlpha=e.blendDstAlpha),e.blendEquationAlpha!==void 0&&(this.blendEquationAlpha=e.blendEquationAlpha),e.blendColor!==void 0&&this.blendColor!==void 0&&this.blendColor.setHex(e.blendColor),e.blendAlpha!==void 0&&(this.blendAlpha=e.blendAlpha),e.stencilWriteMask!==void 0&&(this.stencilWriteMask=e.stencilWriteMask),e.stencilFunc!==void 0&&(this.stencilFunc=e.stencilFunc),e.stencilRef!==void 0&&(this.stencilRef=e.stencilRef),e.stencilFuncMask!==void 0&&(this.stencilFuncMask=e.stencilFuncMask),e.stencilFail!==void 0&&(this.stencilFail=e.stencilFail),e.stencilZFail!==void 0&&(this.stencilZFail=e.stencilZFail),e.stencilZPass!==void 0&&(this.stencilZPass=e.stencilZPass),e.stencilWrite!==void 0&&(this.stencilWrite=e.stencilWrite),e.wireframe!==void 0&&(this.wireframe=e.wireframe),e.wireframeLinewidth!==void 0&&(this.wireframeLinewidth=e.wireframeLinewidth),e.wireframeLinecap!==void 0&&(this.wireframeLinecap=e.wireframeLinecap),e.wireframeLinejoin!==void 0&&(this.wireframeLinejoin=e.wireframeLinejoin),e.rotation!==void 0&&(this.rotation=e.rotation),e.linewidth!==void 0&&(this.linewidth=e.linewidth),e.linecap!==void 0&&(this.linecap=e.linecap),e.linejoin!==void 0&&(this.linejoin=e.linejoin),e.dashSize!==void 0&&(this.dashSize=e.dashSize),e.gapSize!==void 0&&(this.gapSize=e.gapSize),e.scale!==void 0&&(this.scale=e.scale),e.polygonOffset!==void 0&&(this.polygonOffset=e.polygonOffset),e.polygonOffsetFactor!==void 0&&(this.polygonOffsetFactor=e.polygonOffsetFactor),e.polygonOffsetUnits!==void 0&&(this.polygonOffsetUnits=e.polygonOffsetUnits),e.dithering!==void 0&&(this.dithering=e.dithering),e.alphaToCoverage!==void 0&&(this.alphaToCoverage=e.alphaToCoverage),e.premultipliedAlpha!==void 0&&(this.premultipliedAlpha=e.premultipliedAlpha),e.forceSinglePass!==void 0&&(this.forceSinglePass=e.forceSinglePass),e.allowOverride!==void 0&&(this.allowOverride=e.allowOverride),e.visible!==void 0&&(this.visible=e.visible),e.toneMapped!==void 0&&(this.toneMapped=e.toneMapped),e.userData!==void 0&&(this.userData=e.userData),e.vertexColors!==void 0&&(typeof e.vertexColors==`number`?this.vertexColors=e.vertexColors>0:this.vertexColors=e.vertexColors),e.size!==void 0&&(this.size=e.size),e.sizeAttenuation!==void 0&&(this.sizeAttenuation=e.sizeAttenuation),e.map!==void 0&&(this.map=t[e.map]||null),e.matcap!==void 0&&(this.matcap=t[e.matcap]||null),e.alphaMap!==void 0&&(this.alphaMap=t[e.alphaMap]||null),e.bumpMap!==void 0&&(this.bumpMap=t[e.bumpMap]||null),e.bumpScale!==void 0&&(this.bumpScale=e.bumpScale),e.normalMap!==void 0&&(this.normalMap=t[e.normalMap]||null),e.normalMapType!==void 0&&(this.normalMapType=e.normalMapType),e.normalScale!==void 0){let t=e.normalScale;Array.isArray(t)===!1&&(t=[t,t]),this.normalScale=new X().fromArray(t)}return e.displacementMap!==void 0&&(this.displacementMap=t[e.displacementMap]||null),e.displacementScale!==void 0&&(this.displacementScale=e.displacementScale),e.displacementBias!==void 0&&(this.displacementBias=e.displacementBias),e.roughnessMap!==void 0&&(this.roughnessMap=t[e.roughnessMap]||null),e.metalnessMap!==void 0&&(this.metalnessMap=t[e.metalnessMap]||null),e.emissiveMap!==void 0&&(this.emissiveMap=t[e.emissiveMap]||null),e.emissiveIntensity!==void 0&&(this.emissiveIntensity=e.emissiveIntensity),e.specularMap!==void 0&&(this.specularMap=t[e.specularMap]||null),e.specularIntensityMap!==void 0&&(this.specularIntensityMap=t[e.specularIntensityMap]||null),e.specularColorMap!==void 0&&(this.specularColorMap=t[e.specularColorMap]||null),e.envMap!==void 0&&(this.envMap=t[e.envMap]||null),e.envMapRotation!==void 0&&this.envMapRotation.fromArray(e.envMapRotation),e.envMapIntensity!==void 0&&(this.envMapIntensity=e.envMapIntensity),e.reflectivity!==void 0&&(this.reflectivity=e.reflectivity),e.refractionRatio!==void 0&&(this.refractionRatio=e.refractionRatio),e.lightMap!==void 0&&(this.lightMap=t[e.lightMap]||null),e.lightMapIntensity!==void 0&&(this.lightMapIntensity=e.lightMapIntensity),e.aoMap!==void 0&&(this.aoMap=t[e.aoMap]||null),e.aoMapIntensity!==void 0&&(this.aoMapIntensity=e.aoMapIntensity),e.gradientMap!==void 0&&(this.gradientMap=t[e.gradientMap]||null),e.clearcoatMap!==void 0&&(this.clearcoatMap=t[e.clearcoatMap]||null),e.clearcoatRoughnessMap!==void 0&&(this.clearcoatRoughnessMap=t[e.clearcoatRoughnessMap]||null),e.clearcoatNormalMap!==void 0&&(this.clearcoatNormalMap=t[e.clearcoatNormalMap]||null),e.clearcoatNormalScale!==void 0&&(this.clearcoatNormalScale=new X().fromArray(e.clearcoatNormalScale)),e.iridescenceMap!==void 0&&(this.iridescenceMap=t[e.iridescenceMap]||null),e.iridescenceThicknessMap!==void 0&&(this.iridescenceThicknessMap=t[e.iridescenceThicknessMap]||null),e.transmissionMap!==void 0&&(this.transmissionMap=t[e.transmissionMap]||null),e.thicknessMap!==void 0&&(this.thicknessMap=t[e.thicknessMap]||null),e.anisotropyMap!==void 0&&(this.anisotropyMap=t[e.anisotropyMap]||null),e.sheenColorMap!==void 0&&(this.sheenColorMap=t[e.sheenColorMap]||null),e.sheenRoughnessMap!==void 0&&(this.sheenRoughnessMap=t[e.sheenRoughnessMap]||null),this}clone(){return new this.constructor().copy(this)}copy(e){this.name=e.name,this.blending=e.blending,this.side=e.side,this.vertexColors=e.vertexColors,this.opacity=e.opacity,this.transparent=e.transparent,this.blendSrc=e.blendSrc,this.blendDst=e.blendDst,this.blendEquation=e.blendEquation,this.blendSrcAlpha=e.blendSrcAlpha,this.blendDstAlpha=e.blendDstAlpha,this.blendEquationAlpha=e.blendEquationAlpha,this.blendColor.copy(e.blendColor),this.blendAlpha=e.blendAlpha,this.depthFunc=e.depthFunc,this.depthTest=e.depthTest,this.depthWrite=e.depthWrite,this.stencilWriteMask=e.stencilWriteMask,this.stencilFunc=e.stencilFunc,this.stencilRef=e.stencilRef,this.stencilFuncMask=e.stencilFuncMask,this.stencilFail=e.stencilFail,this.stencilZFail=e.stencilZFail,this.stencilZPass=e.stencilZPass,this.stencilWrite=e.stencilWrite;let t=e.clippingPlanes,n=null;if(t!==null){let e=t.length;n=Array(e);for(let r=0;r!==e;++r)n[r]=t[r].clone()}return this.clippingPlanes=n,this.clipIntersection=e.clipIntersection,this.clipShadows=e.clipShadows,this.shadowSide=e.shadowSide,this.colorWrite=e.colorWrite,this.precision=e.precision,this.polygonOffset=e.polygonOffset,this.polygonOffsetFactor=e.polygonOffsetFactor,this.polygonOffsetUnits=e.polygonOffsetUnits,this.dithering=e.dithering,this.alphaTest=e.alphaTest,this.alphaHash=e.alphaHash,this.alphaToCoverage=e.alphaToCoverage,this.premultipliedAlpha=e.premultipliedAlpha,this.forceSinglePass=e.forceSinglePass,this.allowOverride=e.allowOverride,this.visible=e.visible,this.toneMapped=e.toneMapped,this.userData=JSON.parse(JSON.stringify(e.userData)),this}dispose(){this.dispatchEvent({type:`dispose`})}set needsUpdate(e){e===!0&&this.version++}},qf=new Z,Jf=new Z,Yf=new Z,Xf=new Z,Zf=class{constructor(e=new Z,t=new Z(0,0,-1)){this.origin=e,this.direction=t}set(e,t){return this.origin.copy(e),this.direction.copy(t),this}copy(e){return this.origin.copy(e.origin),this.direction.copy(e.direction),this}at(e,t){return t.copy(this.origin).addScaledVector(this.direction,e)}lookAt(e){return this.direction.copy(e).sub(this.origin).normalize(),this}recast(e){return this.origin.copy(this.at(e,qf)),this}closestPointToPoint(e,t){t.subVectors(e,this.origin);let n=t.dot(this.direction);return n<0?t.copy(this.origin):t.copy(this.origin).addScaledVector(this.direction,n)}distanceToPoint(e){return Math.sqrt(this.distanceSqToPoint(e))}distanceSqToPoint(e){let t=qf.subVectors(e,this.origin).dot(this.direction);return t<0?this.origin.distanceToSquared(e):(qf.copy(this.origin).addScaledVector(this.direction,t),qf.distanceToSquared(e))}distanceSqToSegment(e,t,n,r){Jf.copy(e).add(t).multiplyScalar(.5),Yf.copy(t).sub(e).normalize(),Xf.copy(this.origin).sub(Jf);let i=e.distanceTo(t)*.5,a=-this.direction.dot(Yf),o=Xf.dot(this.direction),s=-Xf.dot(Yf),c=Xf.lengthSq(),l=Math.abs(1-a*a),u,d,f,p;if(l>0)if(u=a*s-o,d=a*o-s,p=i*l,u>=0)if(d>=-p)if(d<=p){let e=1/l;u*=e,d*=e,f=u*(u+a*d+2*o)+d*(a*u+d+2*s)+c}else d=i,u=Math.max(0,-(a*d+o)),f=-u*u+d*(d+2*s)+c;else d=-i,u=Math.max(0,-(a*d+o)),f=-u*u+d*(d+2*s)+c;else d<=-p?(u=Math.max(0,-(-a*i+o)),d=u>0?-i:Math.min(Math.max(-i,-s),i),f=-u*u+d*(d+2*s)+c):d<=p?(u=0,d=Math.min(Math.max(-i,-s),i),f=d*(d+2*s)+c):(u=Math.max(0,-(a*i+o)),d=u>0?i:Math.min(Math.max(-i,-s),i),f=-u*u+d*(d+2*s)+c);else d=a>0?-i:i,u=Math.max(0,-(a*d+o)),f=-u*u+d*(d+2*s)+c;return n&&n.copy(this.origin).addScaledVector(this.direction,u),r&&r.copy(Jf).addScaledVector(Yf,d),f}intersectSphere(e,t){if(e.radius<0)return null;qf.subVectors(e.center,this.origin);let n=qf.dot(this.direction),r=qf.dot(qf)-n*n,i=e.radius*e.radius;if(r>i)return null;let a=Math.sqrt(i-r),o=n-a,s=n+a;return s<0?null:o<0?this.at(s,t):this.at(o,t)}intersectsSphere(e){return e.radius<0?!1:this.distanceSqToPoint(e.center)<=e.radius*e.radius}distanceToPlane(e){let t=e.normal.dot(this.direction);if(t===0)return e.distanceToPoint(this.origin)===0?0:null;let n=-(this.origin.dot(e.normal)+e.constant)/t;return n>=0?n:null}intersectPlane(e,t){let n=this.distanceToPlane(e);return n===null?null:this.at(n,t)}intersectsPlane(e){let t=e.distanceToPoint(this.origin);return t===0||e.normal.dot(this.direction)*t<0}intersectBox(e,t){let n,r,i,a,o,s,c=1/this.direction.x,l=1/this.direction.y,u=1/this.direction.z,d=this.origin;return c>=0?(n=(e.min.x-d.x)*c,r=(e.max.x-d.x)*c):(n=(e.max.x-d.x)*c,r=(e.min.x-d.x)*c),l>=0?(i=(e.min.y-d.y)*l,a=(e.max.y-d.y)*l):(i=(e.max.y-d.y)*l,a=(e.min.y-d.y)*l),n>a||i>r||((i>n||isNaN(n))&&(n=i),(a=0?(o=(e.min.z-d.z)*u,s=(e.max.z-d.z)*u):(o=(e.max.z-d.z)*u,s=(e.min.z-d.z)*u),n>s||o>r)||((o>n||n!==n)&&(n=o),(s=0?n:r,t)}intersectsBox(e){return this.intersectBox(e,qf)!==null}intersectTriangle(e,t,n,r,i){let a=this.origin,o=this.direction,s=o.x,c=o.y,l=o.z,u=e.x-a.x,d=e.y-a.y,f=e.z-a.z,p=t.x-a.x,m=t.y-a.y,h=t.z-a.z,g=n.x-a.x,_=n.y-a.y,v=n.z-a.z,y=Math.abs(s),b=Math.abs(c),x=Math.abs(l),S,C,w,T,E,D,O,ee,k,A,te,j;if(y>=b&&y>=x?(w=s,D=u,k=p,j=g,s>=0?(S=c,C=l,T=d,E=f,O=m,ee=h,A=_,te=v):(S=l,C=c,T=f,E=d,O=h,ee=m,A=v,te=_)):b>=x?(w=c,D=d,k=m,j=_,c>=0?(S=l,C=s,T=f,E=u,O=h,ee=p,A=v,te=g):(S=s,C=l,T=u,E=f,O=p,ee=h,A=g,te=v)):(w=l,D=f,k=h,j=v,l>=0?(S=s,C=c,T=u,E=d,O=p,ee=m,A=g,te=_):(S=c,C=s,T=d,E=u,O=m,ee=p,A=_,te=g)),w===0)return null;let M=S/w,ne=C/w,N=1/w,P=T-M*D,F=E-ne*D,re=O-M*k,ie=ee-ne*k,ae=A-M*j,I=te-ne*j,L=ae*ie-I*re,oe=P*I-F*ae,se=re*F-ie*P;if(r){if(L<0||oe<0||se<0)return null}else if((L<0||oe<0||se<0)&&(L>0||oe>0||se>0))return null;let ce=L+oe+se;if(ce===0)return null;let le=N*(L*D+oe*k+se*j);return(ce>0?le<0:le>0)?null:this.at(le/ce,i)}applyMatrix4(e){return this.origin.applyMatrix4(e),this.direction.transformDirection(e),this}equals(e){return e.origin.equals(this.origin)&&e.direction.equals(this.direction)}clone(){return new this.constructor().copy(this)}},Qf=class extends Kf{constructor(e){super(),this.isMeshBasicMaterial=!0,this.type=`MeshBasicMaterial`,this.color=new Q(16777215),this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new hd,this.combine=0,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap=`round`,this.wireframeLinejoin=`round`,this.fog=!0,this.setValues(e)}copy(e){return super.copy(e),this.color.copy(e.color),this.map=e.map,this.lightMap=e.lightMap,this.lightMapIntensity=e.lightMapIntensity,this.aoMap=e.aoMap,this.aoMapIntensity=e.aoMapIntensity,this.specularMap=e.specularMap,this.alphaMap=e.alphaMap,this.envMap=e.envMap,this.envMapRotation.copy(e.envMapRotation),this.combine=e.combine,this.reflectivity=e.reflectivity,this.refractionRatio=e.refractionRatio,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this.wireframeLinecap=e.wireframeLinecap,this.wireframeLinejoin=e.wireframeLinejoin,this.fog=e.fog,this}},$f=new ad,ep=new Zf,tp=new kf,np=new Z,rp=new Z,ip=new Z,ap=new Z,op=new Z,sp=new Z,cp=new Z,lp=new Z,up=class extends Md{constructor(e=new Lf,t=new Qf){super(),this.isMesh=!0,this.type=`Mesh`,this.geometry=e,this.material=t,this.morphTargetDictionary=void 0,this.morphTargetInfluences=void 0,this.count=1,this.updateMorphTargets()}copy(e,t){return super.copy(e,t),e.morphTargetInfluences!==void 0&&(this.morphTargetInfluences=e.morphTargetInfluences.slice()),e.morphTargetDictionary!==void 0&&(this.morphTargetDictionary=Object.assign({},e.morphTargetDictionary)),this.material=Array.isArray(e.material)?e.material.slice():e.material,this.geometry=e.geometry,this}updateMorphTargets(){let e=this.geometry.morphAttributes,t=Object.keys(e);if(t.length>0){let n=e[t[0]];if(n!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let e=0,t=n.length;e(e.far-e.near)**2))&&($f.copy(i).invert(),ep.copy(e.ray).applyMatrix4($f),!(n.boundingBox!==null&&ep.intersectsBox(n.boundingBox)===!1)&&this._computeIntersections(e,t,ep)))}_computeIntersections(e,t,n){let r,i=this.geometry,a=this.material,o=i.index,s=i.attributes.position,c=i.attributes.uv,l=i.attributes.uv1,u=i.attributes.normal,d=i.groups,f=i.drawRange;if(o!==null)if(Array.isArray(a))for(let i=0,s=d.length;in.far?null:{distance:l,point:lp.clone(),object:e}}function fp(e,t,n,r,i,a,o,s,c,l){e.getVertexPosition(s,rp),e.getVertexPosition(c,ip),e.getVertexPosition(l,ap);let u=dp(e,t,n,r,rp,ip,ap,cp);if(u){let e=new Z;nf.getBarycoord(cp,rp,ip,ap,e),i&&(u.uv=nf.getInterpolatedAttribute(i,s,c,l,e,new X)),a&&(u.uv1=nf.getInterpolatedAttribute(a,s,c,l,e,new X)),o&&(u.normal=nf.getInterpolatedAttribute(o,s,c,l,e,new Z),u.normal.dot(r.direction)>0&&u.normal.multiplyScalar(-1));let t={a:s,b:c,c:l,normal:new Z,materialIndex:0};nf.getNormal(rp,ip,ap,t.normal),u.face=t,u.barycoord=e}return u}var pp=new ed,mp=new ed,hp=new ed,gp=new ed,_p=new ad,vp=new Z,yp=new kf,bp=new ad,xp=new Zf,Sp=class extends up{constructor(e,t){super(e,t),this.isSkinnedMesh=!0,this.type=`SkinnedMesh`,this.bindMode=_c,this.bindMatrix=new ad,this.bindMatrixInverse=new ad,this.boundingBox=null,this.boundingSphere=null}computeBoundingBox(){let e=this.geometry;this.boundingBox===null&&(this.boundingBox=new rf),this.boundingBox.makeEmpty();let t=e.getAttribute(`position`);for(let e=0;e0?e.max.x:e.min.x,zp.y=r.normal.y>0?e.max.y:e.min.y,zp.z=r.normal.z>0?e.max.z:e.min.z,r.distanceToPoint(zp)<0)return!1}return!0}containsPoint(e){let t=this.planes;for(let n=0;n<6;n++)if(t[n].distanceToPoint(e)<0)return!1;return!0}clone(){return new this.constructor().copy(this)}},Vp=class extends Kf{constructor(e){super(),this.isLineBasicMaterial=!0,this.type=`LineBasicMaterial`,this.color=new Q(16777215),this.map=null,this.linewidth=1,this.linecap=`round`,this.linejoin=`round`,this.fog=!0,this.setValues(e)}copy(e){return super.copy(e),this.color.copy(e.color),this.map=e.map,this.linewidth=e.linewidth,this.linecap=e.linecap,this.linejoin=e.linejoin,this.fog=e.fog,this}},Hp=new Z,Up=new Z,Wp=new ad,Gp=new Zf,Kp=new kf,qp=new Z,Jp=new Z,Yp=class extends Md{constructor(e=new Lf,t=new Vp){super(),this.isLine=!0,this.type=`Line`,this.geometry=e,this.material=t,this.morphTargetDictionary=void 0,this.morphTargetInfluences=void 0,this.updateMorphTargets()}copy(e,t){return super.copy(e,t),this.material=Array.isArray(e.material)?e.material.slice():e.material,this.geometry=e.geometry,this}computeLineDistances(){let e=this.geometry;if(e.index===null){let t=e.attributes.position,n=[0];for(let e=1,r=t.count;e0){let n=e[t[0]];if(n!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let e=0,t=n.length;er)return;qp.applyMatrix4(e.matrixWorld);let c=t.ray.origin.distanceTo(qp);if(!(ct.far))return{distance:c,point:Jp.clone().applyMatrix4(e.matrixWorld),index:o,face:null,faceIndex:null,barycoord:null,object:e}}var Zp=new Z,Qp=new Z,$p=class extends Yp{constructor(e,t){super(e,t),this.isLineSegments=!0,this.type=`LineSegments`}computeLineDistances(){let e=this.geometry;if(e.index===null){let t=e.attributes.position,n=[];for(let e=0,r=t.count;e0){let n=e[t[0]];if(n!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let e=0,t=n.length;ei.far)return;a.push({distance:c,distanceToRay:Math.sqrt(s),point:n,index:t,face:null,faceIndex:null,barycoord:null,object:o})}}var cm=class extends $u{constructor(e=[],t=301,n,r,i,a,o,s,c,l){super(e,t,n,r,i,a,o,s,c,l),this.isCubeTexture=!0,this.flipY=!1}get images(){return this.image}set images(e){this.image=e}},lm=class extends $u{constructor(e,t,n,r,i,a,o,s,c){super(e,t,n,r,i,a,o,s,c),this.isCanvasTexture=!0,this.needsUpdate=!0}},um=class extends $u{constructor(e,t,n=Mc,r,i,a,o=xc,s=xc,c,l=Uc,u=1){if(l!==1026&&l!==1027)throw Error(`THREE.DepthTexture: format must be either THREE.DepthFormat or THREE.DepthStencilFormat`);super({width:e,height:t,depth:u},r,i,a,o,s,l,n,c),this.isDepthTexture=!0,this.flipY=!1,this.generateMipmaps=!1,this.compareFunction=null}copy(e){return super.copy(e),this.source=new Yu(Object.assign({},e.image)),this.compareFunction=e.compareFunction,this}toJSON(e){let t=super.toJSON(e);return t.compareFunction=this.compareFunction,t}},dm=class extends um{constructor(e,t=Mc,n=301,r,i,a=xc,o=xc,s,c=Uc){let l={width:e,height:e,depth:1},u=[l,l,l,l,l,l];super(e,e,t,n,r,i,a,o,s,c),this.image=u,this.isCubeDepthTexture=!0,this.isCubeTexture=!0}get images(){return this.image}set images(e){this.image=e}},fm=class extends $u{constructor(e=null){super(),this.sourceTexture=e,this.isExternalTexture=!0}copy(e){return super.copy(e),this.sourceTexture=e.sourceTexture,this}},pm=class e extends Lf{constructor(e=1,t=1,n=1,r=1,i=1,a=1){super(),this.type=`BoxGeometry`,this.parameters={width:e,height:t,depth:n,widthSegments:r,heightSegments:i,depthSegments:a};let o=this;r=Math.floor(r),i=Math.floor(i),a=Math.floor(a);let s=[],c=[],l=[],u=[],d=0,f=0;p(`z`,`y`,`x`,-1,-1,n,t,e,a,i,0),p(`z`,`y`,`x`,1,-1,n,t,-e,a,i,1),p(`x`,`z`,`y`,1,1,e,n,t,r,a,2),p(`x`,`z`,`y`,1,-1,e,n,-t,r,a,3),p(`x`,`y`,`z`,1,-1,e,t,n,r,i,4),p(`x`,`y`,`z`,-1,-1,e,t,-n,r,i,5),this.setIndex(s),this.setAttribute(`position`,new Tf(c,3)),this.setAttribute(`normal`,new Tf(l,3)),this.setAttribute(`uv`,new Tf(u,2));function p(e,t,n,r,i,a,p,m,h,g,_){let v=a/h,y=p/g,b=a/2,x=p/2,S=m/2,C=h+1,w=g+1,T=0,E=0,D=new Z;for(let a=0;a0?1:-1,l.push(D.x,D.y,D.z),u.push(s/h),u.push(1-a/g),T+=1}for(let e=0;e0){let e=(_-1)*m;for(let t=0;t0&&v(!0),t>0&&v(!1)),this.setIndex(l),this.setAttribute(`position`,new Tf(u,3)),this.setAttribute(`normal`,new Tf(d,3)),this.setAttribute(`uv`,new Tf(f,2));function _(){let a=new Z,_=new Z,v=0,y=(t-e)/n;for(let c=0;c<=i;c++){let l=[],g=c/i,v=g*(t-e)+e;for(let e=0;e<=r;e++){let t=e/r,i=t*s+o,c=Math.sin(i),m=Math.cos(i);_.x=v*c,_.y=-g*n+h,_.z=v*m,u.push(_.x,_.y,_.z),a.set(c,y,m).normalize(),d.push(a.x,a.y,a.z),f.push(t,1-g),l.push(p++)}m.push(l)}for(let n=0;n0||r!==0)&&(l.push(a,o,c),v+=3),(t>0||r!==i-1)&&(l.push(o,s,c),v+=3)}c.addGroup(g,v,0),g+=v}function v(n){let i=p,a=new X,m=new Z,_=0,v=n===!0?e:t,y=n===!0?1:-1;for(let e=1;e<=r;e++)u.push(0,h*y,0),d.push(0,y,0),f.push(.5,.5),p++;let b=p;for(let e=0;e<=r;e++){let t=e/r*s+o,n=Math.cos(t),i=Math.sin(t);m.x=v*i,m.y=h*y,m.z=v*n,u.push(m.x,m.y,m.z),d.push(0,y,0),a.x=n*.5+.5,a.y=i*.5*y+.5,f.push(a.x,a.y),p++}for(let e=0;e.9&&Math.min(t,n,r)<.1&&(t<.2&&(a[e+0]+=1),n<.2&&(a[e+2]+=1),r<.2&&(a[e+4]+=1))}}function d(e){i.push(e.x,e.y,e.z)}function f(t,n){let r=t*3;n.x=e[r+0],n.y=e[r+1],n.z=e[r+2]}function p(){let e=new Z,t=new Z,n=new Z,r=new Z,o=new X,s=new X,c=new X;for(let l=0,u=0;l0)s=r-1;else{s=r;break}if(r=s,n[r]===a)return r/(i-1);let l=n[r],u=n[r+1]-l,d=(a-l)/u;return(r+d)/(i-1)}getTangent(e,t){let n=1e-4,r=e-n,i=e+n;r<0&&(r=0),i>1&&(i=1);let a=this.getPoint(r),o=this.getPoint(i),s=t||(a.isVector2?new X:new Z);return s.copy(o).sub(a).normalize(),s}getTangentAt(e,t){let n=this.getUtoTmapping(e);return this.getTangent(n,t)}computeFrenetFrames(e,t=!1){let n=new Z,r=[],i=[],a=[],o=new Z,s=new ad;for(let t=0;t<=e;t++){let n=t/e;r[t]=this.getTangentAt(n,new Z)}i[0]=new Z,a[0]=new Z;let c=Number.MAX_VALUE,l=Math.abs(r[0].x),u=Math.abs(r[0].y),d=Math.abs(r[0].z);l<=c&&(c=l,n.set(1,0,0)),u<=c&&(c=u,n.set(0,1,0)),d<=c&&n.set(0,0,1),o.crossVectors(r[0],n).normalize(),i[0].crossVectors(r[0],o),a[0].crossVectors(r[0],i[0]);for(let t=1;t<=e;t++){if(i[t]=i[t-1].clone(),a[t]=a[t-1].clone(),o.crossVectors(r[t-1],r[t]),o.length()>2**-52){o.normalize();let e=Math.acos(pu(r[t-1].dot(r[t]),-1,1));i[t].applyMatrix4(s.makeRotationAxis(o,e))}a[t].crossVectors(r[t],i[t])}if(t===!0){let t=Math.acos(pu(i[0].dot(i[e]),-1,1));t/=e,r[0].dot(o.crossVectors(i[0],i[e]))>0&&(t=-t);for(let n=1;n<=e;n++)i[n].applyMatrix4(s.makeRotationAxis(r[n],t*n)),a[n].crossVectors(r[n],i[n])}return{tangents:r,normals:i,binormals:a}}clone(){return new this.constructor().copy(this)}copy(e){return this.arcLengthDivisions=e.arcLengthDivisions,this}toJSON(){let e={metadata:{version:4.7,type:`Curve`,generator:`Curve.toJSON`}};return e.arcLengthDivisions=this.arcLengthDivisions,e.type=this.type,e}fromJSON(e){return this.arcLengthDivisions=e.arcLengthDivisions,this}},bm=class extends ym{constructor(e=0,t=0,n=1,r=1,i=0,a=Math.PI*2,o=!1,s=0){super(),this.isEllipseCurve=!0,this.type=`EllipseCurve`,this.aX=e,this.aY=t,this.xRadius=n,this.yRadius=r,this.aStartAngle=i,this.aEndAngle=a,this.aClockwise=o,this.aRotation=s}getPoint(e,t=new X){let n=t,r=Math.PI*2,i=this.aEndAngle-this.aStartAngle,a=Math.abs(i)<2**-52;for(;i<0;)i+=r;for(;i>r;)i-=r;i<2**-52&&(i=a?0:r),this.aClockwise===!0&&!a&&(i===r?i=-r:i-=r);let o=this.aStartAngle+e*i,s=this.aX+this.xRadius*Math.cos(o),c=this.aY+this.yRadius*Math.sin(o);if(this.aRotation!==0){let e=Math.cos(this.aRotation),t=Math.sin(this.aRotation),n=s-this.aX,r=c-this.aY;s=n*e-r*t+this.aX,c=n*t+r*e+this.aY}return n.set(s,c)}copy(e){return super.copy(e),this.aX=e.aX,this.aY=e.aY,this.xRadius=e.xRadius,this.yRadius=e.yRadius,this.aStartAngle=e.aStartAngle,this.aEndAngle=e.aEndAngle,this.aClockwise=e.aClockwise,this.aRotation=e.aRotation,this}toJSON(){let e=super.toJSON();return e.aX=this.aX,e.aY=this.aY,e.xRadius=this.xRadius,e.yRadius=this.yRadius,e.aStartAngle=this.aStartAngle,e.aEndAngle=this.aEndAngle,e.aClockwise=this.aClockwise,e.aRotation=this.aRotation,e}fromJSON(e){return super.fromJSON(e),this.aX=e.aX,this.aY=e.aY,this.xRadius=e.xRadius,this.yRadius=e.yRadius,this.aStartAngle=e.aStartAngle,this.aEndAngle=e.aEndAngle,this.aClockwise=e.aClockwise,this.aRotation=e.aRotation,this}},xm=class extends bm{constructor(e,t,n,r,i,a){super(e,t,n,n,r,i,a),this.isArcCurve=!0,this.type=`ArcCurve`}};function Sm(){let e=0,t=0,n=0,r=0;function i(i,a,o,s){e=i,t=o,n=-3*i+3*a-2*o-s,r=2*i-2*a+o+s}return{initCatmullRom:function(e,t,n,r,a){i(t,n,a*(n-e),a*(r-t))},initNonuniformCatmullRom:function(e,t,n,r,a,o,s){let c=(t-e)/a-(n-e)/(a+o)+(n-t)/o,l=(n-t)/o-(r-t)/(o+s)+(r-n)/s;c*=o,l*=o,i(t,n,c,l)},calc:function(i){let a=i*i,o=a*i;return e+t*i+n*a+r*o}}}var Cm=new Z,wm=new Z,Tm=new Sm,Em=new Sm,Dm=new Sm,Om=class extends ym{constructor(e=[],t=!1,n=`centripetal`,r=.5){super(),this.isCatmullRomCurve3=!0,this.type=`CatmullRomCurve3`,this.points=e,this.closed=t,this.curveType=n,this.tension=r}getPoint(e,t=new Z){let n=t,r=this.points,i=r.length,a=(i-+!this.closed)*e,o=Math.floor(a),s=a-o;this.closed?o+=o>0?0:(Math.floor(Math.abs(o)/i)+1)*i:s===0&&o===i-1&&(o=i-2,s=1);let c,l;this.closed||o>0?c=r[(o-1)%i]:(wm.subVectors(r[0],r[1]).add(r[0]),c=wm);let u=r[o%i],d=r[(o+1)%i];if(this.closed||o+2r.length-2?r.length-1:a+1],u=r[a>r.length-3?r.length-1:a+2];return n.set(km(o,s.x,c.x,l.x,u.x),km(o,s.y,c.y,l.y,u.y)),n}copy(e){super.copy(e),this.points=[];for(let t=0,n=e.points.length;t0)&&f.push(t,i,c),(e!==n-1||s${eu.map(e=>``).join(``)} +`,e.append(this.root),this.stickZone=this.root.querySelector(`[data-stick]`),this.stickBase=this.root.querySelector(`[data-stick-base]`),this.stickKnob=this.root.querySelector(`[data-stick-knob]`),this.pads.push(...this.root.querySelectorAll(`[data-punch]`)),this.moves.push(...this.root.querySelectorAll(`[data-move]`)),this.bind()}bind(){let e=this.stickZone;e.addEventListener(`pointerdown`,t=>{if(this.stickPointer!==null)return;t.preventDefault(),this.stickPointer=t.pointerId,this.stickOrigin={x:t.clientX,y:t.clientY},e.setPointerCapture(t.pointerId);let n=e.getBoundingClientRect();this.stickBase.style.left=`${t.clientX-n.left}px`,this.stickBase.style.top=`${t.clientY-n.top}px`,this.stickBase.classList.add(`active`),this.updateStick(t.clientX,t.clientY)}),e.addEventListener(`pointermove`,e=>{e.pointerId===this.stickPointer&&(e.preventDefault(),this.updateStick(e.clientX,e.clientY))});let t=e=>{e.pointerId===this.stickPointer&&(this.stickPointer=null,this.moveX=0,this.moveY=0,this.stickKnob.style.transform=`translate(-50%, -50%)`,this.stickBase.classList.remove(`active`))};e.addEventListener(`pointerup`,t),e.addEventListener(`pointercancel`,t);for(let e of this.pads)e.addEventListener(`pointerdown`,t=>{if(!this.enabled)return;t.preventDefault();let n=e.getBoundingClientRect(),r=t.clientX-n.lefte.classList.remove(`pressed`),120)});for(let e of this.moves)e.addEventListener(`pointerdown`,t=>{!this.enabled||this.knockdown||(t.preventDefault(),this.sharedActions.push(`touch`,{kind:e.dataset.move}),e.classList.add(`pressed`),window.setTimeout(()=>e.classList.remove(`pressed`),120))});for(let e of this.root.querySelectorAll(`[data-mod], [data-guard]`)){let t=e.dataset.mod??e.dataset.guard??``,n=n=>{n.preventDefault(),e.setPointerCapture(n.pointerId),this.heldPointers.set(n.pointerId,t),(t===`guard_high`||t===`guard_low`)&&this.sharedActions.clear(),this.syncHeld(),e.classList.add(`pressed`)},r=n=>{this.heldPointers.delete(n.pointerId)&&(this.syncHeld(),e.classList.toggle(`pressed`,[...this.heldPointers.values()].includes(t)))};e.addEventListener(`pointerdown`,n),e.addEventListener(`pointerup`,r),e.addEventListener(`pointercancel`,r)}}syncHeld(){let e=[...this.heldPointers.values()];this.body=e.includes(`body`),this.power=e.includes(`power`),this.guard=e.findLast(e=>e===`guard_high`||e===`guard_low`)??`none`}updateStick(e,t){let n=e-this.stickOrigin.x,r=t-this.stickOrigin.y,i=Math.hypot(n,r);i>56&&(n=n/i*56,r=r/i*56),this.stickKnob.style.transform=`translate(calc(-50% + ${n}px), calc(-50% + ${r}px))`;let a=Math.min(1,i/56),o=a<.15?0:(a-.15)/.85,s=i>0?n/Math.max(i,1e-6):0,c=i>0?r/Math.max(i,1e-6):0;this.moveX=Math.round(s*o*1e3)||0,this.moveY=Math.round(-c*o*1e3)||0}setKnockdown(e){if(this.knockdown!==e){this.knockdown=e,this.root.classList.toggle(`knockdown`,e);for(let[t,n]of this.pads.entries()){let r=n.querySelector(`.touch-pad-label`);r!==null&&(r.textContent=e?t%2==0?`GET UP`:`RHYTHM`:eu[t].label)}}}setEnabled(e,t=e){this.enabled=e,this.root.classList.toggle(`disabled`,!t),this.root.classList.toggle(`resting`,t&&!e),t?e||this.sharedActions.clearSource(`touch`):this.reset()}frame(){return this.enabled?{moveX:this.moveX,moveY:this.moveY,defense:this.guard,actions:[]}:{moveX:0,moveY:0,defense:`none`,actions:[]}}reset(){this.stickPointer=null,this.moveX=0,this.moveY=0,this.body=!1,this.power=!1,this.guard=`none`,this.heldPointers.clear(),this.stickBase.classList.remove(`active`);for(let e of this.root.querySelectorAll(`.pressed`))e.classList.remove(`pressed`);this.sharedActions.clearSource(`touch`)}destroy(){this.reset(),this.root.remove()}},ru={block:[55,.12,.22],hit:[85,.3,.42],counter_hit:[105,.38,.5],stun:[125,.42,.55],knockdown:[180,.62,.65],parry:[60,.18,.4],body_collapse:[170,.55,.3]},iu=class{settings;constructor(e){this.settings=e}event(e){if(!this.settings().haptics)return;let t=ru[e.kind];if(t===void 0)return;let n=this.padActuator();if(n===null){this.vibrate(t[0]);return}try{Promise.resolve(n.playEffect(`dual-rumble`,{duration:Math.min(180,Math.max(0,t[0])),strongMagnitude:Math.min(.65,Math.max(0,t[1])),weakMagnitude:Math.min(.65,Math.max(0,t[2]))})).catch(()=>void 0)}catch{}}padActuator(){if(typeof navigator.getGamepads!=`function`)return null;let e;try{e=[...navigator.getGamepads.call(navigator)]}catch{return null}let t=e.find(e=>e!==null&&e.connected&&`vibrationActuator`in e)?.vibrationActuator;return t==null||typeof t.playEffect!=`function`?null:t}vibrate(e){if(!(!$l()||typeof navigator.vibrate!=`function`))try{navigator.vibrate(Math.min(180,Math.max(0,e)))}catch{}}},au={KeyF:[`left`,`jab`],KeyJ:[`right`,`jab`],KeyR:[`left`,`straight`],KeyU:[`right`,`straight`],KeyG:[`left`,`hook`],KeyH:[`right`,`hook`],KeyT:[`left`,`uppercut`],KeyY:[`right`,`uppercut`]},ou={KeyZ:{kind:`slip_left`},KeyX:{kind:`slip_right`},KeyC:{kind:`weave`},KeyV:{kind:`pull`},KeyB:{kind:`clinch`},KeyN:{kind:`switch_stance`},KeyM:{kind:`taunt`},Digit1:{kind:`foul`,foul:`low_blow`},Digit2:{kind:`foul`,foul:`headbutt`},ArrowLeft:{kind:`get_up_left`},ArrowRight:{kind:`get_up_right`}},su=new Set([...Object.keys(au),...Object.keys(ou),`KeyW`,`KeyA`,`KeyS`,`KeyD`,`KeyQ`,`KeyE`,`ShiftLeft`,`ShiftRight`,`AltLeft`,`AltRight`]),cu=[`Move: W up · S down · A left · D right`,`High / low guard: Q / E`,`Left/right jab: F / J`,`Left/right straight: R / U`,`Left/right hook: G / H`,`Left/right uppercut: T / Y`,`Body: Shift · Power: Alt`,`Slip: Z / X · Weave: C · Pull: V`,`Clinch: B · Stance: N · Taunt: M · Fouls (on purpose): Shift+1 low blow / Shift+2 headbutt`,`Get-up rhythm: ← / →`,`Camera: K cycles broadcast, close and over the shoulder (behind your fighter, W walks at the opponent and A / D circle him).`,`Style pick before the bout: arrow keys choose · Enter settles · 1–5 settle on that card at once.`,`Controller move: left stick. High / low guard: left / right shoulder (independent of punches).`,`Controller face classes: bottom jab · right straight · left hook · top uppercut.`,`Controller face hand: hold D-pad left for left hand or D-pad right for right hand, then press a face punch; otherwise punches use the right hand. A direction used for a punch is consumed and does not evade.`,`Controller modifiers: left trigger body · right trigger power.`,`Controller actions: left stick press clinch · right stick press switch stance · hold right trigger + right stick press to taunt · D-pad up weave · D-pad down pull · tap and release D-pad left/right to slip; while down, D-pad left/right performs the private get-up rhythm immediately.`,`Controller fouls: View/Back low blow · Menu/Start headbutt.`,`Controller style pick: D-pad or left stick to choose · bottom face button settles.`,`Right-stick gesture: horizontal 0–22.5° hook · 22.5–45° jab · 45–70° straight · 70–90° uppercut; left/right direction selects hand.`,`Between rounds, tell your corner what to work on: 1 close the cut · 2 bring down the swelling · 3 catch your breath (controller: left, top or right face button; or tap).`,`Touch: drag the left side to move · tap a punch pad's left or right half for that hand · hold BODY, POWER, GUARD or LOW · tap ◀ SLIP, SLIP ▶, WEAVE, PULL or CLINCH.`],lu=`Move WASD · Jab F/J · Straight R/U · Hook G/H · Uppercut T/Y · Guard Q/E · Body Shift · Power Alt`,uu=`Drag on the left to move · Tap the pads to punch, L or R hand · Hold BODY, POWER or GUARD · Tap SLIP, WEAVE, PULL or CLINCH`,du=`Drag left to move · Tap pads to punch · Hold GUARD`,fu=(e,t)=>e?t<480?du:uu:lu,pu=[{title:`Keyboard`,items:cu.filter(e=>!e.startsWith(`Controller`)&&!e.startsWith(`Right-stick`)&&!e.startsWith(`Between rounds`)&&!e.startsWith(`Touch`))},{title:`Between rounds`,items:cu.filter(e=>e.startsWith(`Between rounds`))},{title:`Controller`,items:cu.filter(e=>e.startsWith(`Controller`)||e.startsWith(`Right-stick`))},{title:`Touch`,items:cu.filter(e=>e.startsWith(`Touch`))}];function mu(e,t){return e.kind===t.kind?e.kind===`punch`&&t.kind===`punch`?e.hand===t.hand&&e.class===t.class&&e.target===t.target&&e.power===t.power:e.kind===`foul`&&t.kind===`foul`?e.foul===t.foul:!0:!1}function hu(e,t,n){if(n<1||e.length>0&&mu(e.at(-1),t))return;let r=e.length-n+1;r>0&&e.splice(0,r),e.push(t)}var gu=class{maximum;queue=[];counter=0;listener=null;constructor(e=1){this.maximum=e}setListener(e){this.listener=e}push(e,t){if(this.maximum<1)return;let n=this.queue.at(-1);if(n!==void 0&&mu(n.action,t)){n.source=e;return}let r={...t,id:`c${this.counter+=1}`},i=this.queue.length-this.maximum+1;i>0&&this.queue.splice(0,i),this.queue.push({action:r,source:e}),this.listener?.(r)}clear(){this.queue.length=0}clearSource(e){let t=this.queue.filter(t=>t.source!==e);this.queue.splice(0,this.queue.length,...t)}drain(e){return this.queue.splice(0,e).map(e=>e.action)}};function _u(e,t,n=.18){let r=Math.hypot(e,t);if(!Number.isFinite(r)||r<=n)return{x:0,y:0};let i=(Math.min(1,r)-n)/(1-n);return{x:e/r*i,y:t/r*i}}var vu=(e,t)=>{let n=Math.atan2(Math.abs(t),Math.abs(e))*180/Math.PI;return n<22.5?`hook`:n<45?`jab`:n<70?`straight`:`uppercut`},yu=class{active=!1;hand=`right`;target=`head`;power=`normal`;peak={x:0,y:0};peakMagnitude=0;update(e,t,n,r){let i=Math.hypot(e,t);if(!this.active&&i>=.55&&Math.abs(e)>=.12)return this.active=!0,this.hand=e<0?`left`:`right`,this.target=n?`body`:`head`,this.power=r?`power`:`normal`,this.peak={x:e,y:t},this.peakMagnitude=i,null;if(!this.active)return null;if(i>this.peakMagnitude&&(this.peak={x:e,y:t},this.peakMagnitude=i),i<=.25){let e={kind:`punch`,hand:this.hand,class:vu(this.peak.x,this.peak.y),target:this.target,power:this.power};return this.reset(),e}return null}reset(){this.active=!1,this.peak={x:0,y:0},this.peakMagnitude=0}},bu=[`jab`,`straight`,`hook`,`uppercut`],xu=(e,t)=>e.buttons[t]?.pressed===!0;function Su(){let e=navigator.getGamepads;if(typeof e!=`function`)return null;try{return[...e.call(navigator)].find(e=>e!==null&&e.connected&&e.mapping===`standard`)??null}catch{return null}}var Cu=class{maximumQueue;sharedActions;gesture=new yu;queue=[];previous=new Set;selectorConsumed=new Map;raf=0;currentId=null;moveX=0;moveY=0;defense=`none`;knockdown=!1;enabled=!0;disconnect=e=>{e.gamepad.id===this.currentId&&this.reset()};constructor(e=1,t){this.maximumQueue=e,this.sharedActions=t,window.addEventListener(`gamepaddisconnected`,this.disconnect),this.raf=requestAnimationFrame(()=>this.poll())}push(e){this.sharedActions===void 0?hu(this.queue,e,this.maximumQueue):this.sharedActions.push(`gamepad`,e)}clearActions(){this.queue.length=0,this.sharedActions?.clear()}poll(){if(!this.enabled){this.reset(),this.schedulePoll();return}let e=Su();if(e===null){this.currentId!==null&&this.reset(),this.schedulePoll();return}this.currentId!==null&&this.currentId!==e.id&&this.reset(),this.currentId=e.id;let t=_u(e.axes[0]??0,e.axes[1]??0);this.moveX=Math.round(t.x*1e3),this.moveY=Math.round(-t.y*1e3);let n=xu(e,4)?`guard_high`:xu(e,5)?`guard_low`:`none`;n!==`none`&&n!==this.defense&&this.clearActions(),this.defense=n;let r=this.gesture.update(e.axes[2]??0,e.axes[3]??0,xu(e,6),xu(e,7));r!==null&&this.push(r);let i=bu.some((t,n)=>xu(e,n)&&!this.previous.has(n));for(let t of[14,15]){let n=xu(e,t),r=this.previous.has(t);this.knockdown?(n&&!r&&this.push({kind:t===14?`get_up_left`:`get_up_right`}),this.selectorConsumed.delete(t)):n?(r||this.selectorConsumed.set(t,!1),i&&this.selectorConsumed.set(t,!0)):r&&(this.selectorConsumed.get(t)===!1&&this.push({kind:t===14?`slip_left`:`slip_right`}),this.selectorConsumed.delete(t))}for(let t=0;tthis.poll())}setKnockdown(e){this.knockdown=e}setEnabled(e){this.enabled=e,e||this.reset()}frame(e=4){let t=this.sharedActions===void 0?this.queue.splice(0,e):[];return{moveX:this.moveX,moveY:this.moveY,defense:this.defense,actions:t}}reset(){this.currentId=null,this.moveX=this.moveY=0,this.defense=`none`,this.queue.length=0,this.sharedActions?.clearSource(`gamepad`),this.previous.clear(),this.selectorConsumed.clear(),this.gesture.reset()}destroy(){cancelAnimationFrame(this.raf),window.removeEventListener(`gamepaddisconnected`,this.disconnect),this.reset()}},wu=class{target;maximumQueue;sharedActions;held=new Set;queue=[];enabled=!0;keydown=e=>{if(!su.has(e.code))return;if(!this.enabled){this.held.add(e.code);return}if(e.preventDefault(),e.repeat){this.held.add(e.code);return}this.held.add(e.code),(e.code===`KeyQ`||e.code===`KeyE`)&&this.clearActions();let t=au[e.code];if(t!==void 0)this.push({kind:`punch`,hand:t[0],class:t[1],target:this.hasShift()?`body`:`head`,power:this.hasAlt()?`power`:`normal`});else{let t=ou[e.code];t!==void 0&&(t.kind!==`foul`||this.hasShift())&&this.push(t)}};keyup=e=>{su.has(e.code)&&(this.enabled&&e.preventDefault(),this.held.delete(e.code))};visibility=()=>{document.hidden&&this.reset()};blur=()=>this.reset();constructor(e=window,t=1,n){this.target=e,this.maximumQueue=t,this.sharedActions=n,e.addEventListener(`keydown`,this.keydown),e.addEventListener(`keyup`,this.keyup),e.addEventListener(`blur`,this.blur),document.addEventListener(`visibilitychange`,this.visibility)}hasShift(){return this.held.has(`ShiftLeft`)||this.held.has(`ShiftRight`)}hasAlt(){return this.held.has(`AltLeft`)||this.held.has(`AltRight`)}push(e){this.sharedActions===void 0?hu(this.queue,e,this.maximumQueue):this.sharedActions.push(`keyboard`,e)}clearActions(){this.queue.length=0,this.sharedActions?.clear()}setEnabled(e){this.enabled=e,e||(this.queue.length=0,this.sharedActions?.clearSource(`keyboard`))}frame(e=4){if(!this.enabled)return{moveX:0,moveY:0,defense:`none`,actions:[]};let t=(this.held.has(`KeyD`)?1e3:0)-(this.held.has(`KeyA`)?1e3:0),n=(this.held.has(`KeyW`)?1e3:0)-(this.held.has(`KeyS`)?1e3:0);t!==0&&n!==0&&(t=Math.sign(t)*707,n=Math.sign(n)*707);let r=this.held.has(`KeyQ`)?`guard_high`:this.held.has(`KeyE`)?`guard_low`:`none`;return{moveX:t,moveY:n,defense:r,actions:this.sharedActions===void 0?this.queue.splice(0,e):[]}}reset(){this.held.clear(),this.queue.length=0,this.sharedActions?.clearSource(`keyboard`)}destroy(){this.reset(),this.target.removeEventListener(`keydown`,this.keydown),this.target.removeEventListener(`keyup`,this.keyup),this.target.removeEventListener(`blur`,this.blur),document.removeEventListener(`visibilitychange`,this.visibility)}};function Tu(e,t,n,r){let i=e=>Math.max(-1e3,Math.min(1e3,Math.round(e)))||0;return{moveX:i(-r*e+n*t),moveY:i(-n*e-r*t)}}var Eu=class{actions=new gu(1);keyboard;gamepad;touch=null;reset=()=>{this.keyboard.reset(),this.gamepad.reset(),this.touch?.reset(),this.actions.clear(),this.gamepad.setKnockdown(!1)};constructor(){this.keyboard=new wu(window,1,this.actions),this.gamepad=new Cu(1,this.actions),window.addEventListener(`blur`,this.reset),document.addEventListener(`visibilitychange`,this.visibility)}visibility=()=>{document.hidden&&this.reset()};attachTouch(e,t=$l()){this.touch!==null||!t||(this.touch=new nu(e,this.actions))}setActive(e,t=e){this.keyboard.setEnabled(e),this.gamepad.setEnabled(e),this.touch?.setEnabled(e,t)}setKnockdown(e){this.gamepad.setKnockdown(e),this.touch?.setKnockdown(e)}onAction(e){this.actions.setListener(e)}setViewForward(e){this.viewForward=e}viewForward=null;held(){let e=this.keyboard.frame(0),t=this.gamepad.frame(0),n=this.touch?.frame()??null,r=t.moveX===0?n!==null&&n.moveX!==0?n.moveX:e.moveX:t.moveX,i=t.moveY===0?n!==null&&n.moveY!==0?n.moveY:e.moveY:t.moveY,a=t.defense===`none`?n!==null&&n.defense!==`none`?n.defense:e.defense:t.defense,o=r!==0||i!==0?this.viewForward?.()??null:null;if(o===null)return{moveX:r,moveY:i,defense:a};let s=Tu(r,i,o.x,o.z);return{moveX:s.moveX,moveY:s.moveY,defense:a}}frame(){return{...this.held(),actions:this.actions.drain(4)}}destroy(){window.removeEventListener(`blur`,this.reset),document.removeEventListener(`visibilitychange`,this.visibility),this.keyboard.destroy(),this.gamepad.destroy(),this.touch?.destroy()}},Du=(e,t,n)=>e+(t-e)*n,Ou=(e,t,n)=>({...t,x:Du(e.x,t.x,n),y:Du(e.y,t.y,n),facing:Du(e.facing,t.facing,n),facing_x:Du(e.facing_x,t.facing_x,n),facing_y:Du(e.facing_y,t.facing_y,n),velocity_x:Du(e.velocity_x,t.velocity_x,n),velocity_y:Du(e.velocity_y,t.velocity_y,n),stamina:Du(e.stamina,t.stamina,n),maximum_stamina:Du(e.maximum_stamina,t.maximum_stamina,n),conditioning:Du(e.conditioning,t.conditioning,n),guard:Du(e.guard,t.guard,n),poise:Du(e.poise,t.poise,n)}),ku=1.5,Au=6,ju=.5,Mu=2500,Nu=1,Pu=.04,Fu=100,Iu=.01,Lu=4,Ru=class{maximum;tickRate;initialDelayTicks;snapshots=[];offsetTicks=null;delayTicks;lastStarvationMs=-1/0;lastFrameMs=null;behindPushes=0;needs=[];constructor(e=8,t=30,n=2){this.maximum=e,this.tickRate=t,this.initialDelayTicks=n,this.delayTicks=n}get interpolationDelayTicks(){return this.delayTicks}push(e,t){let n=this.snapshots.at(-1);if(n!==void 0&&e.tick<=n.tick)return!1;if(this.snapshots.push(e),this.snapshots.length>this.maximum&&this.snapshots.shift(),t!==void 0&&Number.isFinite(t)){let n=e.tick-t*this.tickRate/1e3;this.offsetTicks!==null&&n=Lu&&this.restartClock(),this.offsetTicks===null?(this.offsetTicks=n,this.need(t,this.initialDelayTicks)):this.offsetTicks=Math.max(n,this.offsetTicks-Iu),this.need(t,this.offsetTicks-n+Nu)}return!0}resync(){this.restartClock()}restartClock(){this.offsetTicks=null,this.delayTicks=this.initialDelayTicks,this.lastStarvationMs=-1/0,this.behindPushes=0,this.needs.length=0}need(e,t){for(;this.needs.length>0&&this.needs.at(-1).ticks<=t;)this.needs.pop();this.needs.push({atMs:e,ticks:t})}recentNeed(e){for(;this.needs.length>0&&this.needs[0].atMs<=e-Mu;)this.needs.shift();return this.needs[0]?.ticks??0}latest(){return this.snapshots.at(-1)??null}renderTick(e){let t=this.latest();if(t===null)return 0;let n=this.snapshots[0].tick;if(this.offsetTicks===null)return Math.max(n,t.tick-1);let r=this.lastFrameMs===null?0:Math.min(Fu,Math.max(0,e-this.lastFrameMs));this.lastFrameMs=e;let i=e*this.tickRate/1e3+this.offsetTicks-this.delayTicks;if(i>t.tick+.25)e-this.lastStarvationMs>250&&(this.lastStarvationMs=e,this.delayTicks=Math.min(Au,this.delayTicks+ju),this.need(e,this.delayTicks));else{let t=Math.max(ku,this.recentNeed(e));this.delayTicks>t&&(this.delayTicks=Math.max(t,this.delayTicks-Pu*r*this.tickRate/1e3))}return Math.min(t.tick,Math.max(n,i))}sample(e){let t=this.snapshots.findIndex(t=>t.tick>=e);if(t<=0)return this.snapshots[Math.max(0,t)]??this.latest();let n=this.snapshots[t-1],r=this.snapshots[t];if(n.phase!==r.phase||n.fighters.some((e,t)=>e.knockdowns!==r.fighters[t]?.knockdowns))return r;let i=Math.max(0,Math.min(1,(e-n.tick)/Math.max(1,r.tick-n.tick)));return{...r,fighters:[Ou(n.fighters[0],r.fighters[0],i),Ou(n.fighters[1],r.fighters[1],i)]}}clear(){this.snapshots.length=0,this.offsetTicks=null}},zu=class{highest=-1;accept(e){let t=new Set,n=e.filter(e=>e.event_id<=this.highest||t.has(e.event_id)?!1:(t.add(e.event_id),!0)).sort((e,t)=>e.event_id-t.event_id);for(let e of n)this.highest=Math.max(this.highest,e.event_id);return n}reset(){this.highest=-1}};function Bu(e=window.location){let t=new URL(`/api/hands/ws`,e.origin);if(e.protocol===`https:`)t.protocol=`wss:`;else if(e.protocol===`http:`&&[`localhost`,`127.0.0.1`,`::1`].includes(e.hostname))t.protocol=`ws:`;else throw Error(`insecure_websocket_origin`);return t.search=``,t.hash=``,t.toString()}var Vu=1,Hu={moveX:0,moveY:0,defense:`none`,actions:[]},Uu=33,Wu=8,Gu=50,Ku=20,qu=2048;function Ju(e){try{return We(e)}catch(t){let n;try{let t=pe(e);n=typeof t==`object`&&t&&`version`in t?t.version:void 0}catch{}throw typeof n==`number`&&n!==4?new Bn(`client_outdated`,!0):t}}var Yu=class{getInput;callbacks;makeSocket;now;socket=null;reconnectTicket;reconnectTimer=null;opponentPauseTimer=null;inputTimer=null;reconnectDeadline=0;opponentPauseDeadline=0;opponentPaused=!1;attempts=0;nextSequence=0;playerId=null;sentAt=new Map;actionSequences=new Map;latencyMs=null;serverTick=0;role=null;active=!1;resting=!1;boutStarted=!1;disposed=!1;terminal=!1;inputSuppressed=!1;pointerFocus=!1;listenersBound=!1;lastInputSentAt=-1/0;sendTimes=[];edgeSendTimes=[];constructor(e,t,n,r=e=>new WebSocket(e),i=()=>performance.now()){this.getInput=t,this.callbacks=n,this.makeSocket=r,this.now=i,this.reconnectTicket=e}start(){this.disposed||this.listenersBound||(this.bindInputListeners(),this.connect(),!this.disposed&&!this.terminal&&(this.inputTimer=window.setInterval(()=>this.flushInput(),Uu)))}bindInputListeners(){this.listenersBound=!0,window.addEventListener(`blur`,this.onInputLoss),window.addEventListener(`focus`,this.onInputRegain),window.addEventListener(`pointerdown`,this.onPagePointer,!0),document.addEventListener(`visibilitychange`,this.onVisibilityChange)}setActive(e){this.setInputActive(e)}setInputActive(e){let t=this.active;this.active=e,e?this.boutStarted=!0:t&&this.sendInput(Hu,!0)}notifyAction(){this.now()-(this.edgeSendTimes.at(-1)??-1/0)=Ku||this.flushInput()&&this.edgeSendTimes.push(this.lastInputSentAt)}sendsInLastSecond(){return this.withinLastSecond(this.sendTimes)}withinLastSecond(e){let t=this.now()-1e3;for(;e.length>0&&e[0]<=t;)e.shift();return e.length}onInputLoss=()=>{this.inputSuppressed||(this.sendInput(Hu,this.boutStarted),this.inputSuppressed=!0,this.pointerFocus=!1)};onInputRegain=()=>{!document.hidden&&document.hasFocus()&&(this.inputSuppressed=!1)};onPagePointer=()=>{if(!document.hidden&&(this.pointerFocus=!0,this.inputSuppressed=!1,!document.hasFocus()))try{window.focus()}catch{}};onVisibilityChange=()=>{document.hidden?this.onInputLoss():this.onInputRegain()};connect(){if(this.disposed||this.terminal)return;let e=this.reconnectTicket;if(e===null){this.terminal=!0,this.stopInputLifecycle(),this.callbacks.onFreshAuth();return}let t;try{t=this.makeSocket(Bu())}catch{this.terminal=!0,this.stopInputLifecycle(),this.callbacks.onFatal(`network_unavailable`);return}this.socket=t,t.binaryType=`arraybuffer`,t.onopen=()=>{if(!(this.socket!==t||this.disposed))try{t.send(JSON.stringify({version:4,type:`authenticate`,ticket:e})),this.reconnectTicket=null}catch{this.handleClose(t)}},t.onmessage=e=>this.handleMessage(t,e),t.onerror=()=>void 0,t.onclose=()=>this.handleClose(t)}handleMessage(e,t){if(!(this.socket!==e||this.disposed||this.terminal))try{if(typeof t.data!=`string`&&!(t.data instanceof ArrayBuffer))throw Error(`unsupported_frame`);let n=Ju(t.data);if(this.applyMessage(n),n.type===`ticket`){try{e.send(JSON.stringify({version:4,type:`ticket_ack`,refresh_id:n.refresh_id}))}catch{this.handleClose(e)}return}this.callbacks.onMessage(n),n.type===`error`?this.terminate(n.code,e):n.type===`final`&&(this.terminal=!0,this.clearTransportReconnect(!0),this.clearOpponentPause(!0),this.stopInputLifecycle(),this.socket===e&&(this.socket=null),e.onopen=e.onmessage=e.onclose=e.onerror=null,e.close(1e3,`complete`))}catch(e){this.callbacks.onFatal(Kn(e)),this.dispose()}}applyMessage(e){if(e.type===`welcome`){if(e.reconnect_ticket===void 0)throw Error(`missing_reconnect_ticket`);this.reconnectTicket=e.reconnect_ticket,this.serverTick=e.server_tick;let t=this.role===`spectator`&&e.role===`fighter`;this.role=e.role,this.playerId=e.role===`fighter`?e.player_id:null,this.sentAt.clear(),e.role===`fighter`?this.nextSequence=Math.max(this.nextSequence,e.next_sequence):this.stopInputLifecycle(),t&&(this.listenersBound||this.bindInputListeners(),this.inputTimer??=window.setInterval(()=>this.flushInput(),Uu)),this.attempts=0,this.clearTransportReconnect(!0)}else e.type===`ticket`?this.reconnectTicket=e.reconnect_ticket:e.type===`snapshot`?(this.serverTick=Math.max(this.serverTick,e.payload.tick),this.setInputActive([`countdown`,`fight`,`knockdown`,`foul_recovery`].includes(e.payload.phase)),this.resting=e.payload.phase===`rest`,this.observeAcknowledgement(e.payload)):e.type===`paused`?(this.setInputActive(!1),this.startOpponentPause(e.grace_ms)):e.type===`resumed`?(this.setInputActive(!0),this.attempts=0,this.clearOpponentPause(!0)):e.type===`select`?this.clearOpponentPause(!0):e.type===`ready`?(this.clearOpponentPause(!0),this.setInputActive(!0)):e.type===`waiting`?(this.clearOpponentPause(!0),this.setInputActive(!1)):(e.type===`final`||e.type===`error`)&&(this.active=!1,this.clearOpponentPause(!0))}terminate(e,t){this.terminal=!0,this.active=!1,this.clearTransportReconnect(!0),this.clearOpponentPause(!0),this.stopInputLifecycle(),this.callbacks.onFatal(e),this.socket===t&&(this.socket=null,t.onopen=t.onmessage=t.onclose=t.onerror=null,t.close(1e3,`terminal`))}handleClose(e){if(this.socket!==e||(this.socket=null,this.active=!1,this.disposed||this.terminal))return;if(this.reconnectTicket===null){this.terminal=!0,this.stopInputLifecycle(),this.callbacks.onFreshAuth();return}this.clearOpponentPause(!1),this.reconnectDeadline===0&&(this.reconnectDeadline=this.now()+2e4);let t=Math.max(0,this.reconnectDeadline-this.now());if(this.callbacks.onReconnect(t),t<=0){this.terminal=!0,this.stopInputLifecycle(),this.callbacks.onFreshAuth();return}let n=Math.min(3e3,250*2**Math.min(this.attempts,4),t);this.attempts+=1,this.reconnectTimer=window.setTimeout(()=>{this.reconnectTimer=null,this.callbacks.onReconnect(Math.max(0,this.reconnectDeadline-this.now())),this.connect()},n)}requestCpu(e){let t=this.socket;if(this.role!==`fighter`||this.disposed||this.terminal||t?.readyState!==Vu)return!1;try{return t.send(Xe(e)),!0}catch{return this.handleClose(t),!1}}chooseStyle(e,t){let n=this.socket;if(this.role!==`fighter`||this.disposed||this.terminal||n?.readyState!==Vu)return!1;try{return n.send(Je(e,t)),!0}catch{return this.handleClose(n),!1}}get inputLatencyMs(){return this.latencyMs}sequenceOf(e){return this.actionSequences.get(e)??null}observeAcknowledgement(e){if(this.playerId===null)return;let t=e.fighters.find(e=>e.player_id===this.playerId);if(t===void 0||t.last_input_sequence<0)return;let n=this.sentAt.get(t.last_input_sequence);if(n!==void 0){let e=Math.max(0,this.now()-n);this.latencyMs=this.latencyMs===null?e:this.latencyMs*.8+e*.2}for(let e of this.sentAt.keys())e<=t.last_input_sequence&&this.sentAt.delete(e)}sendInput(e,t=!1){return(this.active||t)&&this.transmit(e)}sendCornerChoice(e){return this.resting?this.transmit({moveX:0,moveY:0,defense:`none`,actions:[{kind:e}]}):!1}transmit(e){let t=this.socket;if(this.role!==`fighter`||this.disposed||this.terminal||t?.readyState!==Vu)return!1;try{let n=e.actions.slice(0,4);t.send(qe(this.nextSequence,this.serverTick,{...e,actions:n})),this.sentAt.set(this.nextSequence,this.now()),this.sentAt.size>128&&this.sentAt.delete(this.sentAt.keys().next().value);for(let e of n)e.id!==void 0&&this.actionSequences.set(e.id,this.nextSequence);for(;this.actionSequences.size>64;)this.actionSequences.delete(this.actionSequences.keys().next().value);return this.nextSequence+=1,this.lastInputSentAt=this.now(),this.sendTimes.push(this.lastInputSentAt),!0}catch{return this.handleClose(t),!1}}flushInput(){return this.inputSuppressed||document.hidden||!(this.pointerFocus||document.hasFocus())||this.sendsInLastSecond()>=Gu||(this.socket?.bufferedAmount??0)>qu?!1:this.sendInput(this.getInput())}startOpponentPause(e){this.clearOpponentPause(!1),this.opponentPaused=!0,this.opponentPauseDeadline=this.now()+e,this.callbacks.onReconnect(e),e>0&&this.scheduleOpponentPauseTick()}scheduleOpponentPauseTick(){let e=Math.max(0,this.opponentPauseDeadline-this.now());if(e<=0){this.opponentPauseDeadline=0,this.callbacks.onReconnect(0);return}this.opponentPauseTimer=window.setTimeout(()=>{this.opponentPauseTimer=null;let e=Math.max(0,this.opponentPauseDeadline-this.now());this.callbacks.onReconnect(e),e>0?this.scheduleOpponentPauseTick():this.opponentPauseDeadline=0},Math.min(250,e))}clearOpponentPause(e){let t=this.opponentPaused;this.opponentPaused=!1,this.opponentPauseDeadline=0,this.opponentPauseTimer!==null&&(clearTimeout(this.opponentPauseTimer),this.opponentPauseTimer=null),e&&t&&this.callbacks.onReconnect(0)}cancelReconnect(){this.reconnectTimer!==null&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null)}clearTransportReconnect(e){let t=this.reconnectDeadline!==0;this.reconnectDeadline=0,this.cancelReconnect(),e&&t&&this.callbacks.onReconnect(0)}stopInputLifecycle(){this.inputTimer!==null&&(clearInterval(this.inputTimer),this.inputTimer=null),this.listenersBound&&=(window.removeEventListener(`blur`,this.onInputLoss),window.removeEventListener(`focus`,this.onInputRegain),window.removeEventListener(`pointerdown`,this.onPagePointer,!0),document.removeEventListener(`visibilitychange`,this.onVisibilityChange),!1)}dispose(){if(this.disposed)return;this.disposed=!0,this.active=!1,this.reconnectTicket=null,this.clearTransportReconnect(!0),this.clearOpponentPause(!0),this.stopInputLifecycle();let e=this.socket;this.socket=null,e!==null&&(e.onopen=e.onmessage=e.onclose=e.onerror=null,e.close(1e3,`teardown`))}},Xu={play:`CALLAHAN`,colour:`OKAFOR`,announcer:`RING ANNOUNCER`},Zu=.3,Qu=.9,$u=.25,ed=2.6,td=4.5,nd=.8,rd=6,id=9,ad=6,od=42,sd=8,cd=15,ld=10,ud=[60,Math.round(k*.6),Math.round(k*.8)],dd={cut:Math.round(k*.7),swelling:Math.round(ne*.75)},fd=500,pd=.2,md=.3,hd=120,gd={knockdown:[`Down goes {b}!`,`{a} drops {b}!`,`{b} is down! What a shot from {a}!`,`Down goes {b}! {a} found the chin!`],knockdownAgain:[`{b} is down again!`,`Down for the second time! {b} is in deep trouble!`,`{a} puts {b} down again!`],knockdownCounter:[`Down goes {b}, off a perfect counter!`,`{a} times it, and {b} is down!`],struggle:[`{b} is struggling to get up...`,`{b} is trying to beat the count...`,`Can {b} get up?`],upLate:[`{b} beats the count at {n}!`,`Up at {n}! {b} is still in this!`,`{b} makes it up at {n}, but the legs look heavy.`],upEarly:[`{b} is up at {n}.`,`{b} is back up at the count of {n}.`,`Up at {n}. {b} wants to keep fighting.`],finishCall:[`{a} has to go for the finish now.`,`{b} needs to survive the next minute.`,`{b} has to hold and clear the head.`],hurt:[`{b} is hurt!`,`{a} has {b} in trouble!`,`{b}'s legs just buckled!`,`Big shot! {b} is wobbled!`,`{b} is rocked! Keep the pressure on, {a}!`],hurtBadly:[`{b} is out on their feet!`,`{b} is in all sorts of trouble!`,`{b} is badly hurt!`],guardBreak:[`{a} blasts right through the guard!`,`The guard is gone! {b} can't keep those hands up!`,`{a} breaks the guard down!`],counter:[`What a counter from {a}!`,`{a} times it perfectly! A {punch} on the counter!`,`{b} walked right into that {punch}!`,`Counter {punch} from {a}! Beautiful timing.`],bigShot:[`Big {punch} from {a}!`,`{a} lands a thudding {punch}!`,`That {punch} landed flush!`,`Huge {punch}! {b} felt that one.`],cut:[`{b} is cut over the {side} eye.`,`There's blood now. A cut over {b}'s {side} eye.`,`That's opened a cut over the {side} eye of {b}.`],cutWorse:[`That cut over {b}'s {side} eye is getting worse.`,`The blood is pouring from that {side} eye now.`,`{b} is bleeding badly from the {side} eye.`],cutDoctor:[`The doctor will be watching that cut very closely.`,`If that cut gets any worse, the doctor could stop this.`],eyeSwelling:[`{b}'s {side} eye is swelling badly.`,`{b} is having trouble seeing out of that {side} eye.`],eyeShut:[`{b}'s {side} eye has closed completely!`,`That {side} eye is shut! {b} can't see them coming from that side.`,`{b} is fighting with one eye now.`],blindSide:[`{a} is working the blind side.`,`{b} never saw that one. Nothing is getting through that {side} eye.`,`Smart from {a}, punching where {b} can't see.`],parried:[`{a} picks it off and {b} is wide open!`,`Beautiful parry from {a}! {b} is off balance.`,`{a} slaps it away and {b} is left stranded.`],bodyCollapse:[`Body shot! {b} is going down!`,`{a} digs to the body and {b} is folding!`,`Oh, that body shot! {b} is in agony!`],knockdownBody:[`Down goes {b} from the body shot!`,`{b} takes a knee! That body shot took everything out!`,`{a} goes downstairs and {b} can't take it!`],cornerCut:[`{a}'s cutman is working on that cut.`,`They're working hard on the cut in {a}'s corner.`],cornerSwelling:[`{a}'s corner is getting the swelling down.`,`Ice and the enswell on that eye in {a}'s corner.`],cornerBreath:[`{a}'s corner wants the breath back first. Big drink of water.`,`{a} is told to breathe and settle down.`],combo:[`A three-punch combination from {a}!`,`{a} puts it together beautifully.`,`Lovely combination from {a}!`],unanswered:[`Five unanswered punches from {a}!`,`{a} is unloading! {b} has to fire back!`,`{b} is just covering up as {a} lets the hands go!`],jab:[`{a}'s jab is finding a home.`,`Stiff jab from {a}, and another.`,`{a} keeps popping that jab in {b}'s face.`],body:[`{a} is going to work on the body.`,`{a} keeps digging to the body.`,`Another one downstairs from {a}.`],payoff:[`The body work is paying off. {b} is slowing down.`,`Those body shots are taking it out of {b}.`,`{a} has invested in the body, and {b} is paying for it now.`],tiring:[`{b} is running on empty.`,`{b} is breathing hard. Those punches have nothing on them.`,`{b} has punched out. The tank is empty.`],takeover:[`{a} is taking over this round.`,`This is all {a} right now.`,`{a} is in complete control.`],clinch:[`{a} ties {b} up.`,`{a} grabs hold.`,`They're tied up in the clinch.`],holdingOn:[`{a} is hurt and holding on!`,`{a} grabs on for dear life!`,`Smart from {a}, holding on to clear the head.`],refBreak:[`The referee breaks them.`,`The referee steps in to separate them.`,`Break! The referee pulls them apart.`],taunt:[`{a} drops the hands and invites {b} in!`,`Showboating from {a}! The crowd loves it.`,`{a} is taunting {b} now!`,`{a} sticks the chin out. Come and get it!`],lowBlow:[`Low blow from {a}! The referee steps in.`,`That's below the belt! {b} is doubled over.`,`{a} goes low, and the referee calls time.`],headbutt:[`Headbutt from {a}! The referee calls time.`,`{a} leads with the head! That's a foul.`],warning:[`A warning for {a}. One more and it costs a point.`,`The referee warns {a}. That can't happen again.`],deduction:[`The referee takes a point from {a}!`,`That costs {a} a point!`,`Point deduction! {a} has been warned enough.`],resume:[`And we're back underway.`,`Time in. Let's box.`],box:[`The referee waves them on. {b} has to survive now.`,`Box! {a} will be looking to finish it.`,`{b} has convinced the referee. Here comes {a}.`],evade:[`{a} slips it! Lovely head movement.`,`{b} misses, and {a} was never there.`,`Great defence from {a}.`],parry:[`{a} picks that off with the gloves.`,`{a} parries it away.`,`Tight defence from {a}.`],whiff:[`{a} misses wildly with the {punch}.`,`Big swing and a miss from {a}.`,`{a} throws everything into that {punch} and hits nothing but air.`],southpaw:[`{a} switches to southpaw.`,`{a} turns southpaw now.`],orthodox:[`{a} goes back to orthodox.`,`{a} switches back to orthodox.`],roundStart:[`Round {r}. Here we go.`,`We're underway in round {r}.`,`Round {r} is on.`],roundEnd:[`That's the bell to end round {r}.`,`The bell ends round {r}.`,`And that's round {r} in the books.`],savedByBell:[`Saved by the bell! {b} was in real trouble.`,`{b} is saved by the bell!`,`The bell rescues {b}!`],finalBell:[`That's the final bell!`,`The final bell! We go to the scorecards.`],summary:[`Round {r}: {a} landed {x} of {y}, {b} {z} of {w}.`],roundFor:[`I had that round for {a}.`,`{a} took that round.`,`That was {a}'s round.`],roundBig:[`The knockdown makes that a big round for {a}.`,`{a} wins that round big with the knockdown.`],roundClose:[`Close round. That one could go either way.`,`Tough round to score.`,`Nothing between them in that round.`],pullingAway:[`{a} is pulling away on the cards.`,`{b} needs something big. {a} is building a lead.`],doctor:[`The ringside doctor is taking a close look at {b}.`,`The doctor is in {b}'s corner checking that cut.`],ko:[`It's all over! {a} wins by knockout!`,`{b} can't beat the count! {a} wins it!`,`That's it! {a} has knocked out {b}!`],flashKo:[`Lights out! {b} is out cold!`,`One punch, and it's over! {a} wins!`,`Good night! {a} ends it with one shot!`],tko:[`Three knockdowns, and it's over! {a} wins!`,`The referee waves it off! {a} wins by stoppage!`],doctorStop:[`The doctor has seen enough! It's stopped on the cut!`,`The doctor stops it! {b} can't continue!`],disqualified:[`{b} is disqualified!`,`That's it! {b} has been thrown out!`],forfeit:[`{b} has left the ring. {a} wins by forfeit.`],replay:[`Here it is again.`,`Let's see that one more time.`,`Watch {a} set it up...`]},_d={ko:`ko`,flash_ko:`flashKo`,tko:`tko`,doctor_stoppage:`doctorStop`,disqualification:`disqualified`,forfeit:`forfeit`},vd={struggle:new Set([`knockdown`])},yd=`struggle`,bd=new Set([`countdown`]),xd=3.2,Sd=new Set([`knockdown`,`knockdownAgain`,`knockdownCounter`,`knockdownBody`,`bodyCollapse`,`upLate`,`upEarly`,`hurt`,`hurtBadly`,`lowBlow`,`headbutt`,`deduction`,`ko`,`flashKo`,`tko`,`doctorStop`,`disqualified`,`forfeit`,`replay`,`savedByBell`,`finalBell`,`roundEnd`,`roundStart`]),Cd={ko:98,flashKo:98,tko:98,doctorStop:98,disqualified:98,forfeit:98,knockdown:95,knockdownAgain:95,knockdownCounter:95,knockdownBody:95,bodyCollapse:86,upLate:90,upEarly:90,replay:88,deduction:84,lowBlow:82,headbutt:82,hurt:80,box:78,hurtBadly:80,holdingOn:76,savedByBell:74,finalBell:72,struggle:70,guardBreak:65,eyeShut:62,counter:60,parried:58,bigShot:55,roundEnd:52,cut:50,cutWorse:50,summary:50,cutDoctor:48,doctor:48,eyeSwelling:48,cornerCut:47,cornerSwelling:47,cornerBreath:47,blindSide:42,roundFor:46,roundBig:46,roundClose:46,pullingAway:46,unanswered:45,finishCall:44,warning:42,combo:40,payoff:40,takeover:38,body:35,jab:30,tiring:30,roundStart:28,taunt:25,parry:22,evade:20,clinch:20,whiff:18,refBreak:12,southpaw:10,orthodox:10,resume:10},wd={finishCall:`colour`,payoff:`colour`,tiring:`colour`,takeover:`colour`,holdingOn:`colour`,taunt:`colour`,parry:`colour`,evade:`colour`,warning:`colour`,blindSide:`colour`,cornerCut:`colour`,cornerSwelling:`colour`,cornerBreath:`colour`,roundFor:`colour`,roundBig:`colour`,roundClose:`colour`,pullingAway:`colour`,doctor:`colour`,cutDoctor:`colour`,jab:`colour`},Td={combo:6,jab:20,body:18,evade:12,parry:15,parried:8,blindSide:20,whiff:12,clinch:15,refBreak:20,taunt:10,southpaw:15,orthodox:15,bigShot:4,counter:3,unanswered:15,takeover:25},Ed=[`zero`,`one`,`two`,`three`,`four`,`five`,`six`,`seven`,`eight`,`nine`,`ten`,`eleven`,`twelve`,`thirteen`,`fourteen`,`fifteen`],Dd=e=>Ed[e]??String(e),Od=[...Ed,`sixteen`,`seventeen`,`eighteen`,`nineteen`],kd=[``,``,`twenty`,`thirty`,`forty`,`fifty`,`sixty`,`seventy`,`eighty`,`ninety`];function Ad(e){return!Number.isInteger(e)||e<0||e>=1e3?String(e):e<20?Od[e]:e<100?`${kd[Math.floor(e/10)]}${e%10==0?``:`-${Od[e%10]}`}`:`${Od[Math.floor(e/100)]} hundred${e%100==0?``:` and ${Ad(e%100)}`}`}function jd(e,t=!0){if(le(e))return`making a pro debut`;let n=e.draws>0?`${Ad(e.wins)}, ${Ad(e.losses)} and ${Ad(e.draws)}`:`${Ad(e.wins)} and ${e.losses===0?`oh`:Ad(e.losses)}`;return t&&e.knockouts>0?`${n}, ${Ad(e.knockouts)} by knockout`:n}function Md(e){return .85+e.length*.077+(e.split(`...`).length-1)*.35}var Nd=.3;function Pd(e,t){let n=t-Nd,r=[e=>[e.record===void 0?null:jd(e.record),rt(e.style)],e=>[rt(e.style)],()=>[]];for(let t of r){let r=e.map((e,n)=>{let r=n===0?`In the blue corner`:`And in the red corner`,i=t(e).filter(e=>e!==null);return i.length===0?`${r}, ${e.name}!`:`${r}, ${i.join(`, `)}... ${e.name}!`});if(r.reduce((e,t)=>e+Md(t),0)<=n)return r}let i=`${e[0].name}... versus ${e[1].name}!`;return Md(i)<=n?[i]:[]}function Fd(e,t){let n=e.replace(/\{(a|b|n|side|punch|r|x|y|z|w)\}/gu,(e,n)=>String(t[n]??``));return n.charAt(0).toUpperCase()+n.slice(1)}var Id=(e,t)=>{let n=Math.imul((e|0)^Math.imul(t,2654435761),2246822507);return n^=n>>>13,n=Math.imul(n,3266489909),(n^n>>>16)>>>0};function Ld(e){let t=e.split(`:`);return[t.at(-2),t.at(-1)]}function Rd(e,t){let[n=`punch`,r=`head`]=Ld(e),i=t!==void 0&&t.action===n?t.action_hand:null,a=n===`jab`?`jab`:n===`straight`?i===null?`straight`:`${i} hand`:i===null?n:`${i} ${n}`;return r===`body`?`${a} to the body`:a}function zd(e){if(e.phase===`knockdown`){let t=Math.max(...e.fighters.map(e=>e.get_up_count));return Math.min(1,.3+t*.07)}if(e.phase!==`fight`)return 0;let t=0;for(let[n,r]of e.fighters.entries()){let i=e.fighters[1-n];r.is_downed||(r.stunned_ticks>0?t=Math.max(t,.75):r.poise<150&&(t=Math.max(t,.45)),(Math.max(Math.abs(r.x),Math.abs(r.y))>400||Math.abs(r.x)+Math.abs(r.y)>640)&&Math.hypot(r.x-i.x,r.y-i.y)<150&&(t=Math.max(t,.4)))}return t}var Bd=()=>({thrown:0,landed:0,damage:0,knockdowns:0,body:0}),Vd=()=>({jabs:[],window:[],evasions:[],run:0,runTick:-1e3,unanswered:0,stunnedAt:-1e3,knockedDownAt:-1e5,lowSince:null,tiredRound:0,takeoverRound:0,payoffRound:0,cutMarks:[0,0],eyeShut:[!1,!1],eyeSwelling:[!1,!1],rounds:0}),Hd=class{hooks;queue=[];showing=null;lastEnd=-1/0;lastChoice=new Map;cooldowns=new Map;notes=new Map;tallies=new Map;players={};fighters=null;tickRate=30;introduced=!1;lastPhase=null;round=0;roundLength=null;fightRemaining=0;struggleCalled=-1;lastKnockdown=null;final=null;verdictGiven=!1;resultCalled=!1;caption={line:null,opacity:0,entering:0};constructor(e={}){this.hooks=e}observe(e,t,n,r,i){this.players=n,this.fighters=e.fighters,this.tickRate=r,this.trackClock(e),this.introduced||(this.introduced=!0,e.phase===`countdown`&&e.round_number===1&&this.introduce(e,i));let a=t.find(e=>e.kind===`result`)??null;for(let n of t)this.consider(n,e,a,i);this.inspect(e,i),this.lastPhase=e.phase,this.dropOutOfPhase(e.phase,i)}dropOutOfPhase(e,t){for(let t=this.queue.length-1;t>=0;--t){let n=this.queue[t].phases;n!==null&&!n.has(e)&&this.queue.splice(t,1)}let n=this.showing;n!==null&&n.phases!==null&&!n.phases.has(e)&&(n.end=Math.min(n.end,t+$u))}finish(e,t,n,r){this.players=t,this.final=e,n&&(this.enqueue(this.announcement(`Ladies and gentlemen, we go to the scorecards.`,{kicker:`LADIES AND GENTLEMEN`,title:`WE GO TO THE SCORECARDS`,detail:``,corner:null},ad,99),r,null,30),this.hooks.speak?.([`Ladies and gentlemen, we go to the scorecards.`])),e.method===`forfeit`&&!this.resultCalled&&e.winner_id!==null&&(this.resultCalled=!0,this.say(`forfeit`,e.round,r,{a:this.nameOf(e.winner_id),b:this.nameOf(this.otherId(e.winner_id))},`result`,0))}verdict(e){let t=this.final;if(t===null||this.verdictGiven)return;this.verdictGiven=!0;let n=nn(t);if(t.winner_id===null){let r=`After ${Dd(t.round)} rounds, this bout is scored a ${n.toLowerCase()}.`;this.enqueue(this.announcement(r,{kicker:`AFTER ${Dd(t.round).toUpperCase()} ROUNDS`,title:n,detail:``,corner:null},id,100),e,`verdict`,30),this.hooks.speak?.([r]);return}let r=this.nameOf(t.winner_id),i=n.toLowerCase();this.enqueue(this.announcement(`The winner, by ${i}: ${r}!`,{kicker:`THE WINNER, BY ${n}`,title:r.toUpperCase(),detail:``,corner:this.seatOf(t.winner_id)},id,100),e,`verdict`,30),this.hooks.speak?.([`And the winner, by ${i}... ${r}!`])}resultShown(e){let t=this.final;if(t===null||this.verdictGiven)return;if(t.method===`decision`||t.method===`draw`){this.verdict(e);return}if(this.verdictGiven=!0,t.winner_id===null)return;let n=this.nameOf(t.winner_id),r={ko:`knockout`,flash_ko:`knockout`,tko:`technical knockout`,doctor_stoppage:`doctor stoppage`,disqualification:`disqualification`,forfeit:`forfeit`}[t.method]??t.method.replaceAll(`_`,` `),i=this.stoppageClock(t.round),a=t.method===`ko`||t.method===`flash_ko`||t.method===`tko`||t.method===`doctor_stoppage`,o=a&&i!==null?`STOPPED AT ${i} OF ROUND ${t.round}`:`ROUND ${t.round}`,s=a&&i!==null?`${n} wins by ${r} at ${i} of round ${t.round}.`:`${n} wins by ${r} in round ${t.round}.`;this.enqueue(this.announcement(s,{kicker:o,title:n.toUpperCase(),detail:`WINS BY ${r.toUpperCase()}`,corner:this.seatOf(t.winner_id)},id,100),e,`verdict`,30);let c=a&&i!==null?`The referee stops the contest at ${i} of round ${Dd(t.round)}. `:``;this.hooks.speak?.([`${c}Your winner, by ${r}... ${n}!`])}replay(e){let t=this.lastKnockdown;this.say(`replay`,t?.tick??0,e,{a:this.nameOf(t?.actor??null),b:this.nameOf(t?.target??null)},`replay`,.6,6)}current(e){for(let t=this.queue.length-1;t>=0;--t)this.queue[t].expires<=e&&this.queue.splice(t,1);this.showing!==null&&e>=this.showing.end&&(this.lastEnd=this.showing.end,this.showing=null);let t=null;for(let n of this.queue)n.at>e||(t===null||n.line.priority>t.line.priority||n.line.priority===t.line.priority&&n.at=this.lastEnd+Qu:t.line.urgent&&t.line.priority>n.line.priority&&e-n.start>=nd)&&(this.queue.splice(this.queue.indexOf(t),1),this.showing={line:t.line,start:e,end:e+t.line.hold,moment:t.moment,phases:t.phases},t.spoken!==null&&this.hooks.speak?.([t.spoken]))}let n=this.showing;return n===null?null:(this.caption.line=n.line,this.caption.opacity=Math.max(0,Math.min(1,(e-n.start)/$u,(n.end-e)/$u)),this.caption.entering=Math.max(0,1-(e-n.start)/$u),this.caption)}trackClock(e){e.phase===`fight`&&((this.lastPhase===`countdown`||this.lastPhase===`rest`)&&(this.roundLength={round:e.round_number,ticks:e.phase_ticks_remaining+1}),this.fightRemaining=e.phase_ticks_remaining),e.round_number!==this.round&&(this.round=e.round_number,this.tallies.clear())}stoppageClock(e){let t=this.roundLength;if(t===null||t.round!==e)return null;let n=Math.max(0,Math.floor((t.ticks-this.fightRemaining)/this.tickRate));return`${Math.floor(n/60)}:${String(n%60).padStart(2,`0`)}`}introduce(e,t){let n=e.phase_ticks_remaining/this.tickRate;if(n<1.2)return;let r=e.fighters.map(e=>this.nameOf(e.player_id)),i=e.fighters.map((e,t)=>({name:r[t],record:this.players[e.player_id]?.record,style:e.style})),a=Pd([i[0],i[1]],n),o=Math.max(1.4,a.length===2?Md(a[0]):n/2),s=[o,Math.max(1.4,n-o)];for(let n of[0,1]){let i=n===0?`blue`:`red`,c=this.players[e.fighters[n].player_id],l=nt(e.fighters[n].style),u=c===void 0?``:`${l===null?``:`${l} · `}${c.record===void 0?``:`${ue(c.record)} · `}${c.cpu===!0?`COMPUTER`:`RATED ${c.rating}`}`;this.enqueue(this.announcement(`In the ${i} corner, ${r[n]}.`,{kicker:`IN THE ${i.toUpperCase()} CORNER`,title:r[n].toUpperCase(),detail:u,corner:n},s[n],97),t+n*o,null,30,bd,a[n]??null)}}consider(e,t,n,r){let i=e.actor_id,a=e.target_id,o=`${e.tick}:${a??i??``}`,s={a:this.nameOf(i),b:this.nameOf(a)};switch(e.kind){case`punch_start`:i!==null&&(this.tally(i).thrown+=1);return;case`hit`:case`counter_hit`:i!==null&&a!==null&&!en(e,t.events)&&this.landed(e,i,a,t,r);return;case`stun`:{if(a===null)return;this.notesFor(a).stunnedAt=e.tick;let t=this.fighter(a),n=t!==void 0&&(t.poise=S-6);this.say(n?`hurtBadly`:`hurt`,e.event_id,r,s,o);return}case`guard_break`:this.say(`guardBreak`,e.event_id,r,s,o);return;case`knockdown`:{if(a===null||(this.lastKnockdown={actor:i,target:a,tick:e.tick},this.notesFor(a).knockedDownAt=e.tick,i!==null&&(this.tally(i).knockdowns+=1),n!==null))return;let c=t.events.some(t=>t.kind===`counter_hit`&&t.tick===e.tick&&t.target_id===a);this.say(e.amount>=2?`knockdownAgain`:e.detail===`body`?`knockdownBody`:c?`knockdownCounter`:`knockdown`,e.event_id,r,s,o);return}case`body_collapse`:this.say(`bodyCollapse`,e.event_id,r,s,o);return;case`parry`:this.say(`parried`,e.event_id,r,s,o,Zu,void 0,i);return;case`eye_shut`:if(a===null||e.detail!==`left`&&e.detail!==`right`)return;this.notesFor(a).eyeShut[e.detail===`left`?0:1]=!0,this.say(`eyeShut`,e.event_id,r,{...s,side:e.detail},o);return;case`blind_side`:if(e.detail!==`left`&&e.detail!==`right`)return;this.say(`blindSide`,e.event_id,r,{...s,side:e.detail},null,Zu,void 0,i);return;case`corner`:{let t=e.detail===`cut`?`cornerCut`:e.detail===`swelling`?`cornerSwelling`:e.detail===`breath`?`cornerBreath`:null;t!==null&&this.say(t,e.event_id,r,s,null,Zu,void 0,i);return}case`count`:e.amount>=6&&this.struggleCalled!==this.lastKnockdown?.tick&&this.fighter(a)?.is_downed===!0&&(this.struggleCalled=this.lastKnockdown?.tick??e.tick,this.say(`struggle`,e.event_id,r,{a:this.nameOf(this.otherId(a)),b:this.nameOf(a)},yd));return;case`get_up`:{for(let e=this.queue.length-1;e>=0;--e)this.queue[e].moment===yd&&this.queue.splice(e,1);this.showing?.moment===yd&&(this.showing.end=Math.min(this.showing.end,r+$u));let t={a:this.nameOf(this.otherId(i)),b:this.nameOf(i),n:Dd(e.amount)};this.say(e.amount>=8?`upLate`:`upEarly`,e.event_id,r,t,o),this.say(`finishCall`,e.event_id,r,t,null,3.4,4);return}case`perfect_block`:this.say(`parry`,e.event_id,r,s,o,Zu,void 0,i);return;case`evade`:{if(i===null)return;let t=this.notesFor(i);t.evasions=t.evasions.filter(t=>t>e.tick-ld*this.tickRate),t.evasions.push(e.tick),(this.fighter(a)?.action_power===`power`||t.evasions.length>=3)&&this.say(`evade`,e.event_id,r,s,o,Zu,void 0,i)&&(t.evasions.length=0);return}case`whiff`:{let t=this.fighter(i);if(t?.action_power!==`power`||t.action===null)return;this.say(`whiff`,e.event_id,r,{...s,punch:Rd(`${t.action}:${t.action_target??`head`}`,t)},o,Zu,void 0,i);return}case`clinch`:{if(i===null)return;let t=this.fighter(i),n=e.tick-this.notesFor(i).stunnedAt<=3*this.tickRate||t!==void 0&&t.poise<150;this.say(n?`holdingOn`:`clinch`,e.event_id,r,s,o,Zu,void 0,i);return}case`referee_break`:this.say(`refBreak`,e.event_id,r,s,null,Zu,void 0,`referee`);return;case`taunt`:this.say(`taunt`,e.event_id,r,{a:s.a,b:this.nameOf(this.otherId(i))},o,Zu,void 0,i);return;case`foul`:this.say(e.detail===`headbutt`?`headbutt`:`lowBlow`,e.event_id,r,s,o),this.fighter(i)?.warnings===1&&this.say(`warning`,e.event_id,r,s,null,3.6,4);return;case`point_deduction`:this.say(`deduction`,e.event_id,r,s,`deduction:${e.tick}`);return;case`resume`:this.say(`resume`,e.event_id,r,s,null);return;case`box`:this.say(`box`,e.event_id,r,{a:this.nameOf(this.otherId(a)),b:this.nameOf(a)},o);return;case`stance`:this.say(e.detail===`southpaw`?`southpaw`:`orthodox`,e.event_id,r,s,null,Zu,void 0,i);return;case`exhausted`:i!==null&&this.tired(i,e.event_id,r);return;case`bell`:e.detail===`round_start`?(this.tallies.clear(),t.round_number>=2&&this.say(`roundStart`,e.event_id,r,{r:t.round_number},null,Zu,xd)):e.detail===`round_end`&&this.roundEnded(e,t,n,r);return;case`result`:this.resulted(e,r);return;default:return}}landed(e,t,n,r,i){let[a,o]=Ld(e.detail),s=this.tally(t);s.landed+=1,s.damage+=e.amount,o===`body`&&(s.body+=1);let c=this.notesFor(t),l=this.notesFor(n);l.run=0,l.unanswered=0,c.run=e.tick-c.runTick<=od?c.run+1:1,c.runTick=e.tick,c.unanswered+=1,c.window=c.window.filter(t=>t>e.tick-cd*this.tickRate),c.window.push(e.tick);let u=`${e.tick}:${n}`,d=r.fighters.find(e=>e.player_id===t),f={a:this.nameOf(t),b:this.nameOf(n),punch:Rd(e.detail,d)},p=d?.action_power===`power`&&d.action===a;if(e.kind===`counter_hit`&&e.amount>=40?this.say(`counter`,e.event_id,i,f,u,Zu,void 0,t):(e.amount>=85||p&&o===`head`&&e.amount>=60)&&this.say(`bigShot`,e.event_id,i,f,u,Zu,void 0,t),c.unanswered===5?this.say(`unanswered`,e.event_id,i,f,u,Zu,void 0,t):c.run===3&&this.say(`combo`,e.event_id,i,f,u,Zu,void 0,t),a===`jab`&&(c.jabs=c.jabs.filter(t=>t>e.tick-sd*this.tickRate),c.jabs.push(e.tick),c.jabs.length>=3&&this.say(`jab`,e.event_id,i,f,null,Zu,void 0,t)&&(c.jabs.length=0)),o===`body`){let a=this.fighter(n);a!==void 0&&s.body>=5&&a.staminat>e.tick-cd*this.tickRate).length;c.window.length>=7&&m<=2&&c.takeoverRound!==r.round_number&&(c.takeoverRound=r.round_number,this.say(`takeover`,e.event_id,i,f,null,Zu,void 0,t)&&this.hooks.cue?.(`chant`))}roundEnded(e,t,n,r){let i=t.round_number;if(n!==null){(n.detail===`decision`||n.detail===`draw`)&&this.say(`finalBell`,e.event_id,r,{},`bell`,Zu,xd);return}let a=t.fighters.find(t=>{let n=this.notesFor(t.player_id);return t.stunned_ticks>0||t.poise({id:e.player_id,tally:this.tally(e.player_id)})),[c,l]=o.tally.landed>=s.tally.landed?[o,s]:[s,o];c.tally.thrown+l.tally.thrown>0&&this.sayAt(`summary`,e.event_id,r+2.4,{r:i,a:this.nameOf(c.id),x:c.tally.landed,y:c.tally.thrown,b:this.nameOf(l.id),z:l.tally.landed,w:l.tally.thrown},5);let u=(o.tally.knockdowns-s.tally.knockdowns)*1e3+(o.tally.damage-s.tally.damage)+6*(o.tally.landed-s.tally.landed),d=Math.abs(u)0?o:s;d!==null&&(this.notesFor(d.id).rounds+=1);let f=d===null?null:d===o?s:o,p=d===null||f===null?0:this.notesFor(d.id).rounds-this.notesFor(f.id).rounds,m=d===null?`roundClose`:o.tally.knockdowns===s.tally.knockdowns?i>=2&&p>=2?`pullingAway`:`roundFor`:`roundBig`;this.sayAt(m,e.event_id,r+6.4,{a:this.nameOf(d?.id??null),b:this.nameOf(f?.id??null)},5);let h=t.fighters.find(e=>Math.max(e.trauma.left_cut,e.trauma.right_cut)>=dd.cut||e.trauma.swelling>=dd.swelling);h!==void 0&&this.sayAt(`doctor`,e.event_id,r+10,{b:this.nameOf(h.player_id)},4)}resulted(e,t){let n=_d[e.detail];if(n===void 0||this.resultCalled)return;this.resultCalled=!0;let r=e.actor_id;this.say(n,e.event_id,t,{a:this.nameOf(r),b:this.nameOf(this.otherId(r))},`result`,0)}inspect(e,t){if(e.phase===`fight`)for(let n of e.fighters){let r=this.notesFor(n.player_id),i={a:this.nameOf(this.otherId(n.player_id)),b:this.nameOf(n.player_id)};for(let[a,o]of[[0,`left`],[1,`right`]]){let s=a===0?n.trauma.left_cut:n.trauma.right_cut,c=r.cutMarks[a];c=ud[c]&&(r.cutMarks[a]=ud.filter(e=>s>=e).length,this.say(c===0?`cut`:c===1?`cutWorse`:`cutDoctor`,e.tick+a,t,{...i,side:o},null));let l=a===0?n.trauma.left_eye:n.trauma.right_eye;!r.eyeSwelling[a]&&!r.eyeShut[a]&&l>=fd&&(r.eyeSwelling[a]=!0,this.say(`eyeSwelling`,e.tick+a,t,{...i,side:o},null))}let a=n.stamina/Math.max(1,n.maximum_stamina);a=2*this.tickRate&&this.tired(n.player_id,e.tick,t)):a>=md&&(r.lowSince=null)}}tired(e,t,n){let r=this.notesFor(e);r.tiredRound!==this.round&&(r.tiredRound=this.round,this.say(`tiring`,t,n,{a:this.nameOf(this.otherId(e)),b:this.nameOf(e)},null))}say(e,t,n,r,i,a=Zu,o,s){let c=Td[e],l=`${e}:${s??``}`;if(c!==void 0){if(n<(this.cooldowns.get(l)??-1/0))return!1;this.cooldowns.set(l,n+c)}let u=Fd(this.choose(e,t),r),d={speaker:wd[e]??`play`,text:u,priority:Cd[e],urgent:Sd.has(e),hold:Ud(u),card:null};return this.enqueue(d,n+a,i,o,vd[e]??null)}sayAt(e,t,n,r,i){let a=Fd(this.choose(e,t),r);this.enqueue({speaker:wd[e]??`play`,text:a,priority:Cd[e],urgent:Sd.has(e),hold:Ud(a),card:null},n,null,i)}announcement(e,t,n,r){return{speaker:`announcer`,text:e,priority:r,urgent:!0,hold:n,card:t}}choose(e,t){let n=gd[e],r=Id(t,e.length*31+e.charCodeAt(0))%n.length;return n.length>1&&this.lastChoice.get(e)===r&&(r=(r+1)%n.length),this.lastChoice.set(e,r),n[r]}enqueue(e,t,n,r,i=null,a=null){if(n!==null){if(this.queue.some(t=>t.moment===n&&t.line.priority>=e.priority)||this.showing?.moment===n&&this.showing.line.priority>=e.priority)return!1;for(let e=this.queue.length-1;e>=0;--e)this.queue[e].moment===n&&this.queue.splice(e,1)}if(this.queue.push({line:e,at:t,expires:t+(r??(e.urgent?td:ed)),moment:n,phases:i,spoken:a}),this.queue.length>rd){let e=0;for(let t=1;tt.player_id===e)}otherId(e){let t=this.fighters;return t===null||e===null?null:t[0].player_id===e?t[1].player_id:t[1].player_id===e?t[0].player_id:null}seatOf(e){let t=this.fighters?.findIndex(t=>t.player_id===e)??-1;return t===0||t===1?t:null}nameOf(e){if(e===null)return`the fighter`;let t=this.players[e]?.name;if(t!==void 0&&t.trim().length>0)return t.trim();let n=this.seatOf(e);return n===0?`the blue corner`:n===1?`the red corner`:`the fighter`}};function Ud(e){return Math.min(5.2,Math.max(2.2,1.4+e.length*.05))}var Wd=`attached`,Gd=1e3,Kd=1001,qd=1002,Jd=1003,Yd=1004,Xd=1005,Zd=1006,Qd=1007,$d=1008,ef=1009,tf=1010,nf=1011,rf=1012,af=1013,of=1014,sf=1015,cf=1016,lf=1017,uf=1018,df=1020,ff=35902,pf=35899,mf=1021,hf=1022,gf=1023,_f=1026,vf=1027,yf=1028,bf=1029,xf=1030,Sf=1031,Cf=1033,wf=33776,Tf=33777,Ef=33778,Df=33779,Of=35840,kf=35841,Af=35842,jf=35843,Mf=36196,Nf=37492,Pf=37496,Ff=37488,If=37489,Lf=37490,Rf=37491,zf=37808,Bf=37809,Vf=37810,Hf=37811,Uf=37812,Wf=37813,Gf=37814,Kf=37815,qf=37816,Jf=37817,Yf=37818,Xf=37819,Zf=37820,Qf=37821,$f=36492,ep=36494,tp=36495,np=36283,rp=36284,ip=36285,ap=36286,op=2300,sp=2301,cp=2302,lp=2303,up=2400,dp=2401,fp=2402,pp=2500,mp=3200,hp=`srgb`,gp=`srgb-linear`,_p=`linear`,vp=`srgb`,yp=7680,bp=35044,xp=35048,Sp=2e3;function Cp(e){for(let t=e.length-1;t>=0;--t)if(e[t]>=65535)return!0;return!1}function wp(e){return ArrayBuffer.isView(e)&&!(e instanceof DataView)}function Tp(e){return document.createElementNS(`http://www.w3.org/1999/xhtml`,e)}function Ep(){let e=Tp(`canvas`);return e.style.display=`block`,e}var Dp={};function Op(...e){let t=`THREE.`+e.shift();console.log(t,...e)}function kp(e){let t=e[0];if(typeof t==`string`&&t.startsWith(`TSL:`)){let t=e[1];t&&t.isStackTrace?e[0]+=` `+t.getLocation():e[1]=`Stack trace not available. Enable "THREE.Node.captureStackTrace" to capture stack traces.`}return e}function Ap(...e){e=kp(e);let t=`THREE.`+e.shift();{let n=e[0];n&&n.isStackTrace?console.warn(n.getError(t)):console.warn(t,...e)}}function jp(...e){e=kp(e);let t=`THREE.`+e.shift();{let n=e[0];n&&n.isStackTrace?console.error(n.getError(t)):console.error(t,...e)}}function Mp(...e){let t=e.join(` `);t in Dp||(Dp[t]=!0,Ap(...e))}function Np(e,t,n){return new Promise(function(r,i){function a(){switch(e.clientWaitSync(t,e.SYNC_FLUSH_COMMANDS_BIT,0)){case e.WAIT_FAILED:i();break;case e.TIMEOUT_EXPIRED:setTimeout(a,n);break;default:r()}}setTimeout(a,n)})}var Pp={0:1,2:6,4:7,3:5,1:0,6:2,7:4,5:3},Fp=class{addEventListener(e,t){this._listeners===void 0&&(this._listeners={});let n=this._listeners;n[e]===void 0&&(n[e]=[]),n[e].indexOf(t)===-1&&n[e].push(t)}hasEventListener(e,t){let n=this._listeners;return n!==void 0&&n[e]!==void 0&&n[e].indexOf(t)!==-1}removeEventListener(e,t){let n=this._listeners;if(n===void 0)return;let r=n[e];if(r!==void 0){let e=r.indexOf(t);e!==-1&&r.splice(e,1)}}dispatchEvent(e){let t=this._listeners;if(t===void 0)return;let n=t[e.type];if(n!==void 0){e.target=this;let t=n.slice(0);for(let n=0,r=t.length;n>8&255]+Ip[e>>16&255]+Ip[e>>24&255]+`-`+Ip[t&255]+Ip[t>>8&255]+`-`+Ip[t>>16&15|64]+Ip[t>>24&255]+`-`+Ip[n&63|128]+Ip[n>>8&255]+`-`+Ip[n>>16&255]+Ip[n>>24&255]+Ip[r&255]+Ip[r>>8&255]+Ip[r>>16&255]+Ip[r>>24&255]).toLowerCase()}function Vp(e,t,n){return Math.max(t,Math.min(n,e))}function Hp(e,t){return(e%t+t)%t}function Up(e,t,n,r,i){return r+(e-t)*(i-r)/(n-t)}function Wp(e,t,n){return e===t?0:(n-e)/(t-e)}function Gp(e,t,n){return(1-n)*e+n*t}function Kp(e,t,n,r){return Gp(e,t,1-Math.exp(-n*r))}function qp(e,t=1){return t-Math.abs(Hp(e,t*2)-t)}function Jp(e,t,n){return e<=t?0:e>=n?1:(e=(e-t)/(n-t),e*e*(3-2*e))}function Yp(e,t,n){return e<=t?0:e>=n?1:(e=(e-t)/(n-t),e*e*e*(e*(e*6-15)+10))}function Xp(e,t){return e+Math.floor(Math.random()*(t-e+1))}function Zp(e,t){return e+Math.random()*(t-e)}function Qp(e){return e*(.5-Math.random())}function $p(e){e!==void 0&&(Lp=e);let t=Lp+=1831565813;return t=Math.imul(t^t>>>15,t|1),t^=t+Math.imul(t^t>>>7,t|61),((t^t>>>14)>>>0)/4294967296}function em(e){return e*Rp}function tm(e){return e*zp}function nm(e){return e>0&&Number.isInteger(e)&&2**Math.round(Math.log2(e))===e}function rm(e){return 2**Math.ceil(Math.log(e)/Math.LN2)}function im(e){return 2**Math.floor(Math.log(e)/Math.LN2)}function am(e,t,n,r,i){let a=Math.cos,o=Math.sin,s=a(n/2),c=o(n/2),l=a((t+r)/2),u=o((t+r)/2),d=a((t-r)/2),f=o((t-r)/2),p=a((r-t)/2),m=o((r-t)/2);switch(i){case`XYX`:e.set(s*u,c*d,c*f,s*l);break;case`YZY`:e.set(c*f,s*u,c*d,s*l);break;case`ZXZ`:e.set(c*d,c*f,s*u,s*l);break;case`XZX`:e.set(s*u,c*m,c*p,s*l);break;case`YXY`:e.set(c*p,s*u,c*m,s*l);break;case`ZYZ`:e.set(c*m,c*p,s*u,s*l);break;default:Ap(`MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: `+i)}}function om(e,t){switch(t.constructor){case Float32Array:return e;case Uint32Array:return e/4294967295;case Uint16Array:return e/65535;case Uint8Array:case Uint8ClampedArray:return e/255;case Int32Array:return Math.max(e/2147483647,-1);case Int16Array:return Math.max(e/32767,-1);case Int8Array:return Math.max(e/127,-1);default:throw Error(`THREE.MathUtils: Invalid component type.`)}}function sm(e,t){switch(t.constructor){case Float32Array:return e;case Uint32Array:return Math.round(e*4294967295);case Uint16Array:return Math.round(e*65535);case Uint8Array:case Uint8ClampedArray:return Math.round(e*255);case Int32Array:return Math.round(e*2147483647);case Int16Array:return Math.round(e*32767);case Int8Array:return Math.round(e*127);default:throw Error(`THREE.MathUtils: Invalid component type.`)}}var K={DEG2RAD:Rp,RAD2DEG:zp,generateUUID:Bp,clamp:Vp,euclideanModulo:Hp,mapLinear:Up,inverseLerp:Wp,lerp:Gp,damp:Kp,pingpong:qp,smoothstep:Jp,smootherstep:Yp,randInt:Xp,randFloat:Zp,randFloatSpread:Qp,seededRandom:$p,degToRad:em,radToDeg:tm,isPowerOfTwo:nm,ceilPowerOfTwo:rm,floorPowerOfTwo:im,setQuaternionFromProperEuler:am,normalize:sm,denormalize:om},q=class e{static{e.prototype.isVector2=!0}constructor(e=0,t=0){this.x=e,this.y=t}get width(){return this.x}set width(e){this.x=e}get height(){return this.y}set height(e){this.y=e}set(e,t){return this.x=e,this.y=t,this}setScalar(e){return this.x=e,this.y=e,this}setX(e){return this.x=e,this}setY(e){return this.y=e,this}setComponent(e,t){switch(e){case 0:this.x=t;break;case 1:this.y=t;break;default:throw Error(`THREE.Vector2: index is out of range: `+e)}return this}getComponent(e){switch(e){case 0:return this.x;case 1:return this.y;default:throw Error(`THREE.Vector2: index is out of range: `+e)}}clone(){return new this.constructor(this.x,this.y)}copy(e){return this.x=e.x,this.y=e.y,this}add(e){return this.x+=e.x,this.y+=e.y,this}addScalar(e){return this.x+=e,this.y+=e,this}addVectors(e,t){return this.x=e.x+t.x,this.y=e.y+t.y,this}addScaledVector(e,t){return this.x+=e.x*t,this.y+=e.y*t,this}sub(e){return this.x-=e.x,this.y-=e.y,this}subScalar(e){return this.x-=e,this.y-=e,this}subVectors(e,t){return this.x=e.x-t.x,this.y=e.y-t.y,this}multiply(e){return this.x*=e.x,this.y*=e.y,this}multiplyScalar(e){return this.x*=e,this.y*=e,this}divide(e){return this.x/=e.x,this.y/=e.y,this}divideScalar(e){return this.multiplyScalar(1/e)}applyMatrix3(e){let t=this.x,n=this.y,r=e.elements;return this.x=r[0]*t+r[3]*n+r[6],this.y=r[1]*t+r[4]*n+r[7],this}min(e){return this.x=Math.min(this.x,e.x),this.y=Math.min(this.y,e.y),this}max(e){return this.x=Math.max(this.x,e.x),this.y=Math.max(this.y,e.y),this}clamp(e,t){return this.x=Vp(this.x,e.x,t.x),this.y=Vp(this.y,e.y,t.y),this}clampScalar(e,t){return this.x=Vp(this.x,e,t),this.y=Vp(this.y,e,t),this}clampLength(e,t){let n=this.length();return this.divideScalar(n||1).multiplyScalar(Vp(n,e,t))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this}negate(){return this.x=-this.x,this.y=-this.y,this}dot(e){return this.x*e.x+this.y*e.y}cross(e){return this.x*e.y-this.y*e.x}lengthSq(){return this.x*this.x+this.y*this.y}length(){return Math.sqrt(this.x*this.x+this.y*this.y)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)}normalize(){return this.divideScalar(this.length()||1)}angle(){return Math.atan2(-this.y,-this.x)+Math.PI}angleTo(e){let t=Math.sqrt(this.lengthSq()*e.lengthSq());if(t===0)return Math.PI/2;let n=this.dot(e)/t;return Math.acos(Vp(n,-1,1))}distanceTo(e){return Math.sqrt(this.distanceToSquared(e))}distanceToSquared(e){let t=this.x-e.x,n=this.y-e.y;return t*t+n*n}manhattanDistanceTo(e){return Math.abs(this.x-e.x)+Math.abs(this.y-e.y)}setLength(e){return this.normalize().multiplyScalar(e)}lerp(e,t){return this.x+=(e.x-this.x)*t,this.y+=(e.y-this.y)*t,this}lerpVectors(e,t,n){return this.x=e.x+(t.x-e.x)*n,this.y=e.y+(t.y-e.y)*n,this}equals(e){return e.x===this.x&&e.y===this.y}fromArray(e,t=0){return this.x=e[t],this.y=e[t+1],this}toArray(e=[],t=0){return e[t]=this.x,e[t+1]=this.y,e}fromBufferAttribute(e,t){return this.x=e.getX(t),this.y=e.getY(t),this}rotateAround(e,t){let n=Math.cos(t),r=Math.sin(t),i=this.x-e.x,a=this.y-e.y;return this.x=i*n-a*r+e.x,this.y=i*r+a*n+e.y,this}random(){return this.x=Math.random(),this.y=Math.random(),this}*[Symbol.iterator](){yield this.x,yield this.y}},cm=class{constructor(e=0,t=0,n=0,r=1){this.isQuaternion=!0,this._x=e,this._y=t,this._z=n,this._w=r}static slerpFlat(e,t,n,r,i,a,o){let s=n[r+0],c=n[r+1],l=n[r+2],u=n[r+3],d=i[a+0],f=i[a+1],p=i[a+2],m=i[a+3];if(u!==m||s!==d||c!==f||l!==p){let e=s*d+c*f+l*p+u*m;e<0&&(d=-d,f=-f,p=-p,m=-m,e=-e);let t=1-o;if(e<.9995){let n=Math.acos(e),r=Math.sin(n);t=Math.sin(t*n)/r,o=Math.sin(o*n)/r,s=s*t+d*o,c=c*t+f*o,l=l*t+p*o,u=u*t+m*o}else{s=s*t+d*o,c=c*t+f*o,l=l*t+p*o,u=u*t+m*o;let e=1/Math.sqrt(s*s+c*c+l*l+u*u);s*=e,c*=e,l*=e,u*=e}}e[t]=s,e[t+1]=c,e[t+2]=l,e[t+3]=u}static multiplyQuaternionsFlat(e,t,n,r,i,a){let o=n[r],s=n[r+1],c=n[r+2],l=n[r+3],u=i[a],d=i[a+1],f=i[a+2],p=i[a+3];return e[t]=o*p+l*u+s*f-c*d,e[t+1]=s*p+l*d+c*u-o*f,e[t+2]=c*p+l*f+o*d-s*u,e[t+3]=l*p-o*u-s*d-c*f,e}get x(){return this._x}set x(e){this._x=e,this._onChangeCallback()}get y(){return this._y}set y(e){this._y=e,this._onChangeCallback()}get z(){return this._z}set z(e){this._z=e,this._onChangeCallback()}get w(){return this._w}set w(e){this._w=e,this._onChangeCallback()}set(e,t,n,r){return this._x=e,this._y=t,this._z=n,this._w=r,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._w)}copy(e){return this._x=e.x,this._y=e.y,this._z=e.z,this._w=e.w,this._onChangeCallback(),this}setFromEuler(e,t=!0){let n=e._x,r=e._y,i=e._z,a=e._order,o=Math.cos,s=Math.sin,c=o(n/2),l=o(r/2),u=o(i/2),d=s(n/2),f=s(r/2),p=s(i/2);switch(a){case`XYZ`:this._x=d*l*u+c*f*p,this._y=c*f*u-d*l*p,this._z=c*l*p+d*f*u,this._w=c*l*u-d*f*p;break;case`YXZ`:this._x=d*l*u+c*f*p,this._y=c*f*u-d*l*p,this._z=c*l*p-d*f*u,this._w=c*l*u+d*f*p;break;case`ZXY`:this._x=d*l*u-c*f*p,this._y=c*f*u+d*l*p,this._z=c*l*p+d*f*u,this._w=c*l*u-d*f*p;break;case`ZYX`:this._x=d*l*u-c*f*p,this._y=c*f*u+d*l*p,this._z=c*l*p-d*f*u,this._w=c*l*u+d*f*p;break;case`YZX`:this._x=d*l*u+c*f*p,this._y=c*f*u+d*l*p,this._z=c*l*p-d*f*u,this._w=c*l*u-d*f*p;break;case`XZY`:this._x=d*l*u-c*f*p,this._y=c*f*u-d*l*p,this._z=c*l*p+d*f*u,this._w=c*l*u+d*f*p;break;default:Ap(`Quaternion: .setFromEuler() encountered an unknown order: `+a)}return t===!0&&this._onChangeCallback(),this}setFromAxisAngle(e,t){let n=t/2,r=Math.sin(n);return this._x=e.x*r,this._y=e.y*r,this._z=e.z*r,this._w=Math.cos(n),this._onChangeCallback(),this}setFromRotationMatrix(e){let t=e.elements,n=t[0],r=t[4],i=t[8],a=t[1],o=t[5],s=t[9],c=t[2],l=t[6],u=t[10],d=n+o+u;if(d>0){let e=.5/Math.sqrt(d+1);this._w=.25/e,this._x=(l-s)*e,this._y=(i-c)*e,this._z=(a-r)*e}else if(n>o&&n>u){let e=2*Math.sqrt(1+n-o-u);this._w=(l-s)/e,this._x=.25*e,this._y=(r+a)/e,this._z=(i+c)/e}else if(o>u){let e=2*Math.sqrt(1+o-n-u);this._w=(i-c)/e,this._x=(r+a)/e,this._y=.25*e,this._z=(s+l)/e}else{let e=2*Math.sqrt(1+u-n-o);this._w=(a-r)/e,this._x=(i+c)/e,this._y=(s+l)/e,this._z=.25*e}return this._onChangeCallback(),this}setFromUnitVectors(e,t){let n=e.dot(t)+1;return n<1e-8?(n=0,Math.abs(e.x)>Math.abs(e.z)?(this._x=-e.y,this._y=e.x,this._z=0,this._w=n):(this._x=0,this._y=-e.z,this._z=e.y,this._w=n)):(this._x=e.y*t.z-e.z*t.y,this._y=e.z*t.x-e.x*t.z,this._z=e.x*t.y-e.y*t.x,this._w=n),this.normalize()}angleTo(e){return 2*Math.acos(Math.abs(Vp(this.dot(e),-1,1)))}rotateTowards(e,t){let n=this.angleTo(e);if(n===0)return this;let r=Math.min(1,t/n);return this.slerp(e,r),this}identity(){return this.set(0,0,0,1)}invert(){return this.conjugate()}conjugate(){return this._x*=-1,this._y*=-1,this._z*=-1,this._onChangeCallback(),this}dot(e){return this._x*e._x+this._y*e._y+this._z*e._z+this._w*e._w}lengthSq(){return this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w}length(){return Math.sqrt(this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w)}normalize(){let e=this.length();return e===0?(this._x=0,this._y=0,this._z=0,this._w=1):(e=1/e,this._x*=e,this._y*=e,this._z*=e,this._w*=e),this._onChangeCallback(),this}multiply(e){return this.multiplyQuaternions(this,e)}premultiply(e){return this.multiplyQuaternions(e,this)}multiplyQuaternions(e,t){let n=e._x,r=e._y,i=e._z,a=e._w,o=t._x,s=t._y,c=t._z,l=t._w;return this._x=n*l+a*o+r*c-i*s,this._y=r*l+a*s+i*o-n*c,this._z=i*l+a*c+n*s-r*o,this._w=a*l-n*o-r*s-i*c,this._onChangeCallback(),this}slerp(e,t){let n=e._x,r=e._y,i=e._z,a=e._w,o=this.dot(e);o<0&&(n=-n,r=-r,i=-i,a=-a,o=-o);let s=1-t;if(o<.9995){let e=Math.acos(o),c=Math.sin(e);s=Math.sin(s*e)/c,t=Math.sin(t*e)/c,this._x=this._x*s+n*t,this._y=this._y*s+r*t,this._z=this._z*s+i*t,this._w=this._w*s+a*t,this._onChangeCallback()}else this._x=this._x*s+n*t,this._y=this._y*s+r*t,this._z=this._z*s+i*t,this._w=this._w*s+a*t,this.normalize();return this}slerpQuaternions(e,t,n){return this.copy(e).slerp(t,n)}random(){let e=2*Math.PI*Math.random(),t=2*Math.PI*Math.random(),n=Math.random(),r=Math.sqrt(1-n),i=Math.sqrt(n);return this.set(r*Math.sin(e),r*Math.cos(e),i*Math.sin(t),i*Math.cos(t))}equals(e){return e._x===this._x&&e._y===this._y&&e._z===this._z&&e._w===this._w}fromArray(e,t=0){return this._x=e[t],this._y=e[t+1],this._z=e[t+2],this._w=e[t+3],this._onChangeCallback(),this}toArray(e=[],t=0){return e[t]=this._x,e[t+1]=this._y,e[t+2]=this._z,e[t+3]=this._w,e}fromBufferAttribute(e,t){return this._x=e.getX(t),this._y=e.getY(t),this._z=e.getZ(t),this._w=e.getW(t),this._onChangeCallback(),this}toJSON(){return this.toArray()}_onChange(e){return this._onChangeCallback=e,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._w}},J=class e{static{e.prototype.isVector3=!0}constructor(e=0,t=0,n=0){this.x=e,this.y=t,this.z=n}set(e,t,n){return n===void 0&&(n=this.z),this.x=e,this.y=t,this.z=n,this}setScalar(e){return this.x=e,this.y=e,this.z=e,this}setX(e){return this.x=e,this}setY(e){return this.y=e,this}setZ(e){return this.z=e,this}setComponent(e,t){switch(e){case 0:this.x=t;break;case 1:this.y=t;break;case 2:this.z=t;break;default:throw Error(`THREE.Vector3: index is out of range: `+e)}return this}getComponent(e){switch(e){case 0:return this.x;case 1:return this.y;case 2:return this.z;default:throw Error(`THREE.Vector3: index is out of range: `+e)}}clone(){return new this.constructor(this.x,this.y,this.z)}copy(e){return this.x=e.x,this.y=e.y,this.z=e.z,this}add(e){return this.x+=e.x,this.y+=e.y,this.z+=e.z,this}addScalar(e){return this.x+=e,this.y+=e,this.z+=e,this}addVectors(e,t){return this.x=e.x+t.x,this.y=e.y+t.y,this.z=e.z+t.z,this}addScaledVector(e,t){return this.x+=e.x*t,this.y+=e.y*t,this.z+=e.z*t,this}sub(e){return this.x-=e.x,this.y-=e.y,this.z-=e.z,this}subScalar(e){return this.x-=e,this.y-=e,this.z-=e,this}subVectors(e,t){return this.x=e.x-t.x,this.y=e.y-t.y,this.z=e.z-t.z,this}multiply(e){return this.x*=e.x,this.y*=e.y,this.z*=e.z,this}multiplyScalar(e){return this.x*=e,this.y*=e,this.z*=e,this}multiplyVectors(e,t){return this.x=e.x*t.x,this.y=e.y*t.y,this.z=e.z*t.z,this}applyEuler(e){return this.applyQuaternion(um.setFromEuler(e))}applyAxisAngle(e,t){return this.applyQuaternion(um.setFromAxisAngle(e,t))}applyMatrix3(e){let t=this.x,n=this.y,r=this.z,i=e.elements;return this.x=i[0]*t+i[3]*n+i[6]*r,this.y=i[1]*t+i[4]*n+i[7]*r,this.z=i[2]*t+i[5]*n+i[8]*r,this}applyNormalMatrix(e){return this.applyMatrix3(e).normalize()}applyMatrix4(e){let t=this.x,n=this.y,r=this.z,i=e.elements,a=1/(i[3]*t+i[7]*n+i[11]*r+i[15]);return this.x=(i[0]*t+i[4]*n+i[8]*r+i[12])*a,this.y=(i[1]*t+i[5]*n+i[9]*r+i[13])*a,this.z=(i[2]*t+i[6]*n+i[10]*r+i[14])*a,this}applyQuaternion(e){let t=this.x,n=this.y,r=this.z,i=e.x,a=e.y,o=e.z,s=e.w,c=2*(a*r-o*n),l=2*(o*t-i*r),u=2*(i*n-a*t);return this.x=t+s*c+a*u-o*l,this.y=n+s*l+o*c-i*u,this.z=r+s*u+i*l-a*c,this}project(e){return this.applyMatrix4(e.matrixWorldInverse).applyMatrix4(e.projectionMatrix)}unproject(e){return this.applyMatrix4(e.projectionMatrixInverse).applyMatrix4(e.matrixWorld)}transformDirection(e){let t=this.x,n=this.y,r=this.z,i=e.elements;return this.x=i[0]*t+i[4]*n+i[8]*r,this.y=i[1]*t+i[5]*n+i[9]*r,this.z=i[2]*t+i[6]*n+i[10]*r,this.normalize()}divide(e){return this.x/=e.x,this.y/=e.y,this.z/=e.z,this}divideScalar(e){return this.multiplyScalar(1/e)}min(e){return this.x=Math.min(this.x,e.x),this.y=Math.min(this.y,e.y),this.z=Math.min(this.z,e.z),this}max(e){return this.x=Math.max(this.x,e.x),this.y=Math.max(this.y,e.y),this.z=Math.max(this.z,e.z),this}clamp(e,t){return this.x=Vp(this.x,e.x,t.x),this.y=Vp(this.y,e.y,t.y),this.z=Vp(this.z,e.z,t.z),this}clampScalar(e,t){return this.x=Vp(this.x,e,t),this.y=Vp(this.y,e,t),this.z=Vp(this.z,e,t),this}clampLength(e,t){let n=this.length();return this.divideScalar(n||1).multiplyScalar(Vp(n,e,t))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this.z=Math.trunc(this.z),this}negate(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this}dot(e){return this.x*e.x+this.y*e.y+this.z*e.z}lengthSq(){return this.x*this.x+this.y*this.y+this.z*this.z}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)}normalize(){return this.divideScalar(this.length()||1)}setLength(e){return this.normalize().multiplyScalar(e)}lerp(e,t){return this.x+=(e.x-this.x)*t,this.y+=(e.y-this.y)*t,this.z+=(e.z-this.z)*t,this}lerpVectors(e,t,n){return this.x=e.x+(t.x-e.x)*n,this.y=e.y+(t.y-e.y)*n,this.z=e.z+(t.z-e.z)*n,this}cross(e){return this.crossVectors(this,e)}crossVectors(e,t){let n=e.x,r=e.y,i=e.z,a=t.x,o=t.y,s=t.z;return this.x=r*s-i*o,this.y=i*a-n*s,this.z=n*o-r*a,this}projectOnVector(e){let t=e.lengthSq();if(t===0)return this.set(0,0,0);let n=e.dot(this)/t;return this.copy(e).multiplyScalar(n)}projectOnPlane(e){return lm.copy(this).projectOnVector(e),this.sub(lm)}reflect(e){return this.sub(lm.copy(e).multiplyScalar(2*this.dot(e)))}angleTo(e){let t=Math.sqrt(this.lengthSq()*e.lengthSq());if(t===0)return Math.PI/2;let n=this.dot(e)/t;return Math.acos(Vp(n,-1,1))}distanceTo(e){return Math.sqrt(this.distanceToSquared(e))}distanceToSquared(e){let t=this.x-e.x,n=this.y-e.y,r=this.z-e.z;return t*t+n*n+r*r}manhattanDistanceTo(e){return Math.abs(this.x-e.x)+Math.abs(this.y-e.y)+Math.abs(this.z-e.z)}setFromSpherical(e){return this.setFromSphericalCoords(e.radius,e.phi,e.theta)}setFromSphericalCoords(e,t,n){let r=Math.sin(t)*e;return this.x=r*Math.sin(n),this.y=Math.cos(t)*e,this.z=r*Math.cos(n),this}setFromCylindrical(e){return this.setFromCylindricalCoords(e.radius,e.theta,e.y)}setFromCylindricalCoords(e,t,n){return this.x=e*Math.sin(t),this.y=n,this.z=e*Math.cos(t),this}setFromMatrixPosition(e){let t=e.elements;return this.x=t[12],this.y=t[13],this.z=t[14],this}setFromMatrixScale(e){let t=this.setFromMatrixColumn(e,0).length(),n=this.setFromMatrixColumn(e,1).length(),r=this.setFromMatrixColumn(e,2).length();return this.x=t,this.y=n,this.z=r,this}setFromMatrixColumn(e,t){return this.fromArray(e.elements,t*4)}setFromMatrix3Column(e,t){return this.fromArray(e.elements,t*3)}setFromEuler(e){return this.x=e._x,this.y=e._y,this.z=e._z,this}setFromColor(e){return this.x=e.r,this.y=e.g,this.z=e.b,this}equals(e){return e.x===this.x&&e.y===this.y&&e.z===this.z}fromArray(e,t=0){return this.x=e[t],this.y=e[t+1],this.z=e[t+2],this}toArray(e=[],t=0){return e[t]=this.x,e[t+1]=this.y,e[t+2]=this.z,e}fromBufferAttribute(e,t){return this.x=e.getX(t),this.y=e.getY(t),this.z=e.getZ(t),this}random(){return this.x=Math.random(),this.y=Math.random(),this.z=Math.random(),this}randomDirection(){let e=Math.random()*Math.PI*2,t=Math.random()*2-1,n=Math.sqrt(1-t*t);return this.x=n*Math.cos(e),this.y=t,this.z=n*Math.sin(e),this}*[Symbol.iterator](){yield this.x,yield this.y,yield this.z}},lm=new J,um=new cm,dm=class e{static{e.prototype.isMatrix3=!0}constructor(e,t,n,r,i,a,o,s,c){this.elements=[1,0,0,0,1,0,0,0,1],e!==void 0&&this.set(e,t,n,r,i,a,o,s,c)}set(e,t,n,r,i,a,o,s,c){let l=this.elements;return l[0]=e,l[1]=r,l[2]=o,l[3]=t,l[4]=i,l[5]=s,l[6]=n,l[7]=a,l[8]=c,this}identity(){return this.set(1,0,0,0,1,0,0,0,1),this}copy(e){let t=this.elements,n=e.elements;return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t[4]=n[4],t[5]=n[5],t[6]=n[6],t[7]=n[7],t[8]=n[8],this}extractBasis(e,t,n){return e.setFromMatrix3Column(this,0),t.setFromMatrix3Column(this,1),n.setFromMatrix3Column(this,2),this}setFromMatrix4(e){let t=e.elements;return this.set(t[0],t[4],t[8],t[1],t[5],t[9],t[2],t[6],t[10]),this}multiply(e){return this.multiplyMatrices(this,e)}premultiply(e){return this.multiplyMatrices(e,this)}multiplyMatrices(e,t){let n=e.elements,r=t.elements,i=this.elements,a=n[0],o=n[3],s=n[6],c=n[1],l=n[4],u=n[7],d=n[2],f=n[5],p=n[8],m=r[0],h=r[3],g=r[6],_=r[1],v=r[4],y=r[7],b=r[2],x=r[5],S=r[8];return i[0]=a*m+o*_+s*b,i[3]=a*h+o*v+s*x,i[6]=a*g+o*y+s*S,i[1]=c*m+l*_+u*b,i[4]=c*h+l*v+u*x,i[7]=c*g+l*y+u*S,i[2]=d*m+f*_+p*b,i[5]=d*h+f*v+p*x,i[8]=d*g+f*y+p*S,this}multiplyScalar(e){let t=this.elements;return t[0]*=e,t[3]*=e,t[6]*=e,t[1]*=e,t[4]*=e,t[7]*=e,t[2]*=e,t[5]*=e,t[8]*=e,this}determinant(){let e=this.elements,t=e[0],n=e[1],r=e[2],i=e[3],a=e[4],o=e[5],s=e[6],c=e[7],l=e[8];return t*a*l-t*o*c-n*i*l+n*o*s+r*i*c-r*a*s}invert(){let e=this.elements,t=e[0],n=e[1],r=e[2],i=e[3],a=e[4],o=e[5],s=e[6],c=e[7],l=e[8],u=l*a-o*c,d=o*s-l*i,f=c*i-a*s,p=t*u+n*d+r*f;if(p===0)return this.set(0,0,0,0,0,0,0,0,0);let m=1/p;return e[0]=u*m,e[1]=(r*c-l*n)*m,e[2]=(o*n-r*a)*m,e[3]=d*m,e[4]=(l*t-r*s)*m,e[5]=(r*i-o*t)*m,e[6]=f*m,e[7]=(n*s-c*t)*m,e[8]=(a*t-n*i)*m,this}transpose(){let e,t=this.elements;return e=t[1],t[1]=t[3],t[3]=e,e=t[2],t[2]=t[6],t[6]=e,e=t[5],t[5]=t[7],t[7]=e,this}getNormalMatrix(e){return this.setFromMatrix4(e).invert().transpose()}transposeIntoArray(e){let t=this.elements;return e[0]=t[0],e[1]=t[3],e[2]=t[6],e[3]=t[1],e[4]=t[4],e[5]=t[7],e[6]=t[2],e[7]=t[5],e[8]=t[8],this}setUvTransform(e,t,n,r,i,a,o){let s=Math.cos(i),c=Math.sin(i);return this.set(n*s,n*c,-n*(s*a+c*o)+a+e,-r*c,r*s,-r*(-c*a+s*o)+o+t,0,0,1),this}scale(e,t){return Mp(`Matrix3: .scale() is deprecated. Use .makeScale() instead.`),this.premultiply(fm.makeScale(e,t)),this}rotate(e){return Mp(`Matrix3: .rotate() is deprecated. Use .makeRotation() instead.`),this.premultiply(fm.makeRotation(-e)),this}translate(e,t){return Mp(`Matrix3: .translate() is deprecated. Use .makeTranslation() instead.`),this.premultiply(fm.makeTranslation(e,t)),this}makeTranslation(e,t){return e.isVector2?this.set(1,0,e.x,0,1,e.y,0,0,1):this.set(1,0,e,0,1,t,0,0,1),this}makeRotation(e){let t=Math.cos(e),n=Math.sin(e);return this.set(t,-n,0,n,t,0,0,0,1),this}makeScale(e,t){return this.set(e,0,0,0,t,0,0,0,1),this}equals(e){let t=this.elements,n=e.elements;for(let e=0;e<9;e++)if(t[e]!==n[e])return!1;return!0}fromArray(e,t=0){for(let n=0;n<9;n++)this.elements[n]=e[n+t];return this}toArray(e=[],t=0){let n=this.elements;return e[t]=n[0],e[t+1]=n[1],e[t+2]=n[2],e[t+3]=n[3],e[t+4]=n[4],e[t+5]=n[5],e[t+6]=n[6],e[t+7]=n[7],e[t+8]=n[8],e}clone(){return new this.constructor().fromArray(this.elements)}},fm=new dm,pm=new dm().set(.4123908,.3575843,.1804808,.212639,.7151687,.0721923,.0193308,.1191948,.9505322),mm=new dm().set(3.2409699,-1.5373832,-.4986108,-.9692436,1.8759675,.0415551,.0556301,-.203977,1.0569715);function hm(){let e={enabled:!0,workingColorSpace:gp,spaces:{},convert:function(e,t,n){return this.enabled===!1||t===n||!t||!n?e:(this.spaces[t].transfer===`srgb`&&(e.r=_m(e.r),e.g=_m(e.g),e.b=_m(e.b)),this.spaces[t].primaries!==this.spaces[n].primaries&&(e.applyMatrix3(this.spaces[t].toXYZ),e.applyMatrix3(this.spaces[n].fromXYZ)),this.spaces[n].transfer===`srgb`&&(e.r=vm(e.r),e.g=vm(e.g),e.b=vm(e.b)),e)},workingToColorSpace:function(e,t){return this.convert(e,this.workingColorSpace,t)},colorSpaceToWorking:function(e,t){return this.convert(e,t,this.workingColorSpace)},getPrimaries:function(e){return this.spaces[e].primaries},getTransfer:function(e){return e===``?_p:this.spaces[e].transfer},getToneMappingMode:function(e){return this.spaces[e].outputColorSpaceConfig.toneMappingMode||`standard`},getLuminanceCoefficients:function(e,t=this.workingColorSpace){return e.fromArray(this.spaces[t].luminanceCoefficients)},define:function(e){Object.assign(this.spaces,e)},_getMatrix:function(e,t,n){return e.copy(this.spaces[t].toXYZ).multiply(this.spaces[n].fromXYZ)},_getDrawingBufferColorSpace:function(e){return this.spaces[e].outputColorSpaceConfig.drawingBufferColorSpace},_getUnpackColorSpace:function(e=this.workingColorSpace){return this.spaces[e].workingColorSpaceConfig.unpackColorSpace},fromWorkingColorSpace:function(t,n){return Mp(`ColorManagement: .fromWorkingColorSpace() has been renamed to .workingToColorSpace().`),e.workingToColorSpace(t,n)},toWorkingColorSpace:function(t,n){return Mp(`ColorManagement: .toWorkingColorSpace() has been renamed to .colorSpaceToWorking().`),e.colorSpaceToWorking(t,n)}},t=[.64,.33,.3,.6,.15,.06],n=[.2126,.7152,.0722],r=[.3127,.329];return e.define({[gp]:{primaries:t,whitePoint:r,transfer:_p,toXYZ:pm,fromXYZ:mm,luminanceCoefficients:n,workingColorSpaceConfig:{unpackColorSpace:hp},outputColorSpaceConfig:{drawingBufferColorSpace:hp}},[hp]:{primaries:t,whitePoint:r,transfer:vp,toXYZ:pm,fromXYZ:mm,luminanceCoefficients:n,outputColorSpaceConfig:{drawingBufferColorSpace:hp}}}),e}var gm=hm();function _m(e){return e<.04045?e*.0773993808:(e*.9478672986+.0521327014)**2.4}function vm(e){return e<.0031308?e*12.92:1.055*e**.41666-.055}var ym,bm=class{static getDataURL(e,t=`image/png`){if(/^data:/i.test(e.src)||typeof HTMLCanvasElement>`u`)return e.src;let n;if(e instanceof HTMLCanvasElement)n=e;else{ym===void 0&&(ym=Tp(`canvas`)),ym.width=e.width,ym.height=e.height;let t=ym.getContext(`2d`);e instanceof ImageData?t.putImageData(e,0,0):t.drawImage(e,0,0,e.width,e.height),n=ym}return n.toDataURL(t)}static sRGBToLinear(e){if(typeof HTMLImageElement<`u`&&e instanceof HTMLImageElement||typeof HTMLCanvasElement<`u`&&e instanceof HTMLCanvasElement||typeof ImageBitmap<`u`&&e instanceof ImageBitmap){let t=Tp(`canvas`);t.width=e.width,t.height=e.height;let n=t.getContext(`2d`);n.drawImage(e,0,0,e.width,e.height);let r=n.getImageData(0,0,e.width,e.height),i=r.data;for(let e=0;e1),this.pmremVersion=0,this.normalized=!1}get width(){return this.source.getSize(Tm).x}get height(){return this.source.getSize(Tm).y}get depth(){return this.source.getSize(Tm).z}get image(){return this.source.data}set image(e){this.source.data=e}updateMatrix(){this.matrix.setUvTransform(this.offset.x,this.offset.y,this.repeat.x,this.repeat.y,this.rotation,this.center.x,this.center.y)}addUpdateRange(e,t){this.updateRanges.push({start:e,count:t})}clearUpdateRanges(){this.updateRanges.length=0}clone(){return new this.constructor().copy(this)}copy(e){return this.name=e.name,this.source=e.source,this.mipmaps=e.mipmaps.slice(0),this.mapping=e.mapping,this.channel=e.channel,this.wrapS=e.wrapS,this.wrapT=e.wrapT,this.magFilter=e.magFilter,this.minFilter=e.minFilter,this.anisotropy=e.anisotropy,this.format=e.format,this.internalFormat=e.internalFormat,this.type=e.type,this.normalized=e.normalized,this.offset.copy(e.offset),this.repeat.copy(e.repeat),this.center.copy(e.center),this.rotation=e.rotation,this.matrixAutoUpdate=e.matrixAutoUpdate,this.matrix.copy(e.matrix),this.generateMipmaps=e.generateMipmaps,this.premultiplyAlpha=e.premultiplyAlpha,this.flipY=e.flipY,this.unpackAlignment=e.unpackAlignment,this.colorSpace=e.colorSpace,this.renderTarget=e.renderTarget,this.isRenderTargetTexture=e.isRenderTargetTexture,this.isArrayTexture=e.isArrayTexture,this.userData=JSON.parse(JSON.stringify(e.userData)),this.needsUpdate=!0,this}setValues(e){for(let t in e){let n=e[t];if(n===void 0){Ap(`Texture.setValues(): parameter '${t}' has value of undefined.`);continue}let r=this[t];if(r===void 0){Ap(`Texture.setValues(): property '${t}' does not exist.`);continue}r&&n&&r.isVector2&&n.isVector2||r&&n&&r.isVector3&&n.isVector3||r&&n&&r.isMatrix3&&n.isMatrix3?r.copy(n):this[t]=n}}toJSON(e){let t=e===void 0||typeof e==`string`;if(!t&&e.textures[this.uuid]!==void 0)return e.textures[this.uuid];let n={metadata:{version:4.7,type:`Texture`,generator:`Texture.toJSON`},uuid:this.uuid,name:this.name,image:this.source.toJSON(e).uuid,mapping:this.mapping,channel:this.channel,repeat:[this.repeat.x,this.repeat.y],offset:[this.offset.x,this.offset.y],center:[this.center.x,this.center.y],rotation:this.rotation,wrap:[this.wrapS,this.wrapT],format:this.format,internalFormat:this.internalFormat,type:this.type,normalized:this.normalized,colorSpace:this.colorSpace,minFilter:this.minFilter,magFilter:this.magFilter,anisotropy:this.anisotropy,flipY:this.flipY,generateMipmaps:this.generateMipmaps,premultiplyAlpha:this.premultiplyAlpha,unpackAlignment:this.unpackAlignment};return Object.keys(this.userData).length>0&&(n.userData=this.userData),t||(e.textures[this.uuid]=n),n}dispose(){this.dispatchEvent({type:`dispose`})}transformUv(e){if(this.mapping!==300)return e;if(e.applyMatrix3(this.matrix),e.x<0||e.x>1)switch(this.wrapS){case Gd:e.x-=Math.floor(e.x);break;case Kd:e.x=e.x<0?0:1;break;case qd:Math.abs(Math.floor(e.x)%2)===1?e.x=Math.ceil(e.x)-e.x:e.x-=Math.floor(e.x);break}if(e.y<0||e.y>1)switch(this.wrapT){case Gd:e.y-=Math.floor(e.y);break;case Kd:e.y=e.y<0?0:1;break;case qd:Math.abs(Math.floor(e.y)%2)===1?e.y=Math.ceil(e.y)-e.y:e.y-=Math.floor(e.y);break}return this.flipY&&(e.y=1-e.y),e}set needsUpdate(e){e===!0&&(this.version++,this.source.needsUpdate=!0)}set needsPMREMUpdate(e){e===!0&&this.pmremVersion++}};Em.DEFAULT_IMAGE=null,Em.DEFAULT_MAPPING=300,Em.DEFAULT_ANISOTROPY=1;var Dm=class e{static{e.prototype.isVector4=!0}constructor(e=0,t=0,n=0,r=1){this.x=e,this.y=t,this.z=n,this.w=r}get width(){return this.z}set width(e){this.z=e}get height(){return this.w}set height(e){this.w=e}set(e,t,n,r){return this.x=e,this.y=t,this.z=n,this.w=r,this}setScalar(e){return this.x=e,this.y=e,this.z=e,this.w=e,this}setX(e){return this.x=e,this}setY(e){return this.y=e,this}setZ(e){return this.z=e,this}setW(e){return this.w=e,this}setComponent(e,t){switch(e){case 0:this.x=t;break;case 1:this.y=t;break;case 2:this.z=t;break;case 3:this.w=t;break;default:throw Error(`THREE.Vector4: index is out of range: `+e)}return this}getComponent(e){switch(e){case 0:return this.x;case 1:return this.y;case 2:return this.z;case 3:return this.w;default:throw Error(`THREE.Vector4: index is out of range: `+e)}}clone(){return new this.constructor(this.x,this.y,this.z,this.w)}copy(e){return this.x=e.x,this.y=e.y,this.z=e.z,this.w=e.w===void 0?1:e.w,this}add(e){return this.x+=e.x,this.y+=e.y,this.z+=e.z,this.w+=e.w,this}addScalar(e){return this.x+=e,this.y+=e,this.z+=e,this.w+=e,this}addVectors(e,t){return this.x=e.x+t.x,this.y=e.y+t.y,this.z=e.z+t.z,this.w=e.w+t.w,this}addScaledVector(e,t){return this.x+=e.x*t,this.y+=e.y*t,this.z+=e.z*t,this.w+=e.w*t,this}sub(e){return this.x-=e.x,this.y-=e.y,this.z-=e.z,this.w-=e.w,this}subScalar(e){return this.x-=e,this.y-=e,this.z-=e,this.w-=e,this}subVectors(e,t){return this.x=e.x-t.x,this.y=e.y-t.y,this.z=e.z-t.z,this.w=e.w-t.w,this}multiply(e){return this.x*=e.x,this.y*=e.y,this.z*=e.z,this.w*=e.w,this}multiplyScalar(e){return this.x*=e,this.y*=e,this.z*=e,this.w*=e,this}applyMatrix4(e){let t=this.x,n=this.y,r=this.z,i=this.w,a=e.elements;return this.x=a[0]*t+a[4]*n+a[8]*r+a[12]*i,this.y=a[1]*t+a[5]*n+a[9]*r+a[13]*i,this.z=a[2]*t+a[6]*n+a[10]*r+a[14]*i,this.w=a[3]*t+a[7]*n+a[11]*r+a[15]*i,this}divide(e){return this.x/=e.x,this.y/=e.y,this.z/=e.z,this.w/=e.w,this}divideScalar(e){return this.multiplyScalar(1/e)}setAxisAngleFromQuaternion(e){this.w=2*Math.acos(e.w);let t=Math.sqrt(1-e.w*e.w);return t<1e-4?(this.x=1,this.y=0,this.z=0):(this.x=e.x/t,this.y=e.y/t,this.z=e.z/t),this}setAxisAngleFromRotationMatrix(e){let t,n,r,i,a=.01,o=.1,s=e.elements,c=s[0],l=s[4],u=s[8],d=s[1],f=s[5],p=s[9],m=s[2],h=s[6],g=s[10];if(Math.abs(l-d)s&&e>_?e_?s1);this.dispose()}this.viewport.set(0,0,e,t),this.scissor.set(0,0,e,t)}clone(){return new this.constructor().copy(this)}copy(e){this.width=e.width,this.height=e.height,this.depth=e.depth,this.scissor.copy(e.scissor),this.scissorTest=e.scissorTest,this.viewport.copy(e.viewport),this.textures.length=0;for(let t=0,n=e.textures.length;t>>0}enable(e){this.mask|=1<1){for(let e=0;e1){for(let e=0;e0&&(r.userData=this.userData),r.layers=this.layers.mask,r.matrix=this.matrix.toArray(),r.up=this.up.toArray(),this.pivot!==null&&(r.pivot=this.pivot.toArray()),this.morphTargetDictionary!==void 0&&(r.morphTargetDictionary=Object.assign({},this.morphTargetDictionary)),this.morphTargetInfluences!==void 0&&(r.morphTargetInfluences=this.morphTargetInfluences.slice()),this.isInstancedMesh&&(r.type=`InstancedMesh`,r.count=this.count,r.instanceMatrix=this.instanceMatrix.toJSON(),this.instanceColor!==null&&(r.instanceColor=this.instanceColor.toJSON())),this.isBatchedMesh&&(r.type=`BatchedMesh`,r.perObjectFrustumCulled=this.perObjectFrustumCulled,r.sortObjects=this.sortObjects,r.drawRanges=this._drawRanges,r.reservedRanges=this._reservedRanges,r.geometryInfo=this._geometryInfo.map(e=>({...e,boundingBox:e.boundingBox?e.boundingBox.toJSON():void 0,boundingSphere:e.boundingSphere?e.boundingSphere.toJSON():void 0})),r.instanceInfo=this._instanceInfo.map(e=>({...e})),r.availableInstanceIds=this._availableInstanceIds.slice(),r.availableGeometryIds=this._availableGeometryIds.slice(),r.nextIndexStart=this._nextIndexStart,r.nextVertexStart=this._nextVertexStart,r.geometryCount=this._geometryCount,r.maxInstanceCount=this._maxInstanceCount,r.maxVertexCount=this._maxVertexCount,r.maxIndexCount=this._maxIndexCount,r.geometryInitialized=this._geometryInitialized,r.matricesTexture=this._matricesTexture.toJSON(e),r.indirectTexture=this._indirectTexture.toJSON(e),this._colorsTexture!==null&&(r.colorsTexture=this._colorsTexture.toJSON(e)),this.boundingSphere!==null&&(r.boundingSphere=this.boundingSphere.toJSON()),this.boundingBox!==null&&(r.boundingBox=this.boundingBox.toJSON()));function i(t,n){return t[n.uuid]===void 0&&(t[n.uuid]=n.toJSON(e)),n.uuid}if(this.isScene)this.background&&(this.background.isColor?r.background=this.background.toJSON():this.background.isTexture&&(r.background=this.background.toJSON(e).uuid)),this.environment&&this.environment.isTexture&&this.environment.isRenderTargetTexture!==!0&&(r.environment=this.environment.toJSON(e).uuid);else if(this.isMesh||this.isLine||this.isPoints){r.geometry=i(e.geometries,this.geometry);let t=this.geometry.parameters;if(t!==void 0&&t.shapes!==void 0){let n=t.shapes;if(Array.isArray(n))for(let t=0,r=n.length;t0){r.children=[];for(let t=0;t0){r.animations=[];for(let t=0;t0&&(n.geometries=t),r.length>0&&(n.materials=r),i.length>0&&(n.textures=i),o.length>0&&(n.images=o),s.length>0&&(n.shapes=s),c.length>0&&(n.skeletons=c),l.length>0&&(n.animations=l),u.length>0&&(n.nodes=u)}return n.object=r,n;function a(e){let t=[];for(let n in e){let r=e[n];delete r.metadata,t.push(r)}return t}}clone(e){return new this.constructor().copy(this,e)}copy(e,t=!0){if(this.name=e.name,this.up.copy(e.up),this.position.copy(e.position),this.rotation.order=e.rotation.order,this.quaternion.copy(e.quaternion),this.scale.copy(e.scale),this.pivot=e.pivot===null?null:e.pivot.clone(),this.matrix.copy(e.matrix),this.matrixWorld.copy(e.matrixWorld),this.matrixAutoUpdate=e.matrixAutoUpdate,this.matrixWorldAutoUpdate=e.matrixWorldAutoUpdate,this.matrixWorldNeedsUpdate=e.matrixWorldNeedsUpdate,this.layers.mask=e.layers.mask,this.visible=e.visible,this.castShadow=e.castShadow,this.receiveShadow=e.receiveShadow,this.frustumCulled=e.frustumCulled,this.renderOrder=e.renderOrder,this.static=e.static,this.animations=e.animations.slice(),this.userData=JSON.parse(JSON.stringify(e.userData)),t===!0)for(let t=0;t.025?(c.inputState.pinching=!1,this.dispatchEvent({type:`pinchend`,handedness:e.handedness,target:this})):!c.inputState.pinching&&o<=.015&&(c.inputState.pinching=!0,this.dispatchEvent({type:`pinchstart`,handedness:e.handedness,target:this}))}else s!==null&&e.gripSpace&&(i=t.getPose(e.gripSpace,n),i!==null&&(s.matrix.fromArray(i.transform.matrix),s.matrix.decompose(s.position,s.rotation,s.scale),s.matrixWorldNeedsUpdate=!0,i.linearVelocity?(s.hasLinearVelocity=!0,s.linearVelocity.copy(i.linearVelocity)):s.hasLinearVelocity=!1,i.angularVelocity?(s.hasAngularVelocity=!0,s.angularVelocity.copy(i.angularVelocity)):s.hasAngularVelocity=!1,s.eventsEnabled&&s.dispatchEvent({type:`gripUpdated`,data:e,target:this})));o!==null&&(r=t.getPose(e.targetRaySpace,n),r===null&&i!==null&&(r=i),r!==null&&(o.matrix.fromArray(r.transform.matrix),o.matrix.decompose(o.position,o.rotation,o.scale),o.matrixWorldNeedsUpdate=!0,r.linearVelocity?(o.hasLinearVelocity=!0,o.linearVelocity.copy(r.linearVelocity)):o.hasLinearVelocity=!1,r.angularVelocity?(o.hasAngularVelocity=!0,o.angularVelocity.copy(r.angularVelocity)):o.hasAngularVelocity=!1,this.dispatchEvent(sh)))}return o!==null&&(o.visible=r!==null),s!==null&&(s.visible=i!==null),c!==null&&(c.visible=a!==null),this}_getHandJoint(e,t){if(e.joints[t.jointName]===void 0){let n=new oh;n.matrixAutoUpdate=!1,n.visible=!1,e.joints[t.jointName]=n,e.add(n)}return e.joints[t.jointName]}},lh={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074},uh={h:0,s:0,l:0},dh={h:0,s:0,l:0};function fh(e,t,n){return n<0&&(n+=1),n>1&&--n,n<1/6?e+(t-e)*6*n:n<1/2?t:n<2/3?e+(t-e)*6*(2/3-n):e}var Y=class{constructor(e,t,n){return this.isColor=!0,this.r=1,this.g=1,this.b=1,this.set(e,t,n)}set(e,t,n){if(t===void 0&&n===void 0){let t=e;t&&t.isColor?this.copy(t):typeof t==`number`?this.setHex(t):typeof t==`string`&&this.setStyle(t)}else this.setRGB(e,t,n);return this}setScalar(e){return this.r=e,this.g=e,this.b=e,this}setHex(e,t=hp){return e=Math.floor(e),this.r=(e>>16&255)/255,this.g=(e>>8&255)/255,this.b=(e&255)/255,gm.colorSpaceToWorking(this,t),this}setRGB(e,t,n,r=gm.workingColorSpace){return this.r=e,this.g=t,this.b=n,gm.colorSpaceToWorking(this,r),this}setHSL(e,t,n,r=gm.workingColorSpace){if(e=Hp(e,1),t=Vp(t,0,1),n=Vp(n,0,1),t===0)this.r=this.g=this.b=n;else{let r=n<=.5?n*(1+t):n+t-n*t,i=2*n-r;this.r=fh(i,r,e+1/3),this.g=fh(i,r,e),this.b=fh(i,r,e-1/3)}return gm.colorSpaceToWorking(this,r),this}setStyle(e,t=hp){function n(t){t!==void 0&&parseFloat(t)<1&&Ap(`Color: Alpha component of `+e+` will be ignored.`)}let r;if(r=/^(\w+)\(([^\)]*)\)/.exec(e)){let i,a=r[1],o=r[2];switch(a){case`rgb`:case`rgba`:if(i=/^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(i[4]),this.setRGB(Math.min(255,parseInt(i[1],10))/255,Math.min(255,parseInt(i[2],10))/255,Math.min(255,parseInt(i[3],10))/255,t);if(i=/^\s*(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(i[4]),this.setRGB(Math.min(100,parseInt(i[1],10))/100,Math.min(100,parseInt(i[2],10))/100,Math.min(100,parseInt(i[3],10))/100,t);break;case`hsl`:case`hsla`:if(i=/^\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\%\s*,\s*(\d*\.?\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(i[4]),this.setHSL(parseFloat(i[1])/360,parseFloat(i[2])/100,parseFloat(i[3])/100,t);break;default:Ap(`Color: Unknown color model `+e)}}else if(r=/^\#([A-Fa-f\d]+)$/.exec(e)){let n=r[1],i=n.length;if(i===3)return this.setRGB(parseInt(n.charAt(0),16)/15,parseInt(n.charAt(1),16)/15,parseInt(n.charAt(2),16)/15,t);if(i===6)return this.setHex(parseInt(n,16),t);Ap(`Color: Invalid hex color `+e)}else if(e&&e.length>0)return this.setColorName(e,t);return this}setColorName(e,t=hp){let n=lh[e.toLowerCase()];return n===void 0?Ap(`Color: Unknown color `+e):this.setHex(n,t),this}clone(){return new this.constructor(this.r,this.g,this.b)}copy(e){return this.r=e.r,this.g=e.g,this.b=e.b,this}copySRGBToLinear(e){return this.r=_m(e.r),this.g=_m(e.g),this.b=_m(e.b),this}copyLinearToSRGB(e){return this.r=vm(e.r),this.g=vm(e.g),this.b=vm(e.b),this}convertSRGBToLinear(){return this.copySRGBToLinear(this),this}convertLinearToSRGB(){return this.copyLinearToSRGB(this),this}getHex(e=hp){return gm.workingToColorSpace(ph.copy(this),e),Math.round(Vp(ph.r*255,0,255))*65536+Math.round(Vp(ph.g*255,0,255))*256+Math.round(Vp(ph.b*255,0,255))}getHexString(e=hp){return(`000000`+this.getHex(e).toString(16)).slice(-6)}getHSL(e,t=gm.workingColorSpace){gm.workingToColorSpace(ph.copy(this),t);let n=ph.r,r=ph.g,i=ph.b,a=Math.max(n,r,i),o=Math.min(n,r,i),s,c,l=(o+a)/2;if(o===a)s=0,c=0;else{let e=a-o;switch(c=l<=.5?e/(a+o):e/(2-a-o),a){case n:s=(r-i)/e+(r0?r.multiplyScalar(1/Math.sqrt(i)):r.set(0,0,0)}static getBarycoord(e,t,n,r,i){gh.subVectors(r,t),_h.subVectors(n,t),vh.subVectors(e,t);let a=gh.dot(gh),o=gh.dot(_h),s=gh.dot(vh),c=_h.dot(_h),l=_h.dot(vh),u=a*c-o*o;if(u===0)return i.set(0,0,0),null;let d=1/u,f=(c*s-o*l)*d,p=(a*l-o*s)*d;return i.set(1-f-p,p,f)}static containsPoint(e,t,n,r){return this.getBarycoord(e,t,n,r,yh)!==null&&yh.x>=0&&yh.y>=0&&yh.x+yh.y<=1}static getInterpolation(e,t,n,r,i,a,o,s){return this.getBarycoord(e,t,n,r,yh)===null?(s.x=0,s.y=0,`z`in s&&(s.z=0),`w`in s&&(s.w=0),null):(s.setScalar(0),s.addScaledVector(i,yh.x),s.addScaledVector(a,yh.y),s.addScaledVector(o,yh.z),s)}static getInterpolatedAttribute(e,t,n,r,i,a){return Eh.setScalar(0),Dh.setScalar(0),Oh.setScalar(0),Eh.fromBufferAttribute(e,t),Dh.fromBufferAttribute(e,n),Oh.fromBufferAttribute(e,r),a.setScalar(0),a.addScaledVector(Eh,i.x),a.addScaledVector(Dh,i.y),a.addScaledVector(Oh,i.z),a}static isFrontFacing(e,t,n,r){return gh.subVectors(n,t),_h.subVectors(e,t),gh.cross(_h).dot(r)<0}set(e,t,n){return this.a.copy(e),this.b.copy(t),this.c.copy(n),this}setFromPointsAndIndices(e,t,n,r){return this.a.copy(e[t]),this.b.copy(e[n]),this.c.copy(e[r]),this}setFromAttributeAndIndices(e,t,n,r){return this.a.fromBufferAttribute(e,t),this.b.fromBufferAttribute(e,n),this.c.fromBufferAttribute(e,r),this}clone(){return new this.constructor().copy(this)}copy(e){return this.a.copy(e.a),this.b.copy(e.b),this.c.copy(e.c),this}getArea(){return gh.subVectors(this.c,this.b),_h.subVectors(this.a,this.b),gh.cross(_h).length()*.5}getMidpoint(e){return e.addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)}getNormal(t){return e.getNormal(this.a,this.b,this.c,t)}getPlane(e){return e.setFromCoplanarPoints(this.a,this.b,this.c)}getBarycoord(t,n){return e.getBarycoord(t,this.a,this.b,this.c,n)}getInterpolation(t,n,r,i,a){return e.getInterpolation(t,this.a,this.b,this.c,n,r,i,a)}containsPoint(t){return e.containsPoint(t,this.a,this.b,this.c)}isFrontFacing(t){return e.isFrontFacing(this.a,this.b,this.c,t)}intersectsBox(e){return e.intersectsTriangle(this)}closestPointToPoint(e,t){let n=this.a,r=this.b,i=this.c,a,o;bh.subVectors(r,n),xh.subVectors(i,n),Ch.subVectors(e,n);let s=bh.dot(Ch),c=xh.dot(Ch);if(s<=0&&c<=0)return t.copy(n);wh.subVectors(e,r);let l=bh.dot(wh),u=xh.dot(wh);if(l>=0&&u<=l)return t.copy(r);let d=s*u-l*c;if(d<=0&&s>=0&&l<=0)return a=s/(s-l),t.copy(n).addScaledVector(bh,a);Th.subVectors(e,i);let f=bh.dot(Th),p=xh.dot(Th);if(p>=0&&f<=p)return t.copy(i);let m=f*c-s*p;if(m<=0&&c>=0&&p<=0)return o=c/(c-p),t.copy(n).addScaledVector(xh,o);let h=l*p-f*u;if(h<=0&&u-l>=0&&f-p>=0)return Sh.subVectors(i,r),o=(u-l)/(u-l+(f-p)),t.copy(r).addScaledVector(Sh,o);let g=1/(h+m+d);return a=m*g,o=d*g,t.copy(n).addScaledVector(bh,a).addScaledVector(xh,o)}equals(e){return e.a.equals(this.a)&&e.b.equals(this.b)&&e.c.equals(this.c)}},Ah=class{constructor(e=new J(1/0,1/0,1/0),t=new J(-1/0,-1/0,-1/0)){this.isBox3=!0,this.min=e,this.max=t}set(e,t){return this.min.copy(e),this.max.copy(t),this}setFromArray(e){this.makeEmpty();for(let t=0,n=e.length;t=this.min.x&&e.x<=this.max.x&&e.y>=this.min.y&&e.y<=this.max.y&&e.z>=this.min.z&&e.z<=this.max.z}containsBox(e){return this.min.x<=e.min.x&&e.max.x<=this.max.x&&this.min.y<=e.min.y&&e.max.y<=this.max.y&&this.min.z<=e.min.z&&e.max.z<=this.max.z}getParameter(e,t){return t.set((e.x-this.min.x)/(this.max.x-this.min.x),(e.y-this.min.y)/(this.max.y-this.min.y),(e.z-this.min.z)/(this.max.z-this.min.z))}intersectsBox(e){return e.max.x>=this.min.x&&e.min.x<=this.max.x&&e.max.y>=this.min.y&&e.min.y<=this.max.y&&e.max.z>=this.min.z&&e.min.z<=this.max.z}intersectsSphere(e){return this.clampPoint(e.center,Mh),Mh.distanceToSquared(e.center)<=e.radius*e.radius}intersectsPlane(e){let t,n;return e.normal.x>0?(t=e.normal.x*this.min.x,n=e.normal.x*this.max.x):(t=e.normal.x*this.max.x,n=e.normal.x*this.min.x),e.normal.y>0?(t+=e.normal.y*this.min.y,n+=e.normal.y*this.max.y):(t+=e.normal.y*this.max.y,n+=e.normal.y*this.min.y),e.normal.z>0?(t+=e.normal.z*this.min.z,n+=e.normal.z*this.max.z):(t+=e.normal.z*this.max.z,n+=e.normal.z*this.min.z),t<=-e.constant&&n>=-e.constant}intersectsTriangle(e){if(this.isEmpty())return!1;this.getCenter(Bh),Vh.subVectors(this.max,Bh),Ph.subVectors(e.a,Bh),Fh.subVectors(e.b,Bh),Ih.subVectors(e.c,Bh),Lh.subVectors(Fh,Ph),Rh.subVectors(Ih,Fh),zh.subVectors(Ph,Ih);let t=[0,-Lh.z,Lh.y,0,-Rh.z,Rh.y,0,-zh.z,zh.y,Lh.z,0,-Lh.x,Rh.z,0,-Rh.x,zh.z,0,-zh.x,-Lh.y,Lh.x,0,-Rh.y,Rh.x,0,-zh.y,zh.x,0];return!Wh(t,Ph,Fh,Ih,Vh)||(t=[1,0,0,0,1,0,0,0,1],!Wh(t,Ph,Fh,Ih,Vh))?!1:(Hh.crossVectors(Lh,Rh),t=[Hh.x,Hh.y,Hh.z],Wh(t,Ph,Fh,Ih,Vh))}clampPoint(e,t){return t.copy(e).clamp(this.min,this.max)}distanceToPoint(e){return this.clampPoint(e,Mh).distanceTo(e)}getBoundingSphere(e){return this.isEmpty()?e.makeEmpty():(this.getCenter(e.center),e.radius=this.getSize(Mh).length()*.5),e}intersect(e){return this.min.max(e.min),this.max.min(e.max),this.isEmpty()&&this.makeEmpty(),this}union(e){return this.min.min(e.min),this.max.max(e.max),this}applyMatrix4(e){return this.isEmpty()?this:(jh[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(e),jh[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(e),jh[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(e),jh[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(e),jh[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(e),jh[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(e),jh[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(e),jh[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(e),this.setFromPoints(jh),this)}translate(e){return this.min.add(e),this.max.add(e),this}equals(e){return e.min.equals(this.min)&&e.max.equals(this.max)}toJSON(){return{min:this.min.toArray(),max:this.max.toArray()}}fromJSON(e){return this.min.fromArray(e.min),this.max.fromArray(e.max),this}},jh=[new J,new J,new J,new J,new J,new J,new J,new J],Mh=new J,Nh=new Ah,Ph=new J,Fh=new J,Ih=new J,Lh=new J,Rh=new J,zh=new J,Bh=new J,Vh=new J,Hh=new J,Uh=new J;function Wh(e,t,n,r,i){for(let a=0,o=e.length-3;a<=o;a+=3){Uh.fromArray(e,a);let o=i.x*Math.abs(Uh.x)+i.y*Math.abs(Uh.y)+i.z*Math.abs(Uh.z),s=t.dot(Uh),c=n.dot(Uh),l=r.dot(Uh);if(Math.max(-Math.max(s,c,l),Math.min(s,c,l))>o)return!1}return!0}var Gh=new J,Kh=new q,qh=0,Jh=class extends Fp{constructor(e,t,n=!1){if(super(),Array.isArray(e))throw TypeError(`THREE.BufferAttribute: array should be a Typed Array.`);this.isBufferAttribute=!0,Object.defineProperty(this,"id",{value:qh++}),this.name=``,this.array=e,this.itemSize=t,this.count=e===void 0?0:e.length/t,this.normalized=n,this.usage=bp,this.updateRanges=[],this.gpuType=sf,this.version=0}onUploadCallback(){}set needsUpdate(e){e===!0&&this.version++}setUsage(e){return this.usage=e,this}addUpdateRange(e,t){this.updateRanges.push({start:e,count:t})}clearUpdateRanges(){this.updateRanges.length=0}copy(e){return this.name=e.name,this.array=new e.array.constructor(e.array),this.itemSize=e.itemSize,this.count=e.count,this.normalized=e.normalized,this.usage=e.usage,this.gpuType=e.gpuType,this}copyAt(e,t,n){e*=this.itemSize,n*=t.itemSize;for(let r=0,i=this.itemSize;rthis.radius*this.radius&&(t.sub(this.center).normalize(),t.multiplyScalar(this.radius).add(this.center)),t}getBoundingBox(e){return this.isEmpty()?(e.makeEmpty(),e):(e.set(this.center,this.center),e.expandByScalar(this.radius),e)}applyMatrix4(e){return this.center.applyMatrix4(e),this.radius*=e.getMaxScaleOnAxis(),this}translate(e){return this.center.add(e),this}expandByPoint(e){if(this.isEmpty())return this.center.copy(e),this.radius=0,this;$h.subVectors(e,this.center);let t=$h.lengthSq();if(t>this.radius*this.radius){let e=Math.sqrt(t),n=(e-this.radius)*.5;this.center.addScaledVector($h,n/e),this.radius+=n}return this}union(e){return e.isEmpty()?this:this.isEmpty()?(this.copy(e),this):(this.center.equals(e.center)===!0?this.radius=Math.max(this.radius,e.radius):(eg.subVectors(e.center,this.center).setLength(e.radius),this.expandByPoint($h.copy(e.center).add(eg)),this.expandByPoint($h.copy(e.center).sub(eg))),this)}equals(e){return e.center.equals(this.center)&&e.radius===this.radius}clone(){return new this.constructor().copy(this)}toJSON(){return{radius:this.radius,center:this.center.toArray()}}fromJSON(e){return this.radius=e.radius,this.center.fromArray(e.center),this}},ng=0,rg=new Mm,ig=new ah,ag=new J,og=new Ah,sg=new Ah,cg=new J,lg=class e extends Fp{constructor(){super(),this.isBufferGeometry=!0,Object.defineProperty(this,"id",{value:ng++}),this.uuid=Bp(),this.name=``,this.type=`BufferGeometry`,this.index=null,this.indirect=null,this.indirectOffset=0,this.attributes={},this.morphAttributes={},this.morphTargetsRelative=!1,this.groups=[],this.boundingBox=null,this.boundingSphere=null,this.drawRange={start:0,count:1/0},this.userData={},this._transformed=!1}getIndex(){return this.index}setIndex(e){return Array.isArray(e)?this.index=new(Cp(e)?Xh:Yh)(e,1):this.index=e,this}setIndirect(e,t=0){return this.indirect=e,this.indirectOffset=t,this}getIndirect(){return this.indirect}getAttribute(e){return this.attributes[e]}setAttribute(e,t){return this.attributes[e]=t,this}deleteAttribute(e){return delete this.attributes[e],this}hasAttribute(e){return this.attributes[e]!==void 0}addGroup(e,t,n=0){this.groups.push({start:e,count:t,materialIndex:n})}clearGroups(){this.groups=[]}setDrawRange(e,t){this.drawRange.start=e,this.drawRange.count=t}applyMatrix4(e){let t=this.attributes.position;t!==void 0&&(t.applyMatrix4(e),t.needsUpdate=!0);let n=this.attributes.normal;if(n!==void 0){let t=new dm().getNormalMatrix(e);n.applyNormalMatrix(t),n.needsUpdate=!0}let r=this.attributes.tangent;return r!==void 0&&(r.transformDirection(e),r.needsUpdate=!0),this.boundingBox!==null&&this.computeBoundingBox(),this.boundingSphere!==null&&this.computeBoundingSphere(),this._transformed=!0,this}applyQuaternion(e){return rg.makeRotationFromQuaternion(e),this.applyMatrix4(rg),this}rotateX(e){return rg.makeRotationX(e),this.applyMatrix4(rg),this}rotateY(e){return rg.makeRotationY(e),this.applyMatrix4(rg),this}rotateZ(e){return rg.makeRotationZ(e),this.applyMatrix4(rg),this}translate(e,t,n){return rg.makeTranslation(e,t,n),this.applyMatrix4(rg),this}scale(e,t,n){return rg.makeScale(e,t,n),this.applyMatrix4(rg),this}lookAt(e){return ig.lookAt(e),ig.updateMatrix(),this.applyMatrix4(ig.matrix),this}center(){return this.computeBoundingBox(),this.boundingBox.getCenter(ag).negate(),this.translate(ag.x,ag.y,ag.z),this}setFromPoints(e){let t=this.getAttribute(`position`);if(t===void 0){let t=[];for(let n=0,r=e.length;nt.count&&Ap(`BufferGeometry: Buffer size too small for points data. Use .dispose() and create a new geometry.`),t.needsUpdate=!0}return this}computeBoundingBox(){this.boundingBox===null&&(this.boundingBox=new Ah);let e=this.attributes.position,t=this.morphAttributes.position;if(e&&e.isGLBufferAttribute){jp(`BufferGeometry.computeBoundingBox(): GLBufferAttribute requires a manual bounding box.`,this),this.boundingBox.set(new J(-1/0,-1/0,-1/0),new J(1/0,1/0,1/0));return}if(e!==void 0){if(this.boundingBox.setFromBufferAttribute(e),t)for(let e=0,n=t.length;e0&&(e.userData=this.userData),this.parameters!==void 0&&this._transformed!==!0){let t=this.parameters;for(let n in t)t[n]!==void 0&&(e[n]=t[n]);return e}e.data={attributes:{}};let t=this.index;t!==null&&(e.data.index={type:t.array.constructor.name,array:Array.prototype.slice.call(t.array)});let n=this.attributes;for(let t in n){let r=n[t];e.data.attributes[t]=r.toJSON(e.data)}let r={},i=!1;for(let t in this.morphAttributes){let n=this.morphAttributes[t],a=[];for(let t=0,r=n.length;t0&&(r[t]=a,i=!0)}i&&(e.data.morphAttributes=r,e.data.morphTargetsRelative=this.morphTargetsRelative);let a=this.groups;a.length>0&&(e.data.groups=JSON.parse(JSON.stringify(a)));let o=this.boundingSphere;return o!==null&&(e.data.boundingSphere=o.toJSON()),e}clone(){return new this.constructor().copy(this)}copy(e){this.index=null,this.attributes={},this.morphAttributes={},this.groups=[],this.boundingBox=null,this.boundingSphere=null;let t={};this.name=e.name;let n=e.index;n!==null&&this.setIndex(n.clone());let r=e.attributes;for(let e in r){let n=r[e];this.setAttribute(e,n.clone(t))}let i=e.morphAttributes;for(let e in i){let n=[],r=i[e];for(let e=0,i=r.length;e1)?null:t.copy(e.start).addScaledVector(r,a)}intersectsLine(e){let t=this.distanceToPoint(e.start),n=this.distanceToPoint(e.end);return t<0&&n>0||n<0&&t>0}intersectsBox(e){return e.intersectsPlane(this)}intersectsSphere(e){return e.intersectsPlane(this)}coplanarPoint(e){return e.copy(this.normal).multiplyScalar(-this.constant)}applyMatrix4(e,t){let n=t||hg.getNormalMatrix(e),r=this.coplanarPoint(pg).applyMatrix4(e),i=this.normal.applyMatrix3(n).normalize();return this.constant=-r.dot(i),this}translate(e){return this.constant-=e.dot(this.normal),this}equals(e){return e.normal.equals(this.normal)&&e.constant===this.constant}clone(){return new this.constructor().copy(this)}toJSON(){return{normal:this.normal.toArray(),constant:this.constant}}fromJSON(e){return this.normal.fromArray(e.normal),this.constant=e.constant,this}},_g=0,vg=class extends Fp{constructor(){super(),this.isMaterial=!0,Object.defineProperty(this,"id",{value:_g++}),this.uuid=Bp(),this.name=``,this.type=`Material`,this.blending=1,this.side=0,this.vertexColors=!1,this.opacity=1,this.transparent=!1,this.alphaHash=!1,this.blendSrc=204,this.blendDst=205,this.blendEquation=100,this.blendSrcAlpha=null,this.blendDstAlpha=null,this.blendEquationAlpha=null,this.blendColor=new Y(0,0,0),this.blendAlpha=0,this.depthFunc=3,this.depthTest=!0,this.depthWrite=!0,this.stencilWriteMask=255,this.stencilFunc=519,this.stencilRef=0,this.stencilFuncMask=255,this.stencilFail=yp,this.stencilZFail=yp,this.stencilZPass=yp,this.stencilWrite=!1,this.clippingPlanes=null,this.clipIntersection=!1,this.clipShadows=!1,this.shadowSide=null,this.colorWrite=!0,this.precision=null,this.polygonOffset=!1,this.polygonOffsetFactor=0,this.polygonOffsetUnits=0,this.dithering=!1,this.alphaToCoverage=!1,this.premultipliedAlpha=!1,this.forceSinglePass=!1,this.allowOverride=!0,this.visible=!0,this.toneMapped=!0,this.userData={},this.version=0,this._alphaTest=0}get alphaTest(){return this._alphaTest}set alphaTest(e){this._alphaTest>0!=e>0&&this.version++,this._alphaTest=e}onBeforeRender(){}onBeforeCompile(){}customProgramCacheKey(){return this.onBeforeCompile.toString()}setValues(e){if(e!==void 0)for(let t in e){let n=e[t];if(n===void 0){Ap(`Material: parameter '${t}' has value of undefined.`);continue}let r=this[t];if(r===void 0){Ap(`Material: '${t}' is not a property of THREE.${this.type}.`);continue}r&&r.isColor?r.set(n):r&&r.isVector2&&n&&n.isVector2||r&&r.isEuler&&n&&n.isEuler||r&&r.isVector3&&n&&n.isVector3?r.copy(n):this[t]=n}}toJSON(e){let t=e===void 0||typeof e==`string`;t&&(e={textures:{},images:{}});let n={metadata:{version:4.7,type:`Material`,generator:`Material.toJSON`}};n.uuid=this.uuid,n.type=this.type,n.blending=this.blending,n.side=this.side,n.shadowSide=this.shadowSide,n.vertexColors=this.vertexColors,n.opacity=this.opacity,n.transparent=this.transparent,n.blendSrc=this.blendSrc,n.blendDst=this.blendDst,n.blendEquation=this.blendEquation,n.blendSrcAlpha=this.blendSrcAlpha,n.blendDstAlpha=this.blendDstAlpha,n.blendEquationAlpha=this.blendEquationAlpha,n.blendColor=this.blendColor.getHex(),n.blendAlpha=this.blendAlpha,n.depthFunc=this.depthFunc,n.depthTest=this.depthTest,n.depthWrite=this.depthWrite,n.colorWrite=this.colorWrite,n.clipIntersection=this.clipIntersection,n.clipShadows=this.clipShadows,n.stencilWriteMask=this.stencilWriteMask,n.stencilFunc=this.stencilFunc,n.stencilRef=this.stencilRef,n.stencilFuncMask=this.stencilFuncMask,n.stencilFail=this.stencilFail,n.stencilZFail=this.stencilZFail,n.stencilZPass=this.stencilZPass,n.stencilWrite=this.stencilWrite,n.polygonOffset=this.polygonOffset,n.polygonOffsetFactor=this.polygonOffsetFactor,n.polygonOffsetUnits=this.polygonOffsetUnits,n.dithering=this.dithering,n.alphaTest=this.alphaTest,n.alphaHash=this.alphaHash,n.alphaToCoverage=this.alphaToCoverage,n.premultipliedAlpha=this.premultipliedAlpha,n.forceSinglePass=this.forceSinglePass,n.allowOverride=this.allowOverride,n.visible=this.visible,n.toneMapped=this.toneMapped,n.name=this.name,this.color&&this.color.isColor&&(n.color=this.color.getHex()),this.roughness!==void 0&&(n.roughness=this.roughness),this.metalness!==void 0&&(n.metalness=this.metalness),this.sheen!==void 0&&(n.sheen=this.sheen),this.sheenColor&&this.sheenColor.isColor&&(n.sheenColor=this.sheenColor.getHex()),this.sheenRoughness!==void 0&&(n.sheenRoughness=this.sheenRoughness),this.emissive&&this.emissive.isColor&&(n.emissive=this.emissive.getHex()),this.emissiveIntensity!==void 0&&(n.emissiveIntensity=this.emissiveIntensity),this.specular&&this.specular.isColor&&(n.specular=this.specular.getHex()),this.specularIntensity!==void 0&&(n.specularIntensity=this.specularIntensity),this.specularColor&&this.specularColor.isColor&&(n.specularColor=this.specularColor.getHex()),this.shininess!==void 0&&(n.shininess=this.shininess),this.clearcoat!==void 0&&(n.clearcoat=this.clearcoat),this.clearcoatRoughness!==void 0&&(n.clearcoatRoughness=this.clearcoatRoughness),this.clearcoatMap&&this.clearcoatMap.isTexture&&(n.clearcoatMap=this.clearcoatMap.toJSON(e).uuid),this.clearcoatRoughnessMap&&this.clearcoatRoughnessMap.isTexture&&(n.clearcoatRoughnessMap=this.clearcoatRoughnessMap.toJSON(e).uuid),this.clearcoatNormalMap&&this.clearcoatNormalMap.isTexture&&(n.clearcoatNormalMap=this.clearcoatNormalMap.toJSON(e).uuid,n.clearcoatNormalScale=this.clearcoatNormalScale.toArray()),this.sheenColorMap&&this.sheenColorMap.isTexture&&(n.sheenColorMap=this.sheenColorMap.toJSON(e).uuid),this.sheenRoughnessMap&&this.sheenRoughnessMap.isTexture&&(n.sheenRoughnessMap=this.sheenRoughnessMap.toJSON(e).uuid),this.dispersion!==void 0&&(n.dispersion=this.dispersion),this.retroreflectivity!==void 0&&(n.retroreflectivity=this.retroreflectivity),this.iridescence!==void 0&&(n.iridescence=this.iridescence),this.iridescenceIOR!==void 0&&(n.iridescenceIOR=this.iridescenceIOR),this.iridescenceThicknessRange!==void 0&&(n.iridescenceThicknessRange=this.iridescenceThicknessRange),this.iridescenceMap&&this.iridescenceMap.isTexture&&(n.iridescenceMap=this.iridescenceMap.toJSON(e).uuid),this.iridescenceThicknessMap&&this.iridescenceThicknessMap.isTexture&&(n.iridescenceThicknessMap=this.iridescenceThicknessMap.toJSON(e).uuid),this.anisotropy!==void 0&&(n.anisotropy=this.anisotropy),this.anisotropyRotation!==void 0&&(n.anisotropyRotation=this.anisotropyRotation),this.anisotropyMap&&this.anisotropyMap.isTexture&&(n.anisotropyMap=this.anisotropyMap.toJSON(e).uuid),this.map&&this.map.isTexture&&(n.map=this.map.toJSON(e).uuid),this.matcap&&this.matcap.isTexture&&(n.matcap=this.matcap.toJSON(e).uuid),this.alphaMap&&this.alphaMap.isTexture&&(n.alphaMap=this.alphaMap.toJSON(e).uuid),this.lightMap&&this.lightMap.isTexture&&(n.lightMap=this.lightMap.toJSON(e).uuid,n.lightMapIntensity=this.lightMapIntensity),this.aoMap&&this.aoMap.isTexture&&(n.aoMap=this.aoMap.toJSON(e).uuid,n.aoMapIntensity=this.aoMapIntensity),this.bumpMap&&this.bumpMap.isTexture&&(n.bumpMap=this.bumpMap.toJSON(e).uuid,n.bumpScale=this.bumpScale),this.normalMap&&this.normalMap.isTexture&&(n.normalMap=this.normalMap.toJSON(e).uuid,n.normalMapType=this.normalMapType,n.normalScale=this.normalScale.toArray()),this.displacementMap&&this.displacementMap.isTexture&&(n.displacementMap=this.displacementMap.toJSON(e).uuid,n.displacementScale=this.displacementScale,n.displacementBias=this.displacementBias),this.roughnessMap&&this.roughnessMap.isTexture&&(n.roughnessMap=this.roughnessMap.toJSON(e).uuid),this.metalnessMap&&this.metalnessMap.isTexture&&(n.metalnessMap=this.metalnessMap.toJSON(e).uuid),this.emissiveMap&&this.emissiveMap.isTexture&&(n.emissiveMap=this.emissiveMap.toJSON(e).uuid),this.specularMap&&this.specularMap.isTexture&&(n.specularMap=this.specularMap.toJSON(e).uuid),this.specularIntensityMap&&this.specularIntensityMap.isTexture&&(n.specularIntensityMap=this.specularIntensityMap.toJSON(e).uuid),this.specularColorMap&&this.specularColorMap.isTexture&&(n.specularColorMap=this.specularColorMap.toJSON(e).uuid),this.envMap&&this.envMap.isTexture&&(n.envMap=this.envMap.toJSON(e).uuid,this.combine!==void 0&&(n.combine=this.combine)),this.envMapRotation!==void 0&&(n.envMapRotation=this.envMapRotation.toArray()),this.envMapIntensity!==void 0&&(n.envMapIntensity=this.envMapIntensity),this.reflectivity!==void 0&&(n.reflectivity=this.reflectivity),this.refractionRatio!==void 0&&(n.refractionRatio=this.refractionRatio),this.gradientMap&&this.gradientMap.isTexture&&(n.gradientMap=this.gradientMap.toJSON(e).uuid),this.transmission!==void 0&&(n.transmission=this.transmission),this.transmissionMap&&this.transmissionMap.isTexture&&(n.transmissionMap=this.transmissionMap.toJSON(e).uuid),this.thickness!==void 0&&(n.thickness=this.thickness),this.thicknessMap&&this.thicknessMap.isTexture&&(n.thicknessMap=this.thicknessMap.toJSON(e).uuid),this.attenuationDistance!==void 0&&(n.attenuationDistance=this.attenuationDistance),this.attenuationColor!==void 0&&(n.attenuationColor=this.attenuationColor.getHex()),this.size!==void 0&&(n.size=this.size),this.sizeAttenuation!==void 0&&(n.sizeAttenuation=this.sizeAttenuation),Array.isArray(this.clippingPlanes)&&this.clippingPlanes.length>0&&(n.clippingPlanes=this.clippingPlanes.map(e=>e.toJSON())),this.rotation!==void 0&&(n.rotation=this.rotation),this.depthPacking!==void 0&&(n.depthPacking=this.depthPacking),this.linewidth!==void 0&&(n.linewidth=this.linewidth),this.linecap!==void 0&&(n.linecap=this.linecap),this.linejoin!==void 0&&(n.linejoin=this.linejoin),this.dashSize!==void 0&&(n.dashSize=this.dashSize),this.gapSize!==void 0&&(n.gapSize=this.gapSize),this.scale!==void 0&&(n.scale=this.scale),this.wireframe!==void 0&&(n.wireframe=this.wireframe),this.wireframeLinewidth!==void 0&&(n.wireframeLinewidth=this.wireframeLinewidth),this.wireframeLinecap!==void 0&&(n.wireframeLinecap=this.wireframeLinecap),this.wireframeLinejoin!==void 0&&(n.wireframeLinejoin=this.wireframeLinejoin),this.flatShading!==void 0&&(n.flatShading=this.flatShading),this.fog!==void 0&&(n.fog=this.fog),Object.keys(this.userData).length>0&&(n.userData=this.userData);function r(e){let t=[];for(let n in e){let r=e[n];delete r.metadata,t.push(r)}return t}if(t){let t=r(e.textures),i=r(e.images);t.length>0&&(n.textures=t),i.length>0&&(n.images=i)}return n}fromJSON(e,t){if(e.uuid!==void 0&&(this.uuid=e.uuid),e.name!==void 0&&(this.name=e.name),e.color!==void 0&&this.color!==void 0&&this.color.setHex(e.color),e.roughness!==void 0&&(this.roughness=e.roughness),e.metalness!==void 0&&(this.metalness=e.metalness),e.sheen!==void 0&&(this.sheen=e.sheen),e.sheenColor!==void 0&&(this.sheenColor=new Y().setHex(e.sheenColor)),e.sheenRoughness!==void 0&&(this.sheenRoughness=e.sheenRoughness),e.emissive!==void 0&&this.emissive!==void 0&&this.emissive.setHex(e.emissive),e.specular!==void 0&&this.specular!==void 0&&this.specular.setHex(e.specular),e.specularIntensity!==void 0&&(this.specularIntensity=e.specularIntensity),e.specularColor!==void 0&&this.specularColor!==void 0&&this.specularColor.setHex(e.specularColor),e.shininess!==void 0&&(this.shininess=e.shininess),e.clearcoat!==void 0&&(this.clearcoat=e.clearcoat),e.clearcoatRoughness!==void 0&&(this.clearcoatRoughness=e.clearcoatRoughness),e.dispersion!==void 0&&(this.dispersion=e.dispersion),e.retroreflectivity!==void 0&&(this.retroreflectivity=e.retroreflectivity),e.iridescence!==void 0&&(this.iridescence=e.iridescence),e.iridescenceIOR!==void 0&&(this.iridescenceIOR=e.iridescenceIOR),e.iridescenceThicknessRange!==void 0&&(this.iridescenceThicknessRange=e.iridescenceThicknessRange),e.transmission!==void 0&&(this.transmission=e.transmission),e.thickness!==void 0&&(this.thickness=e.thickness),e.attenuationDistance!==void 0&&(this.attenuationDistance=e.attenuationDistance),e.attenuationColor!==void 0&&this.attenuationColor!==void 0&&this.attenuationColor.setHex(e.attenuationColor),e.anisotropy!==void 0&&(this.anisotropy=e.anisotropy),e.anisotropyRotation!==void 0&&(this.anisotropyRotation=e.anisotropyRotation),e.fog!==void 0&&(this.fog=e.fog),e.flatShading!==void 0&&(this.flatShading=e.flatShading),e.blending!==void 0&&(this.blending=e.blending),e.combine!==void 0&&(this.combine=e.combine),e.side!==void 0&&(this.side=e.side),e.shadowSide!==void 0&&(this.shadowSide=e.shadowSide),e.opacity!==void 0&&(this.opacity=e.opacity),e.transparent!==void 0&&(this.transparent=e.transparent),e.alphaTest!==void 0&&(this.alphaTest=e.alphaTest),e.alphaHash!==void 0&&(this.alphaHash=e.alphaHash),e.depthFunc!==void 0&&(this.depthFunc=e.depthFunc),e.depthTest!==void 0&&(this.depthTest=e.depthTest),e.depthWrite!==void 0&&(this.depthWrite=e.depthWrite),e.colorWrite!==void 0&&(this.colorWrite=e.colorWrite),e.clippingPlanes!==void 0&&(this.clippingPlanes=e.clippingPlanes.map(e=>new gg().fromJSON(e))),e.clipIntersection!==void 0&&(this.clipIntersection=e.clipIntersection),e.clipShadows!==void 0&&(this.clipShadows=e.clipShadows),e.depthPacking!==void 0&&(this.depthPacking=e.depthPacking),e.blendSrc!==void 0&&(this.blendSrc=e.blendSrc),e.blendDst!==void 0&&(this.blendDst=e.blendDst),e.blendEquation!==void 0&&(this.blendEquation=e.blendEquation),e.blendSrcAlpha!==void 0&&(this.blendSrcAlpha=e.blendSrcAlpha),e.blendDstAlpha!==void 0&&(this.blendDstAlpha=e.blendDstAlpha),e.blendEquationAlpha!==void 0&&(this.blendEquationAlpha=e.blendEquationAlpha),e.blendColor!==void 0&&this.blendColor!==void 0&&this.blendColor.setHex(e.blendColor),e.blendAlpha!==void 0&&(this.blendAlpha=e.blendAlpha),e.stencilWriteMask!==void 0&&(this.stencilWriteMask=e.stencilWriteMask),e.stencilFunc!==void 0&&(this.stencilFunc=e.stencilFunc),e.stencilRef!==void 0&&(this.stencilRef=e.stencilRef),e.stencilFuncMask!==void 0&&(this.stencilFuncMask=e.stencilFuncMask),e.stencilFail!==void 0&&(this.stencilFail=e.stencilFail),e.stencilZFail!==void 0&&(this.stencilZFail=e.stencilZFail),e.stencilZPass!==void 0&&(this.stencilZPass=e.stencilZPass),e.stencilWrite!==void 0&&(this.stencilWrite=e.stencilWrite),e.wireframe!==void 0&&(this.wireframe=e.wireframe),e.wireframeLinewidth!==void 0&&(this.wireframeLinewidth=e.wireframeLinewidth),e.wireframeLinecap!==void 0&&(this.wireframeLinecap=e.wireframeLinecap),e.wireframeLinejoin!==void 0&&(this.wireframeLinejoin=e.wireframeLinejoin),e.rotation!==void 0&&(this.rotation=e.rotation),e.linewidth!==void 0&&(this.linewidth=e.linewidth),e.linecap!==void 0&&(this.linecap=e.linecap),e.linejoin!==void 0&&(this.linejoin=e.linejoin),e.dashSize!==void 0&&(this.dashSize=e.dashSize),e.gapSize!==void 0&&(this.gapSize=e.gapSize),e.scale!==void 0&&(this.scale=e.scale),e.polygonOffset!==void 0&&(this.polygonOffset=e.polygonOffset),e.polygonOffsetFactor!==void 0&&(this.polygonOffsetFactor=e.polygonOffsetFactor),e.polygonOffsetUnits!==void 0&&(this.polygonOffsetUnits=e.polygonOffsetUnits),e.dithering!==void 0&&(this.dithering=e.dithering),e.alphaToCoverage!==void 0&&(this.alphaToCoverage=e.alphaToCoverage),e.premultipliedAlpha!==void 0&&(this.premultipliedAlpha=e.premultipliedAlpha),e.forceSinglePass!==void 0&&(this.forceSinglePass=e.forceSinglePass),e.allowOverride!==void 0&&(this.allowOverride=e.allowOverride),e.visible!==void 0&&(this.visible=e.visible),e.toneMapped!==void 0&&(this.toneMapped=e.toneMapped),e.userData!==void 0&&(this.userData=e.userData),e.vertexColors!==void 0&&(typeof e.vertexColors==`number`?this.vertexColors=e.vertexColors>0:this.vertexColors=e.vertexColors),e.size!==void 0&&(this.size=e.size),e.sizeAttenuation!==void 0&&(this.sizeAttenuation=e.sizeAttenuation),e.map!==void 0&&(this.map=t[e.map]||null),e.matcap!==void 0&&(this.matcap=t[e.matcap]||null),e.alphaMap!==void 0&&(this.alphaMap=t[e.alphaMap]||null),e.bumpMap!==void 0&&(this.bumpMap=t[e.bumpMap]||null),e.bumpScale!==void 0&&(this.bumpScale=e.bumpScale),e.normalMap!==void 0&&(this.normalMap=t[e.normalMap]||null),e.normalMapType!==void 0&&(this.normalMapType=e.normalMapType),e.normalScale!==void 0){let t=e.normalScale;Array.isArray(t)===!1&&(t=[t,t]),this.normalScale=new q().fromArray(t)}return e.displacementMap!==void 0&&(this.displacementMap=t[e.displacementMap]||null),e.displacementScale!==void 0&&(this.displacementScale=e.displacementScale),e.displacementBias!==void 0&&(this.displacementBias=e.displacementBias),e.roughnessMap!==void 0&&(this.roughnessMap=t[e.roughnessMap]||null),e.metalnessMap!==void 0&&(this.metalnessMap=t[e.metalnessMap]||null),e.emissiveMap!==void 0&&(this.emissiveMap=t[e.emissiveMap]||null),e.emissiveIntensity!==void 0&&(this.emissiveIntensity=e.emissiveIntensity),e.specularMap!==void 0&&(this.specularMap=t[e.specularMap]||null),e.specularIntensityMap!==void 0&&(this.specularIntensityMap=t[e.specularIntensityMap]||null),e.specularColorMap!==void 0&&(this.specularColorMap=t[e.specularColorMap]||null),e.envMap!==void 0&&(this.envMap=t[e.envMap]||null),e.envMapRotation!==void 0&&this.envMapRotation.fromArray(e.envMapRotation),e.envMapIntensity!==void 0&&(this.envMapIntensity=e.envMapIntensity),e.reflectivity!==void 0&&(this.reflectivity=e.reflectivity),e.refractionRatio!==void 0&&(this.refractionRatio=e.refractionRatio),e.lightMap!==void 0&&(this.lightMap=t[e.lightMap]||null),e.lightMapIntensity!==void 0&&(this.lightMapIntensity=e.lightMapIntensity),e.aoMap!==void 0&&(this.aoMap=t[e.aoMap]||null),e.aoMapIntensity!==void 0&&(this.aoMapIntensity=e.aoMapIntensity),e.gradientMap!==void 0&&(this.gradientMap=t[e.gradientMap]||null),e.clearcoatMap!==void 0&&(this.clearcoatMap=t[e.clearcoatMap]||null),e.clearcoatRoughnessMap!==void 0&&(this.clearcoatRoughnessMap=t[e.clearcoatRoughnessMap]||null),e.clearcoatNormalMap!==void 0&&(this.clearcoatNormalMap=t[e.clearcoatNormalMap]||null),e.clearcoatNormalScale!==void 0&&(this.clearcoatNormalScale=new q().fromArray(e.clearcoatNormalScale)),e.iridescenceMap!==void 0&&(this.iridescenceMap=t[e.iridescenceMap]||null),e.iridescenceThicknessMap!==void 0&&(this.iridescenceThicknessMap=t[e.iridescenceThicknessMap]||null),e.transmissionMap!==void 0&&(this.transmissionMap=t[e.transmissionMap]||null),e.thicknessMap!==void 0&&(this.thicknessMap=t[e.thicknessMap]||null),e.anisotropyMap!==void 0&&(this.anisotropyMap=t[e.anisotropyMap]||null),e.sheenColorMap!==void 0&&(this.sheenColorMap=t[e.sheenColorMap]||null),e.sheenRoughnessMap!==void 0&&(this.sheenRoughnessMap=t[e.sheenRoughnessMap]||null),this}clone(){return new this.constructor().copy(this)}copy(e){this.name=e.name,this.blending=e.blending,this.side=e.side,this.vertexColors=e.vertexColors,this.opacity=e.opacity,this.transparent=e.transparent,this.blendSrc=e.blendSrc,this.blendDst=e.blendDst,this.blendEquation=e.blendEquation,this.blendSrcAlpha=e.blendSrcAlpha,this.blendDstAlpha=e.blendDstAlpha,this.blendEquationAlpha=e.blendEquationAlpha,this.blendColor.copy(e.blendColor),this.blendAlpha=e.blendAlpha,this.depthFunc=e.depthFunc,this.depthTest=e.depthTest,this.depthWrite=e.depthWrite,this.stencilWriteMask=e.stencilWriteMask,this.stencilFunc=e.stencilFunc,this.stencilRef=e.stencilRef,this.stencilFuncMask=e.stencilFuncMask,this.stencilFail=e.stencilFail,this.stencilZFail=e.stencilZFail,this.stencilZPass=e.stencilZPass,this.stencilWrite=e.stencilWrite;let t=e.clippingPlanes,n=null;if(t!==null){let e=t.length;n=Array(e);for(let r=0;r!==e;++r)n[r]=t[r].clone()}return this.clippingPlanes=n,this.clipIntersection=e.clipIntersection,this.clipShadows=e.clipShadows,this.shadowSide=e.shadowSide,this.colorWrite=e.colorWrite,this.precision=e.precision,this.polygonOffset=e.polygonOffset,this.polygonOffsetFactor=e.polygonOffsetFactor,this.polygonOffsetUnits=e.polygonOffsetUnits,this.dithering=e.dithering,this.alphaTest=e.alphaTest,this.alphaHash=e.alphaHash,this.alphaToCoverage=e.alphaToCoverage,this.premultipliedAlpha=e.premultipliedAlpha,this.forceSinglePass=e.forceSinglePass,this.allowOverride=e.allowOverride,this.visible=e.visible,this.toneMapped=e.toneMapped,this.userData=JSON.parse(JSON.stringify(e.userData)),this}dispose(){this.dispatchEvent({type:`dispose`})}set needsUpdate(e){e===!0&&this.version++}},yg=new J,bg=new J,xg=new J,Sg=new J,Cg=class{constructor(e=new J,t=new J(0,0,-1)){this.origin=e,this.direction=t}set(e,t){return this.origin.copy(e),this.direction.copy(t),this}copy(e){return this.origin.copy(e.origin),this.direction.copy(e.direction),this}at(e,t){return t.copy(this.origin).addScaledVector(this.direction,e)}lookAt(e){return this.direction.copy(e).sub(this.origin).normalize(),this}recast(e){return this.origin.copy(this.at(e,yg)),this}closestPointToPoint(e,t){t.subVectors(e,this.origin);let n=t.dot(this.direction);return n<0?t.copy(this.origin):t.copy(this.origin).addScaledVector(this.direction,n)}distanceToPoint(e){return Math.sqrt(this.distanceSqToPoint(e))}distanceSqToPoint(e){let t=yg.subVectors(e,this.origin).dot(this.direction);return t<0?this.origin.distanceToSquared(e):(yg.copy(this.origin).addScaledVector(this.direction,t),yg.distanceToSquared(e))}distanceSqToSegment(e,t,n,r){bg.copy(e).add(t).multiplyScalar(.5),xg.copy(t).sub(e).normalize(),Sg.copy(this.origin).sub(bg);let i=e.distanceTo(t)*.5,a=-this.direction.dot(xg),o=Sg.dot(this.direction),s=-Sg.dot(xg),c=Sg.lengthSq(),l=Math.abs(1-a*a),u,d,f,p;if(l>0)if(u=a*s-o,d=a*o-s,p=i*l,u>=0)if(d>=-p)if(d<=p){let e=1/l;u*=e,d*=e,f=u*(u+a*d+2*o)+d*(a*u+d+2*s)+c}else d=i,u=Math.max(0,-(a*d+o)),f=-u*u+d*(d+2*s)+c;else d=-i,u=Math.max(0,-(a*d+o)),f=-u*u+d*(d+2*s)+c;else d<=-p?(u=Math.max(0,-(-a*i+o)),d=u>0?-i:Math.min(Math.max(-i,-s),i),f=-u*u+d*(d+2*s)+c):d<=p?(u=0,d=Math.min(Math.max(-i,-s),i),f=d*(d+2*s)+c):(u=Math.max(0,-(a*i+o)),d=u>0?i:Math.min(Math.max(-i,-s),i),f=-u*u+d*(d+2*s)+c);else d=a>0?-i:i,u=Math.max(0,-(a*d+o)),f=-u*u+d*(d+2*s)+c;return n&&n.copy(this.origin).addScaledVector(this.direction,u),r&&r.copy(bg).addScaledVector(xg,d),f}intersectSphere(e,t){if(e.radius<0)return null;yg.subVectors(e.center,this.origin);let n=yg.dot(this.direction),r=yg.dot(yg)-n*n,i=e.radius*e.radius;if(r>i)return null;let a=Math.sqrt(i-r),o=n-a,s=n+a;return s<0?null:o<0?this.at(s,t):this.at(o,t)}intersectsSphere(e){return e.radius<0?!1:this.distanceSqToPoint(e.center)<=e.radius*e.radius}distanceToPlane(e){let t=e.normal.dot(this.direction);if(t===0)return e.distanceToPoint(this.origin)===0?0:null;let n=-(this.origin.dot(e.normal)+e.constant)/t;return n>=0?n:null}intersectPlane(e,t){let n=this.distanceToPlane(e);return n===null?null:this.at(n,t)}intersectsPlane(e){let t=e.distanceToPoint(this.origin);return t===0||e.normal.dot(this.direction)*t<0}intersectBox(e,t){let n,r,i,a,o,s,c=1/this.direction.x,l=1/this.direction.y,u=1/this.direction.z,d=this.origin;return c>=0?(n=(e.min.x-d.x)*c,r=(e.max.x-d.x)*c):(n=(e.max.x-d.x)*c,r=(e.min.x-d.x)*c),l>=0?(i=(e.min.y-d.y)*l,a=(e.max.y-d.y)*l):(i=(e.max.y-d.y)*l,a=(e.min.y-d.y)*l),n>a||i>r||((i>n||isNaN(n))&&(n=i),(a=0?(o=(e.min.z-d.z)*u,s=(e.max.z-d.z)*u):(o=(e.max.z-d.z)*u,s=(e.min.z-d.z)*u),n>s||o>r)||((o>n||n!==n)&&(n=o),(s=0?n:r,t)}intersectsBox(e){return this.intersectBox(e,yg)!==null}intersectTriangle(e,t,n,r,i){let a=this.origin,o=this.direction,s=o.x,c=o.y,l=o.z,u=e.x-a.x,d=e.y-a.y,f=e.z-a.z,p=t.x-a.x,m=t.y-a.y,h=t.z-a.z,g=n.x-a.x,_=n.y-a.y,v=n.z-a.z,y=Math.abs(s),b=Math.abs(c),x=Math.abs(l),S,C,w,T,E,D,O,ee,te,k,ne,A;if(y>=b&&y>=x?(w=s,D=u,te=p,A=g,s>=0?(S=c,C=l,T=d,E=f,O=m,ee=h,k=_,ne=v):(S=l,C=c,T=f,E=d,O=h,ee=m,k=v,ne=_)):b>=x?(w=c,D=d,te=m,A=_,c>=0?(S=l,C=s,T=f,E=u,O=h,ee=p,k=v,ne=g):(S=s,C=l,T=u,E=f,O=p,ee=h,k=g,ne=v)):(w=l,D=f,te=h,A=v,l>=0?(S=s,C=c,T=u,E=d,O=p,ee=m,k=g,ne=_):(S=c,C=s,T=d,E=u,O=m,ee=p,k=_,ne=g)),w===0)return null;let re=S/w,ie=C/w,ae=1/w,oe=T-re*D,j=E-ie*D,se=O-re*te,ce=ee-ie*te,le=k-re*A,ue=ne-ie*A,M=le*ce-ue*se,N=oe*ue-j*le,de=se*j-ce*oe;if(r){if(M<0||N<0||de<0)return null}else if((M<0||N<0||de<0)&&(M>0||N>0||de>0))return null;let fe=M+N+de;if(fe===0)return null;let pe=ae*(M*D+N*te+de*A);return(fe>0?pe<0:pe>0)?null:this.at(pe/fe,i)}applyMatrix4(e){return this.origin.applyMatrix4(e),this.direction.transformDirection(e),this}equals(e){return e.origin.equals(this.origin)&&e.direction.equals(this.direction)}clone(){return new this.constructor().copy(this)}},wg=class extends vg{constructor(e){super(),this.isMeshBasicMaterial=!0,this.type=`MeshBasicMaterial`,this.color=new Y(16777215),this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new Hm,this.combine=0,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap=`round`,this.wireframeLinejoin=`round`,this.fog=!0,this.setValues(e)}copy(e){return super.copy(e),this.color.copy(e.color),this.map=e.map,this.lightMap=e.lightMap,this.lightMapIntensity=e.lightMapIntensity,this.aoMap=e.aoMap,this.aoMapIntensity=e.aoMapIntensity,this.specularMap=e.specularMap,this.alphaMap=e.alphaMap,this.envMap=e.envMap,this.envMapRotation.copy(e.envMapRotation),this.combine=e.combine,this.reflectivity=e.reflectivity,this.refractionRatio=e.refractionRatio,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this.wireframeLinecap=e.wireframeLinecap,this.wireframeLinejoin=e.wireframeLinejoin,this.fog=e.fog,this}},Tg=new Mm,Eg=new Cg,Dg=new tg,Og=new J,kg=new J,Ag=new J,jg=new J,Mg=new J,Ng=new J,Pg=new J,Fg=new J,Ig=class extends ah{constructor(e=new lg,t=new wg){super(),this.isMesh=!0,this.type=`Mesh`,this.geometry=e,this.material=t,this.morphTargetDictionary=void 0,this.morphTargetInfluences=void 0,this.count=1,this.updateMorphTargets()}copy(e,t){return super.copy(e,t),e.morphTargetInfluences!==void 0&&(this.morphTargetInfluences=e.morphTargetInfluences.slice()),e.morphTargetDictionary!==void 0&&(this.morphTargetDictionary=Object.assign({},e.morphTargetDictionary)),this.material=Array.isArray(e.material)?e.material.slice():e.material,this.geometry=e.geometry,this}updateMorphTargets(){let e=this.geometry.morphAttributes,t=Object.keys(e);if(t.length>0){let n=e[t[0]];if(n!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let e=0,t=n.length;e(e.far-e.near)**2))&&(Tg.copy(i).invert(),Eg.copy(e.ray).applyMatrix4(Tg),!(n.boundingBox!==null&&Eg.intersectsBox(n.boundingBox)===!1)&&this._computeIntersections(e,t,Eg)))}_computeIntersections(e,t,n){let r,i=this.geometry,a=this.material,o=i.index,s=i.attributes.position,c=i.attributes.uv,l=i.attributes.uv1,u=i.attributes.normal,d=i.groups,f=i.drawRange;if(o!==null)if(Array.isArray(a))for(let i=0,s=d.length;in.far?null:{distance:l,point:Fg.clone(),object:e}}function Rg(e,t,n,r,i,a,o,s,c,l){e.getVertexPosition(s,kg),e.getVertexPosition(c,Ag),e.getVertexPosition(l,jg);let u=Lg(e,t,n,r,kg,Ag,jg,Pg);if(u){let e=new J;kh.getBarycoord(Pg,kg,Ag,jg,e),i&&(u.uv=kh.getInterpolatedAttribute(i,s,c,l,e,new q)),a&&(u.uv1=kh.getInterpolatedAttribute(a,s,c,l,e,new q)),o&&(u.normal=kh.getInterpolatedAttribute(o,s,c,l,e,new J),u.normal.dot(r.direction)>0&&u.normal.multiplyScalar(-1));let t={a:s,b:c,c:l,normal:new J,materialIndex:0};kh.getNormal(kg,Ag,jg,t.normal),u.face=t,u.barycoord=e}return u}var zg=new Dm,Bg=new Dm,Vg=new Dm,Hg=new Dm,Ug=new Mm,Wg=new J,Gg=new tg,Kg=new Mm,qg=new Cg,Jg=class extends Ig{constructor(e,t){super(e,t),this.isSkinnedMesh=!0,this.type=`SkinnedMesh`,this.bindMode=Wd,this.bindMatrix=new Mm,this.bindMatrixInverse=new Mm,this.boundingBox=null,this.boundingSphere=null}computeBoundingBox(){let e=this.geometry;this.boundingBox===null&&(this.boundingBox=new Ah),this.boundingBox.makeEmpty();let t=e.getAttribute(`position`);for(let e=0;e0?e.max.x:e.min.x,d_.y=r.normal.y>0?e.max.y:e.min.y,d_.z=r.normal.z>0?e.max.z:e.min.z,r.distanceToPoint(d_)<0)return!1}return!0}containsPoint(e){let t=this.planes;for(let n=0;n<6;n++)if(t[n].distanceToPoint(e)<0)return!1;return!0}clone(){return new this.constructor().copy(this)}},p_=class extends vg{constructor(e){super(),this.isLineBasicMaterial=!0,this.type=`LineBasicMaterial`,this.color=new Y(16777215),this.map=null,this.linewidth=1,this.linecap=`round`,this.linejoin=`round`,this.fog=!0,this.setValues(e)}copy(e){return super.copy(e),this.color.copy(e.color),this.map=e.map,this.linewidth=e.linewidth,this.linecap=e.linecap,this.linejoin=e.linejoin,this.fog=e.fog,this}},m_=new J,h_=new J,g_=new Mm,__=new Cg,v_=new tg,y_=new J,b_=new J,x_=class extends ah{constructor(e=new lg,t=new p_){super(),this.isLine=!0,this.type=`Line`,this.geometry=e,this.material=t,this.morphTargetDictionary=void 0,this.morphTargetInfluences=void 0,this.updateMorphTargets()}copy(e,t){return super.copy(e,t),this.material=Array.isArray(e.material)?e.material.slice():e.material,this.geometry=e.geometry,this}computeLineDistances(){let e=this.geometry;if(e.index===null){let t=e.attributes.position,n=[0];for(let e=1,r=t.count;e0){let n=e[t[0]];if(n!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let e=0,t=n.length;er)return;y_.applyMatrix4(e.matrixWorld);let c=t.ray.origin.distanceTo(y_);if(!(ct.far))return{distance:c,point:b_.clone().applyMatrix4(e.matrixWorld),index:o,face:null,faceIndex:null,barycoord:null,object:e}}var C_=new J,w_=new J,T_=class extends x_{constructor(e,t){super(e,t),this.isLineSegments=!0,this.type=`LineSegments`}computeLineDistances(){let e=this.geometry;if(e.index===null){let t=e.attributes.position,n=[];for(let e=0,r=t.count;e0){let n=e[t[0]];if(n!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let e=0,t=n.length;ei.far)return;a.push({distance:c,distanceToRay:Math.sqrt(s),point:n,index:t,face:null,faceIndex:null,barycoord:null,object:o})}}var P_=class extends Em{constructor(e=[],t=301,n,r,i,a,o,s,c,l){super(e,t,n,r,i,a,o,s,c,l),this.isCubeTexture=!0,this.flipY=!1}get images(){return this.image}set images(e){this.image=e}},F_=class extends Em{constructor(e,t,n,r,i,a,o,s,c){super(e,t,n,r,i,a,o,s,c),this.isCanvasTexture=!0,this.needsUpdate=!0}},I_=class extends Em{constructor(e,t,n=of,r,i,a,o=Jd,s=Jd,c,l=_f,u=1){if(l!==1026&&l!==1027)throw Error(`THREE.DepthTexture: format must be either THREE.DepthFormat or THREE.DepthStencilFormat`);super({width:e,height:t,depth:u},r,i,a,o,s,l,n,c),this.isDepthTexture=!0,this.flipY=!1,this.generateMipmaps=!1,this.compareFunction=null}copy(e){return super.copy(e),this.source=new Sm(Object.assign({},e.image)),this.compareFunction=e.compareFunction,this}toJSON(e){let t=super.toJSON(e);return t.compareFunction=this.compareFunction,t}},L_=class extends I_{constructor(e,t=of,n=301,r,i,a=Jd,o=Jd,s,c=_f){let l={width:e,height:e,depth:1},u=[l,l,l,l,l,l];super(e,e,t,n,r,i,a,o,s,c),this.image=u,this.isCubeDepthTexture=!0,this.isCubeTexture=!0}get images(){return this.image}set images(e){this.image=e}},R_=class extends Em{constructor(e=null){super(),this.sourceTexture=e,this.isExternalTexture=!0}copy(e){return super.copy(e),this.sourceTexture=e.sourceTexture,this}},z_=class e extends lg{constructor(e=1,t=1,n=1,r=1,i=1,a=1){super(),this.type=`BoxGeometry`,this.parameters={width:e,height:t,depth:n,widthSegments:r,heightSegments:i,depthSegments:a};let o=this;r=Math.floor(r),i=Math.floor(i),a=Math.floor(a);let s=[],c=[],l=[],u=[],d=0,f=0;p(`z`,`y`,`x`,-1,-1,n,t,e,a,i,0),p(`z`,`y`,`x`,1,-1,n,t,-e,a,i,1),p(`x`,`z`,`y`,1,1,e,n,t,r,a,2),p(`x`,`z`,`y`,1,-1,e,n,-t,r,a,3),p(`x`,`y`,`z`,1,-1,e,t,n,r,i,4),p(`x`,`y`,`z`,-1,-1,e,t,-n,r,i,5),this.setIndex(s),this.setAttribute(`position`,new Zh(c,3)),this.setAttribute(`normal`,new Zh(l,3)),this.setAttribute(`uv`,new Zh(u,2));function p(e,t,n,r,i,a,p,m,h,g,_){let v=a/h,y=p/g,b=a/2,x=p/2,S=m/2,C=h+1,w=g+1,T=0,E=0,D=new J;for(let a=0;a0?1:-1,l.push(D.x,D.y,D.z),u.push(s/h),u.push(1-a/g),T+=1}for(let e=0;e0){let e=(_-1)*m;for(let t=0;t0&&v(!0),t>0&&v(!1)),this.setIndex(l),this.setAttribute(`position`,new Zh(u,3)),this.setAttribute(`normal`,new Zh(d,3)),this.setAttribute(`uv`,new Zh(f,2));function _(){let a=new J,_=new J,v=0,y=(t-e)/n;for(let c=0;c<=i;c++){let l=[],g=c/i,v=g*(t-e)+e;for(let e=0;e<=r;e++){let t=e/r,i=t*s+o,c=Math.sin(i),m=Math.cos(i);_.x=v*c,_.y=-g*n+h,_.z=v*m,u.push(_.x,_.y,_.z),a.set(c,y,m).normalize(),d.push(a.x,a.y,a.z),f.push(t,1-g),l.push(p++)}m.push(l)}for(let n=0;n0||r!==0)&&(l.push(a,o,c),v+=3),(t>0||r!==i-1)&&(l.push(o,s,c),v+=3)}c.addGroup(g,v,0),g+=v}function v(n){let i=p,a=new q,m=new J,_=0,v=n===!0?e:t,y=n===!0?1:-1;for(let e=1;e<=r;e++)u.push(0,h*y,0),d.push(0,y,0),f.push(.5,.5),p++;let b=p;for(let e=0;e<=r;e++){let t=e/r*s+o,n=Math.cos(t),i=Math.sin(t);m.x=v*i,m.y=h*y,m.z=v*n,u.push(m.x,m.y,m.z),d.push(0,y,0),a.x=n*.5+.5,a.y=i*.5*y+.5,f.push(a.x,a.y),p++}for(let e=0;e.9&&Math.min(t,n,r)<.1&&(t<.2&&(a[e+0]+=1),n<.2&&(a[e+2]+=1),r<.2&&(a[e+4]+=1))}}function d(e){i.push(e.x,e.y,e.z)}function f(t,n){let r=t*3;n.x=e[r+0],n.y=e[r+1],n.z=e[r+2]}function p(){let e=new J,t=new J,n=new J,r=new J,o=new q,s=new q,c=new q;for(let l=0,u=0;l0)s=r-1;else{s=r;break}if(r=s,n[r]===a)return r/(i-1);let l=n[r],u=n[r+1]-l,d=(a-l)/u;return(r+d)/(i-1)}getTangent(e,t){let n=1e-4,r=e-n,i=e+n;r<0&&(r=0),i>1&&(i=1);let a=this.getPoint(r),o=this.getPoint(i),s=t||(a.isVector2?new q:new J);return s.copy(o).sub(a).normalize(),s}getTangentAt(e,t){let n=this.getUtoTmapping(e);return this.getTangent(n,t)}computeFrenetFrames(e,t=!1){let n=new J,r=[],i=[],a=[],o=new J,s=new Mm;for(let t=0;t<=e;t++){let n=t/e;r[t]=this.getTangentAt(n,new J)}i[0]=new J,a[0]=new J;let c=Number.MAX_VALUE,l=Math.abs(r[0].x),u=Math.abs(r[0].y),d=Math.abs(r[0].z);l<=c&&(c=l,n.set(1,0,0)),u<=c&&(c=u,n.set(0,1,0)),d<=c&&n.set(0,0,1),o.crossVectors(r[0],n).normalize(),i[0].crossVectors(r[0],o),a[0].crossVectors(r[0],i[0]);for(let t=1;t<=e;t++){if(i[t]=i[t-1].clone(),a[t]=a[t-1].clone(),o.crossVectors(r[t-1],r[t]),o.length()>2**-52){o.normalize();let e=Math.acos(Vp(r[t-1].dot(r[t]),-1,1));i[t].applyMatrix4(s.makeRotationAxis(o,e))}a[t].crossVectors(r[t],i[t])}if(t===!0){let t=Math.acos(Vp(i[0].dot(i[e]),-1,1));t/=e,r[0].dot(o.crossVectors(i[0],i[e]))>0&&(t=-t);for(let n=1;n<=e;n++)i[n].applyMatrix4(s.makeRotationAxis(r[n],t*n)),a[n].crossVectors(r[n],i[n])}return{tangents:r,normals:i,binormals:a}}clone(){return new this.constructor().copy(this)}copy(e){return this.arcLengthDivisions=e.arcLengthDivisions,this}toJSON(){let e={metadata:{version:4.7,type:`Curve`,generator:`Curve.toJSON`}};return e.arcLengthDivisions=this.arcLengthDivisions,e.type=this.type,e}fromJSON(e){return this.arcLengthDivisions=e.arcLengthDivisions,this}},K_=class extends G_{constructor(e=0,t=0,n=1,r=1,i=0,a=Math.PI*2,o=!1,s=0){super(),this.isEllipseCurve=!0,this.type=`EllipseCurve`,this.aX=e,this.aY=t,this.xRadius=n,this.yRadius=r,this.aStartAngle=i,this.aEndAngle=a,this.aClockwise=o,this.aRotation=s}getPoint(e,t=new q){let n=t,r=Math.PI*2,i=this.aEndAngle-this.aStartAngle,a=Math.abs(i)<2**-52;for(;i<0;)i+=r;for(;i>r;)i-=r;i<2**-52&&(i=a?0:r),this.aClockwise===!0&&!a&&(i===r?i=-r:i-=r);let o=this.aStartAngle+e*i,s=this.aX+this.xRadius*Math.cos(o),c=this.aY+this.yRadius*Math.sin(o);if(this.aRotation!==0){let e=Math.cos(this.aRotation),t=Math.sin(this.aRotation),n=s-this.aX,r=c-this.aY;s=n*e-r*t+this.aX,c=n*t+r*e+this.aY}return n.set(s,c)}copy(e){return super.copy(e),this.aX=e.aX,this.aY=e.aY,this.xRadius=e.xRadius,this.yRadius=e.yRadius,this.aStartAngle=e.aStartAngle,this.aEndAngle=e.aEndAngle,this.aClockwise=e.aClockwise,this.aRotation=e.aRotation,this}toJSON(){let e=super.toJSON();return e.aX=this.aX,e.aY=this.aY,e.xRadius=this.xRadius,e.yRadius=this.yRadius,e.aStartAngle=this.aStartAngle,e.aEndAngle=this.aEndAngle,e.aClockwise=this.aClockwise,e.aRotation=this.aRotation,e}fromJSON(e){return super.fromJSON(e),this.aX=e.aX,this.aY=e.aY,this.xRadius=e.xRadius,this.yRadius=e.yRadius,this.aStartAngle=e.aStartAngle,this.aEndAngle=e.aEndAngle,this.aClockwise=e.aClockwise,this.aRotation=e.aRotation,this}},q_=class extends K_{constructor(e,t,n,r,i,a){super(e,t,n,n,r,i,a),this.isArcCurve=!0,this.type=`ArcCurve`}};function J_(){let e=0,t=0,n=0,r=0;function i(i,a,o,s){e=i,t=o,n=-3*i+3*a-2*o-s,r=2*i-2*a+o+s}return{initCatmullRom:function(e,t,n,r,a){i(t,n,a*(n-e),a*(r-t))},initNonuniformCatmullRom:function(e,t,n,r,a,o,s){let c=(t-e)/a-(n-e)/(a+o)+(n-t)/o,l=(n-t)/o-(r-t)/(o+s)+(r-n)/s;c*=o,l*=o,i(t,n,c,l)},calc:function(i){let a=i*i,o=a*i;return e+t*i+n*a+r*o}}}var Y_=new J,X_=new J,Z_=new J_,Q_=new J_,$_=new J_,ev=class extends G_{constructor(e=[],t=!1,n=`centripetal`,r=.5){super(),this.isCatmullRomCurve3=!0,this.type=`CatmullRomCurve3`,this.points=e,this.closed=t,this.curveType=n,this.tension=r}getPoint(e,t=new J){let n=t,r=this.points,i=r.length,a=(i-+!this.closed)*e,o=Math.floor(a),s=a-o;this.closed?o+=o>0?0:(Math.floor(Math.abs(o)/i)+1)*i:s===0&&o===i-1&&(o=i-2,s=1);let c,l;this.closed||o>0?c=r[(o-1)%i]:(X_.subVectors(r[0],r[1]).add(r[0]),c=X_);let u=r[o%i],d=r[(o+1)%i];if(this.closed||o+2r.length-2?r.length-1:a+1],u=r[a>r.length-3?r.length-1:a+2];return n.set(tv(o,s.x,c.x,l.x,u.x),tv(o,s.y,c.y,l.y,u.y)),n}copy(e){super.copy(e),this.points=[];for(let t=0,n=e.points.length;t0)&&f.push(t,i,c),(e!==n-1||s0&&(t.defines=this.defines),t.vertexShader=this.vertexShader,t.fragmentShader=this.fragmentShader,t.lights=this.lights,t.clipping=this.clipping;let n={};for(let e in this.extensions)this.extensions[e]===!0&&(n[e]=!0);return Object.keys(n).length>0&&(t.extensions=n),t}fromJSON(e,t){if(super.fromJSON(e,t),e.uniforms!==void 0)for(let n in e.uniforms){let r=e.uniforms[n];switch(this.uniforms[n]={},r.type){case`t`:this.uniforms[n].value=t[r.value]||null;break;case`c`:this.uniforms[n].value=new Q().setHex(r.value);break;case`v2`:this.uniforms[n].value=new X().fromArray(r.value);break;case`v3`:this.uniforms[n].value=new Z().fromArray(r.value);break;case`v4`:this.uniforms[n].value=new ed().fromArray(r.value);break;case`m3`:this.uniforms[n].value=new Ru().fromArray(r.value);break;case`m4`:this.uniforms[n].value=new ad().fromArray(r.value);break;default:this.uniforms[n].value=r.value}}if(e.defines!==void 0&&(this.defines=e.defines),e.vertexShader!==void 0&&(this.vertexShader=e.vertexShader),e.fragmentShader!==void 0&&(this.fragmentShader=e.fragmentShader),e.glslVersion!==void 0&&(this.glslVersion=e.glslVersion),e.extensions!==void 0)for(let t in e.extensions)this.extensions[t]=e.extensions[t];return e.lights!==void 0&&(this.lights=e.lights),e.clipping!==void 0&&(this.clipping=e.clipping),this}},oh=class extends ah{constructor(e){super(e),this.isRawShaderMaterial=!0,this.type=`RawShaderMaterial`}},sh=class extends Kf{constructor(e){super(),this.isMeshStandardMaterial=!0,this.type=`MeshStandardMaterial`,this.defines={STANDARD:``},this.color=new Q(16777215),this.roughness=1,this.metalness=0,this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new Q(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new X(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.roughnessMap=null,this.metalnessMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new hd,this.envMapIntensity=1,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap=`round`,this.wireframeLinejoin=`round`,this.flatShading=!1,this.fog=!0,this.setValues(e)}copy(e){return super.copy(e),this.defines={STANDARD:``},this.color.copy(e.color),this.roughness=e.roughness,this.metalness=e.metalness,this.map=e.map,this.lightMap=e.lightMap,this.lightMapIntensity=e.lightMapIntensity,this.aoMap=e.aoMap,this.aoMapIntensity=e.aoMapIntensity,this.emissive.copy(e.emissive),this.emissiveMap=e.emissiveMap,this.emissiveIntensity=e.emissiveIntensity,this.bumpMap=e.bumpMap,this.bumpScale=e.bumpScale,this.normalMap=e.normalMap,this.normalMapType=e.normalMapType,this.normalScale.copy(e.normalScale),this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this.roughnessMap=e.roughnessMap,this.metalnessMap=e.metalnessMap,this.alphaMap=e.alphaMap,this.envMap=e.envMap,this.envMapRotation.copy(e.envMapRotation),this.envMapIntensity=e.envMapIntensity,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this.wireframeLinecap=e.wireframeLinecap,this.wireframeLinejoin=e.wireframeLinejoin,this.flatShading=e.flatShading,this.fog=e.fog,this}},ch=class extends sh{constructor(e){super(),this.isMeshPhysicalMaterial=!0,this.defines={STANDARD:``,PHYSICAL:``},this.type=`MeshPhysicalMaterial`,this.anisotropyRotation=0,this.anisotropyMap=null,this.clearcoatMap=null,this.clearcoatRoughness=0,this.clearcoatRoughnessMap=null,this.clearcoatNormalScale=new X(1,1),this.clearcoatNormalMap=null,this.ior=1.5,Object.defineProperty(this,"reflectivity",{get:function(){return pu(2.5*(this.ior-1)/(this.ior+1),0,1)},set:function(e){this.ior=(1+.4*e)/(1-.4*e)}}),this.iridescenceMap=null,this.iridescenceIOR=1.3,this.iridescenceThicknessRange=[100,400],this.iridescenceThicknessMap=null,this.sheenColor=new Q(0),this.sheenColorMap=null,this.sheenRoughness=1,this.sheenRoughnessMap=null,this.transmissionMap=null,this.thickness=0,this.thicknessMap=null,this.attenuationDistance=1/0,this.attenuationColor=new Q(1,1,1),this.specularIntensity=1,this.specularIntensityMap=null,this.specularColor=new Q(1,1,1),this.specularColorMap=null,this._anisotropy=0,this._clearcoat=0,this._dispersion=0,this._iridescence=0,this._retroreflectivity=0,this._sheen=0,this._transmission=0,this.setValues(e)}get anisotropy(){return this._anisotropy}set anisotropy(e){this._anisotropy>0!=e>0&&this.version++,this._anisotropy=e}get clearcoat(){return this._clearcoat}set clearcoat(e){this._clearcoat>0!=e>0&&this.version++,this._clearcoat=e}get iridescence(){return this._iridescence}set iridescence(e){this._iridescence>0!=e>0&&this.version++,this._iridescence=e}get dispersion(){return this._dispersion}set dispersion(e){this._dispersion>0!=e>0&&this.version++,this._dispersion=e}get retroreflectivity(){return this._retroreflectivity}set retroreflectivity(e){this._retroreflectivity>0!=e>0&&this.version++,this._retroreflectivity=e}get sheen(){return this._sheen}set sheen(e){this._sheen>0!=e>0&&this.version++,this._sheen=e}get transmission(){return this._transmission}set transmission(e){this._transmission>0!=e>0&&this.version++,this._transmission=e}copy(e){return super.copy(e),this.defines={STANDARD:``,PHYSICAL:``},this.anisotropy=e.anisotropy,this.anisotropyRotation=e.anisotropyRotation,this.anisotropyMap=e.anisotropyMap,this.clearcoat=e.clearcoat,this.clearcoatMap=e.clearcoatMap,this.clearcoatRoughness=e.clearcoatRoughness,this.clearcoatRoughnessMap=e.clearcoatRoughnessMap,this.clearcoatNormalMap=e.clearcoatNormalMap,this.clearcoatNormalScale.copy(e.clearcoatNormalScale),this.dispersion=e.dispersion,this.ior=e.ior,this.iridescence=e.iridescence,this.iridescenceMap=e.iridescenceMap,this.iridescenceIOR=e.iridescenceIOR,this.iridescenceThicknessRange=[...e.iridescenceThicknessRange],this.iridescenceThicknessMap=e.iridescenceThicknessMap,this.retroreflectivity=e.retroreflectivity,this.sheen=e.sheen,this.sheenColor.copy(e.sheenColor),this.sheenColorMap=e.sheenColorMap,this.sheenRoughness=e.sheenRoughness,this.sheenRoughnessMap=e.sheenRoughnessMap,this.transmission=e.transmission,this.transmissionMap=e.transmissionMap,this.thickness=e.thickness,this.thicknessMap=e.thicknessMap,this.attenuationDistance=e.attenuationDistance,this.attenuationColor.copy(e.attenuationColor),this.specularIntensity=e.specularIntensity,this.specularIntensityMap=e.specularIntensityMap,this.specularColor.copy(e.specularColor),this.specularColorMap=e.specularColorMap,this}},lh=class extends Kf{constructor(e){super(),this.isMeshDepthMaterial=!0,this.type=`MeshDepthMaterial`,this.depthPacking=Bl,this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.wireframe=!1,this.wireframeLinewidth=1,this.setValues(e)}copy(e){return super.copy(e),this.depthPacking=e.depthPacking,this.map=e.map,this.alphaMap=e.alphaMap,this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this}},uh=class extends Kf{constructor(e){super(),this.isMeshDistanceMaterial=!0,this.type=`MeshDistanceMaterial`,this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.setValues(e)}copy(e){return super.copy(e),this.map=e.map,this.alphaMap=e.alphaMap,this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this}};function dh(e,t){return!e||e.constructor===t?e:typeof t.BYTES_PER_ELEMENT==`number`?new t(e):Array.prototype.slice.call(e)}function fh(e){return e!==void 0&&e.inTangents!==void 0&&e.outTangents!==void 0}function ph(e){function t(t,n){return e[t]-e[n]}let n=e.length,r=Array(n);for(let e=0;e!==n;++e)r[e]=e;return r.sort(t),r}function mh(e,t,n){let r=e.length,i=new e.constructor(r);for(let a=0,o=0;o!==r;++a){let r=n[a]*t;for(let n=0;n!==t;++n)i[o++]=e[r+n]}return i}function hh(e,t,n,r){let i=1,a=e[0];for(;a!==void 0&&a[r]===void 0;)a=e[i++];if(a===void 0)return;let o=a[r];if(o!==void 0)if(Array.isArray(o))do o=a[r],o!==void 0&&(t.push(a.time),n.push(...o)),a=e[i++];while(a!==void 0);else if(o.toArray!==void 0)do o=a[r],o!==void 0&&(t.push(a.time),o.toArray(n,n.length)),a=e[i++];while(a!==void 0);else do o=a[r],o!==void 0&&(t.push(a.time),n.push(o)),a=e[i++];while(a!==void 0)}var gh=class{constructor(e,t,n,r){this.parameterPositions=e,this._cachedIndex=0,this.resultBuffer=r===void 0?new t.constructor(n):r,this.sampleValues=t,this.valueSize=n,this.settings=null,this.DefaultSettings_={}}evaluate(e){let t=this.parameterPositions,n=this._cachedIndex,r=t[n],i=t[n-1];validate_interval:{seek:{let a;linear_scan:{forward_scan:if(!(e=i)){let o=t[1];e=i)break seek}a=n,n=0;break linear_scan}break validate_interval}for(;n>>1;et;)--a;if(++a,i!==0||a!==r){i>=a&&(a=Math.max(a,1),i=a-1);let e=this.getValueSize();this.times=n.slice(i,a),this.values=this.values.slice(i*e,a*e)}return this}validate(){let e=!0,t=this.getValueSize();t-Math.floor(t)!==0&&(ru(`KeyframeTrack: Invalid value size in track.`,this),e=!1);let n=this.times,r=this.values,i=n.length;i===0&&(ru(`KeyframeTrack: Track is empty.`,this),e=!1);let a=null;for(let t=0;t!==i;t++){let r=n[t];if(typeof r==`number`&&isNaN(r)){ru(`KeyframeTrack: Time is not a valid number.`,this,t,r),e=!1;break}if(a!==null&&a>r){ru(`KeyframeTrack: Out of order keys.`,this,t,r,a),e=!1;break}a=r}if(r!==void 0&&Xl(r))for(let t=0,n=r.length;t!==n;++t){let n=r[t];if(isNaN(n)){ru(`KeyframeTrack: Value is not a valid number.`,this,t,n),e=!1;break}}return e}optimize(){let e=this.times.slice(),t=this.values.slice(),n=this.getValueSize(),r=this.getInterpolation()===Pl,i=e.length-1,a=1;for(let o=1;o0){e[a]=e[i];for(let e=i*n,r=a*n,o=0;o!==n;++o)t[r+o]=t[e+o];++a}return a===e.length?(this.times=e,this.values=t):(this.times=e.slice(0,a),this.values=t.slice(0,a*n)),this}clone(){let e=this.times.slice(),t=this.values.slice(),n=this.constructor,r=new n(this.name,e,t);return r.createInterpolant=this.createInterpolant,fh(this.settings)&&(r.settings={inTangents:this.settings.inTangents.slice(),outTangents:this.settings.outTangents.slice()}),r}};function Th(e,t){for(let n=0,r=e.length;n!==r;n+=2)e[n]*=t}wh.prototype.ValueTypeName=``,wh.prototype.TimeBufferType=Float32Array,wh.prototype.ValueBufferType=Float32Array,wh.prototype.DefaultInterpolation=Nl;var Eh=class extends wh{constructor(e,t,n){super(e,t,n)}};Eh.prototype.ValueTypeName=`bool`,Eh.prototype.ValueBufferType=Array,Eh.prototype.DefaultInterpolation=Ml,Eh.prototype.InterpolantFactoryMethodLinear=void 0,Eh.prototype.InterpolantFactoryMethodSmooth=void 0;var Dh=class extends wh{constructor(e,t,n,r){super(e,t,n,r)}};Dh.prototype.ValueTypeName=`color`;var Oh=class extends wh{constructor(e,t,n,r){super(e,t,n,r)}};Oh.prototype.ValueTypeName=`number`;var kh=class extends gh{constructor(e,t,n,r){super(e,t,n,r)}interpolate_(e,t,n,r){let i=this.resultBuffer,a=this.sampleValues,o=this.valueSize,s=(n-t)/(r-t),c=e*o;for(let e=c+o;c!==e;c+=4)Fu.slerpFlat(i,0,a,c-o,a,c,s);return i}},Ah=class extends wh{constructor(e,t,n,r){super(e,t,n,r)}InterpolantFactoryMethodLinear(e){return new kh(this.times,this.values,this.getValueSize(),e)}};Ah.prototype.ValueTypeName=`quaternion`,Ah.prototype.InterpolantFactoryMethodSmooth=void 0;var jh=class extends wh{constructor(e,t,n){super(e,t,n)}};jh.prototype.ValueTypeName=`string`,jh.prototype.ValueBufferType=Array,jh.prototype.DefaultInterpolation=Ml,jh.prototype.InterpolantFactoryMethodLinear=void 0,jh.prototype.InterpolantFactoryMethodSmooth=void 0;var Mh=class extends wh{constructor(e,t,n,r){super(e,t,n,r)}};Mh.prototype.ValueTypeName=`vector`;var Nh=class{constructor(e=``,t=-1,n=[],r=zl){this.name=e,this.tracks=n,this.duration=t,this.blendMode=r,this.uuid=fu(),this.userData={},this.duration<0&&this.resetDuration()}static parse(e){let t=[],n=e.tracks,r=1/(e.fps||1);for(let e=0,i=n.length;e!==i;++e)t.push(Fh(n[e]).scale(r));let i=new this(e.name,e.duration,t,e.blendMode);return i.uuid=e.uuid,i.userData=JSON.parse(e.userData||`{}`),i}static toJSON(e){let t=[],n=e.tracks,r={name:e.name,duration:e.duration,tracks:t,uuid:e.uuid,blendMode:e.blendMode,userData:JSON.stringify(e.userData)};for(let e=0,r=n.length;e!==r;++e)t.push(wh.toJSON(n[e]));return r}static CreateFromMorphTargetSequence(e,t,n,r){let i=t.length,a=[];for(let e=0;e1){let e=a[1],t=r[e];t||(r[e]=t=[]),t.push(n)}}let a=[];for(let e in r)a.push(this.CreateFromMorphTargetSequence(e,r[e],t,n));return a}resetDuration(){let e=this.tracks,t=0;for(let n=0,r=e.length;n!==r;++n){let e=this.tracks[n];t=Math.max(t,e.times[e.times.length-1])}return this.duration=t,this}trim(){for(let e=0;e{t&&t(i),this.manager.itemEnd(e)},0);return}if(Bh[e]!==void 0){Bh[e].push({onLoad:t,onProgress:n,onError:r});return}Bh[e]=[],Bh[e].push({onLoad:t,onProgress:n,onError:r});let a=new Request(e,{headers:new Headers(this.requestHeader),credentials:this.withCredentials?`include`:`same-origin`,signal:typeof AbortSignal.any==`function`?AbortSignal.any([this._abortController.signal,this.manager.abortController.signal]):this._abortController.signal}),o=this.mimeType,s=this.responseType;fetch(a).then(t=>{if(t.status===200||t.status===0){if(t.status===0&&nu(`FileLoader: HTTP Status 0 received.`),typeof ReadableStream>`u`||t.body===void 0||t.body.getReader===void 0)return t;let n=Bh[e],r=t.body.getReader(),i=t.headers.get(`X-File-Size`)||t.headers.get(`Content-Length`),a=i?parseInt(i):0,o=a!==0,s=0,c=new ReadableStream({start(e){t();function t(){r.read().then(({done:r,value:i})=>{if(r)e.close();else{s+=i.byteLength;let r=new ProgressEvent(`progress`,{lengthComputable:o,loaded:s,total:a});for(let e=0,t=n.length;e{e.error(t)})}}});return new Response(c)}else throw new Vh(`fetch for "${t.url}" responded with ${t.status}: ${t.statusText}`,t)}).then(e=>{switch(s){case`arraybuffer`:return e.arrayBuffer();case`blob`:return e.blob();case`document`:return e.text().then(e=>new DOMParser().parseFromString(e,o));case`json`:return e.json();default:if(o===``)return e.text();{let t=/charset="?([^;"\s]*)"?/i.exec(o),n=t&&t[1]?t[1].toLowerCase():void 0,r=new TextDecoder(n);return e.arrayBuffer().then(e=>r.decode(e))}}}).then(t=>{Ih.add(`file:${e}`,t);let n=Bh[e];delete Bh[e];for(let e=0,r=n.length;e{let n=Bh[e];if(n===void 0)throw this.manager.itemError(e),t;delete Bh[e];for(let e=0,r=n.length;e{this.manager.itemEnd(e)}),this.manager.itemStart(e)}setResponseType(e){return this.responseType=e,this}setMimeType(e){return this.mimeType=e,this}abort(){return this._abortController.abort(),this._abortController=new AbortController,this}},Uh=new WeakMap,Wh=class extends zh{constructor(e){super(e)}load(e,t,n,r){this.path!==void 0&&(e=this.path+e),e=this.manager.resolveURL(e);let i=this,a=Ih.get(`image:${e}`);if(a!==void 0){if(a.complete===!0)i.manager.itemStart(e),setTimeout(function(){t&&t(a),i.manager.itemEnd(e)},0);else{let e=Uh.get(a);e===void 0&&(e=[],Uh.set(a,e)),e.push({onLoad:t,onError:r})}return a}let o=Zl(`img`);function s(){l(),t&&t(this);let n=Uh.get(this)||[];for(let e=0;e`u`&&nu(`ImageBitmapLoader: createImageBitmap() not supported.`),typeof fetch>`u`&&nu(`ImageBitmapLoader: fetch() not supported.`),this.options={premultiplyAlpha:`none`},this._abortController=new AbortController}setOptions(e){return this.options=e,this}load(e,t,n,r){e===void 0&&(e=``),this.path!==void 0&&(e=this.path+e),e=this.manager.resolveURL(e);let i=this,a=Ih.get(`image-bitmap:${e}`);if(a!==void 0){if(i.manager.itemStart(e),a.then){a.then(n=>{mg.has(a)===!0?(r&&r(mg.get(a)),i.manager.itemError(e),i.manager.itemEnd(e)):(t&&t(n),i.manager.itemEnd(e))});return}setTimeout(function(){t&&t(a),i.manager.itemEnd(e)},0);return}let o={};o.credentials=this.crossOrigin===`anonymous`?`same-origin`:`include`,o.headers=this.requestHeader,o.signal=typeof AbortSignal.any==`function`?AbortSignal.any([this._abortController.signal,this.manager.abortController.signal]):this._abortController.signal;let s=fetch(e,o).then(function(e){return e.blob()}).then(function(e){return createImageBitmap(e,Object.assign({},i.options,{colorSpaceConversion:`none`}))}).then(function(n){return Ih.add(`image-bitmap:${e}`,n),t&&t(n),i.manager.itemEnd(e),n}).catch(function(t){r&&r(t),mg.set(s,t),Ih.remove(`image-bitmap:${e}`),i.manager.itemError(e),i.manager.itemEnd(e)});Ih.add(`image-bitmap:${e}`,s),i.manager.itemStart(e)}abort(){return this._abortController.abort(),this._abortController=new AbortController,this}},gg=-90,_g=1,vg=class extends Md{constructor(e,t,n){super(),this.type=`CubeCamera`,this.renderTarget=n,this.coordinateSystem=null,this.activeMipmapLevel=0;let r=new ag(gg,_g,e,t);r.layers=this.layers,this.add(r);let i=new ag(gg,_g,e,t);i.layers=this.layers,this.add(i);let a=new ag(gg,_g,e,t);a.layers=this.layers,this.add(a);let o=new ag(gg,_g,e,t);o.layers=this.layers,this.add(o);let s=new ag(gg,_g,e,t);s.layers=this.layers,this.add(s);let c=new ag(gg,_g,e,t);c.layers=this.layers,this.add(c)}updateCoordinateSystem(){let e=this.coordinateSystem,t=this.children.concat(),[n,r,i,a,o,s]=t;for(let e of t)this.remove(e);if(e===2e3)n.up.set(0,1,0),n.lookAt(1,0,0),r.up.set(0,1,0),r.lookAt(-1,0,0),i.up.set(0,0,-1),i.lookAt(0,1,0),a.up.set(0,0,1),a.lookAt(0,-1,0),o.up.set(0,1,0),o.lookAt(0,0,1),s.up.set(0,1,0),s.lookAt(0,0,-1);else if(e===2001)n.up.set(0,-1,0),n.lookAt(-1,0,0),r.up.set(0,-1,0),r.lookAt(1,0,0),i.up.set(0,0,1),i.lookAt(0,1,0),a.up.set(0,0,-1),a.lookAt(0,-1,0),o.up.set(0,-1,0),o.lookAt(0,0,1),s.up.set(0,-1,0),s.lookAt(0,0,-1);else throw Error(`THREE.CubeCamera.updateCoordinateSystem(): Invalid coordinate system: `+e);for(let e of t)this.add(e),e.updateMatrixWorld()}update(e,t){this.parent===null&&this.updateMatrixWorld();let{renderTarget:n,activeMipmapLevel:r}=this;this.coordinateSystem!==e.coordinateSystem&&(this.coordinateSystem=e.coordinateSystem,this.updateCoordinateSystem());let[i,a,o,s,c,l]=this.children,u=e.getRenderTarget(),d=e.getActiveCubeFace(),f=e.getActiveMipmapLevel(),p=e.xr.enabled;e.xr.enabled=!1;let m=n.texture.generateMipmaps;n.texture.generateMipmaps=!1;let h=!1;h=e.isWebGLRenderer===!0?e.state.buffers.depth.getReversed():e.reversedDepthBuffer,e.setRenderTarget(n,0,r),h&&e.autoClear===!1&&e.clearDepth(),e.render(t,i),e.setRenderTarget(n,1,r),h&&e.autoClear===!1&&e.clearDepth(),e.render(t,a),e.setRenderTarget(n,2,r),h&&e.autoClear===!1&&e.clearDepth(),e.render(t,o),e.setRenderTarget(n,3,r),h&&e.autoClear===!1&&e.clearDepth(),e.render(t,s),e.setRenderTarget(n,4,r),h&&e.autoClear===!1&&e.clearDepth(),e.render(t,c),n.texture.generateMipmaps=m,e.setRenderTarget(n,5,r),h&&e.autoClear===!1&&e.clearDepth(),e.render(t,l),e.setRenderTarget(u,d,f),e.xr.enabled=p,n.texture.needsPMREMUpdate=!0}},yg=class extends ag{constructor(e=[]){super(),this.isArrayCamera=!0,this.isMultiViewCamera=!1,this.cameras=e}},bg=class{constructor(){this._previousTime=0,this._currentTime=0,this._startTime=performance.now(),this._delta=0,this._elapsed=0,this._timescale=1,this._document=null,this._pageVisibilityHandler=null}connect(e){this._document=e,e.hidden!==void 0&&(this._pageVisibilityHandler=xg.bind(this),e.addEventListener(`visibilitychange`,this._pageVisibilityHandler,!1))}disconnect(){this._pageVisibilityHandler!==null&&(this._document.removeEventListener(`visibilitychange`,this._pageVisibilityHandler),this._pageVisibilityHandler=null),this._document=null}getDelta(){return this._delta/1e3}getElapsed(){return this._elapsed/1e3}getTimescale(){return this._timescale}setTimescale(e){return this._timescale=e,this}reset(){return this._currentTime=performance.now()-this._startTime,this}dispose(){this.disconnect()}update(e){return this._pageVisibilityHandler!==null&&this._document.hidden===!0?this._delta=0:(this._previousTime=this._currentTime,this._currentTime=(e===void 0?performance.now():e)-this._startTime,this._delta=(this._currentTime-this._previousTime)*this._timescale,this._elapsed+=this._delta),this}};function xg(){this._document.hidden===!1&&this.reset()}var Sg=`\\[\\]\\.:\\/`,Cg=RegExp(`[\\[\\]\\.:\\/]`,`g`),wg=`[^\\[\\]\\.:\\/]`,Tg=`[^`+Sg.replace(`\\.`,``)+`]`,Eg=`((?:WC+[\\/:])*)`.replace(`WC`,wg),Dg=`(WCOD+)?`.replace(`WCOD`,Tg),Og=`(?:\\.(WC+)(?:\\[(.+)\\])?)?`.replace(`WC`,wg),kg=`\\.(WC+)(?:\\[(.+)\\])?`.replace(`WC`,wg),Ag=RegExp(`^`+Eg+Dg+Og+kg+`$`),jg=[`material`,`materials`,`bones`,`map`],Mg=class{constructor(e,t,n){let r=n||Ng.parseTrackName(t);this._targetGroup=e,this._bindings=e.subscribe_(t,r)}getValue(e,t){this.bind();let n=this._targetGroup.nCachedObjects_,r=this._bindings[n];r!==void 0&&r.getValue(e,t)}setValue(e,t){let n=this._bindings;for(let r=this._targetGroup.nCachedObjects_,i=n.length;r!==i;++r)n[r].setValue(e,t)}bind(){let e=this._bindings;for(let t=this._targetGroup.nCachedObjects_,n=e.length;t!==n;++t)e[t].bind()}unbind(){let e=this._bindings;for(let t=this._targetGroup.nCachedObjects_,n=e.length;t!==n;++t)e[t].unbind()}},Ng=class e{constructor(t,n,r){this.path=n,this.parsedPath=r||e.parseTrackName(n),this.node=e.findNode(t,this.parsedPath.nodeName),this.rootNode=t,this.getValue=this._getValue_unbound,this.setValue=this._setValue_unbound}static create(t,n,r){return t&&t.isAnimationObjectGroup?new e.Composite(t,n,r):new e(t,n,r)}static sanitizeNodeName(e){return e.replace(/\s/g,`_`).replace(Cg,``)}static parseTrackName(e){let t=Ag.exec(e);if(t===null)throw Error(`THREE.PropertyBinding: Cannot parse trackName: `+e);let n={nodeName:t[2],objectName:t[3],objectIndex:t[4],propertyName:t[5],propertyIndex:t[6]},r=n.nodeName&&n.nodeName.lastIndexOf(`.`);if(r!==void 0&&r!==-1){let e=n.nodeName.substring(r+1);jg.indexOf(e)!==-1&&(n.nodeName=n.nodeName.substring(0,r),n.objectName=e)}if(n.propertyName===null||n.propertyName.length===0)throw Error(`THREE.PropertyBinding: can not parse propertyName from trackName: `+e);return n}static findNode(e,t){if(t===void 0||t===``||t===`.`||t===-1||t===e.name||t===e.uuid)return e;if(e.skeleton){let n=e.skeleton.getBoneByName(t);if(n!==void 0)return n}if(e.children){let n=function(e){for(let r=0;re.start-t.start);let t=0;for(let e=1;e0&&(t.defines=this.defines),t.vertexShader=this.vertexShader,t.fragmentShader=this.fragmentShader,t.lights=this.lights,t.clipping=this.clipping;let n={};for(let e in this.extensions)this.extensions[e]===!0&&(n[e]=!0);return Object.keys(n).length>0&&(t.extensions=n),t}fromJSON(e,t){if(super.fromJSON(e,t),e.uniforms!==void 0)for(let n in e.uniforms){let r=e.uniforms[n];switch(this.uniforms[n]={},r.type){case`t`:this.uniforms[n].value=t[r.value]||null;break;case`c`:this.uniforms[n].value=new Y().setHex(r.value);break;case`v2`:this.uniforms[n].value=new q().fromArray(r.value);break;case`v3`:this.uniforms[n].value=new J().fromArray(r.value);break;case`v4`:this.uniforms[n].value=new Dm().fromArray(r.value);break;case`m3`:this.uniforms[n].value=new dm().fromArray(r.value);break;case`m4`:this.uniforms[n].value=new Mm().fromArray(r.value);break;default:this.uniforms[n].value=r.value}}if(e.defines!==void 0&&(this.defines=e.defines),e.vertexShader!==void 0&&(this.vertexShader=e.vertexShader),e.fragmentShader!==void 0&&(this.fragmentShader=e.fragmentShader),e.glslVersion!==void 0&&(this.glslVersion=e.glslVersion),e.extensions!==void 0)for(let t in e.extensions)this.extensions[t]=e.extensions[t];return e.lights!==void 0&&(this.lights=e.lights),e.clipping!==void 0&&(this.clipping=e.clipping),this}},Nv=class extends Mv{constructor(e){super(e),this.isRawShaderMaterial=!0,this.type=`RawShaderMaterial`}},Pv=class extends vg{constructor(e){super(),this.isMeshStandardMaterial=!0,this.type=`MeshStandardMaterial`,this.defines={STANDARD:``},this.color=new Y(16777215),this.roughness=1,this.metalness=0,this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new Y(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new q(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.roughnessMap=null,this.metalnessMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new Hm,this.envMapIntensity=1,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap=`round`,this.wireframeLinejoin=`round`,this.flatShading=!1,this.fog=!0,this.setValues(e)}copy(e){return super.copy(e),this.defines={STANDARD:``},this.color.copy(e.color),this.roughness=e.roughness,this.metalness=e.metalness,this.map=e.map,this.lightMap=e.lightMap,this.lightMapIntensity=e.lightMapIntensity,this.aoMap=e.aoMap,this.aoMapIntensity=e.aoMapIntensity,this.emissive.copy(e.emissive),this.emissiveMap=e.emissiveMap,this.emissiveIntensity=e.emissiveIntensity,this.bumpMap=e.bumpMap,this.bumpScale=e.bumpScale,this.normalMap=e.normalMap,this.normalMapType=e.normalMapType,this.normalScale.copy(e.normalScale),this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this.roughnessMap=e.roughnessMap,this.metalnessMap=e.metalnessMap,this.alphaMap=e.alphaMap,this.envMap=e.envMap,this.envMapRotation.copy(e.envMapRotation),this.envMapIntensity=e.envMapIntensity,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this.wireframeLinecap=e.wireframeLinecap,this.wireframeLinejoin=e.wireframeLinejoin,this.flatShading=e.flatShading,this.fog=e.fog,this}},Fv=class extends Pv{constructor(e){super(),this.isMeshPhysicalMaterial=!0,this.defines={STANDARD:``,PHYSICAL:``},this.type=`MeshPhysicalMaterial`,this.anisotropyRotation=0,this.anisotropyMap=null,this.clearcoatMap=null,this.clearcoatRoughness=0,this.clearcoatRoughnessMap=null,this.clearcoatNormalScale=new q(1,1),this.clearcoatNormalMap=null,this.ior=1.5,Object.defineProperty(this,"reflectivity",{get:function(){return Vp(2.5*(this.ior-1)/(this.ior+1),0,1)},set:function(e){this.ior=(1+.4*e)/(1-.4*e)}}),this.iridescenceMap=null,this.iridescenceIOR=1.3,this.iridescenceThicknessRange=[100,400],this.iridescenceThicknessMap=null,this.sheenColor=new Y(0),this.sheenColorMap=null,this.sheenRoughness=1,this.sheenRoughnessMap=null,this.transmissionMap=null,this.thickness=0,this.thicknessMap=null,this.attenuationDistance=1/0,this.attenuationColor=new Y(1,1,1),this.specularIntensity=1,this.specularIntensityMap=null,this.specularColor=new Y(1,1,1),this.specularColorMap=null,this._anisotropy=0,this._clearcoat=0,this._dispersion=0,this._iridescence=0,this._retroreflectivity=0,this._sheen=0,this._transmission=0,this.setValues(e)}get anisotropy(){return this._anisotropy}set anisotropy(e){this._anisotropy>0!=e>0&&this.version++,this._anisotropy=e}get clearcoat(){return this._clearcoat}set clearcoat(e){this._clearcoat>0!=e>0&&this.version++,this._clearcoat=e}get iridescence(){return this._iridescence}set iridescence(e){this._iridescence>0!=e>0&&this.version++,this._iridescence=e}get dispersion(){return this._dispersion}set dispersion(e){this._dispersion>0!=e>0&&this.version++,this._dispersion=e}get retroreflectivity(){return this._retroreflectivity}set retroreflectivity(e){this._retroreflectivity>0!=e>0&&this.version++,this._retroreflectivity=e}get sheen(){return this._sheen}set sheen(e){this._sheen>0!=e>0&&this.version++,this._sheen=e}get transmission(){return this._transmission}set transmission(e){this._transmission>0!=e>0&&this.version++,this._transmission=e}copy(e){return super.copy(e),this.defines={STANDARD:``,PHYSICAL:``},this.anisotropy=e.anisotropy,this.anisotropyRotation=e.anisotropyRotation,this.anisotropyMap=e.anisotropyMap,this.clearcoat=e.clearcoat,this.clearcoatMap=e.clearcoatMap,this.clearcoatRoughness=e.clearcoatRoughness,this.clearcoatRoughnessMap=e.clearcoatRoughnessMap,this.clearcoatNormalMap=e.clearcoatNormalMap,this.clearcoatNormalScale.copy(e.clearcoatNormalScale),this.dispersion=e.dispersion,this.ior=e.ior,this.iridescence=e.iridescence,this.iridescenceMap=e.iridescenceMap,this.iridescenceIOR=e.iridescenceIOR,this.iridescenceThicknessRange=[...e.iridescenceThicknessRange],this.iridescenceThicknessMap=e.iridescenceThicknessMap,this.retroreflectivity=e.retroreflectivity,this.sheen=e.sheen,this.sheenColor.copy(e.sheenColor),this.sheenColorMap=e.sheenColorMap,this.sheenRoughness=e.sheenRoughness,this.sheenRoughnessMap=e.sheenRoughnessMap,this.transmission=e.transmission,this.transmissionMap=e.transmissionMap,this.thickness=e.thickness,this.thicknessMap=e.thicknessMap,this.attenuationDistance=e.attenuationDistance,this.attenuationColor.copy(e.attenuationColor),this.specularIntensity=e.specularIntensity,this.specularIntensityMap=e.specularIntensityMap,this.specularColor.copy(e.specularColor),this.specularColorMap=e.specularColorMap,this}},Iv=class extends vg{constructor(e){super(),this.isMeshDepthMaterial=!0,this.type=`MeshDepthMaterial`,this.depthPacking=mp,this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.wireframe=!1,this.wireframeLinewidth=1,this.setValues(e)}copy(e){return super.copy(e),this.depthPacking=e.depthPacking,this.map=e.map,this.alphaMap=e.alphaMap,this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this}},Lv=class extends vg{constructor(e){super(),this.isMeshDistanceMaterial=!0,this.type=`MeshDistanceMaterial`,this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.setValues(e)}copy(e){return super.copy(e),this.map=e.map,this.alphaMap=e.alphaMap,this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this}};function Rv(e,t){return!e||e.constructor===t?e:typeof t.BYTES_PER_ELEMENT==`number`?new t(e):Array.prototype.slice.call(e)}function zv(e){return e!==void 0&&e.inTangents!==void 0&&e.outTangents!==void 0}function Bv(e){function t(t,n){return e[t]-e[n]}let n=e.length,r=Array(n);for(let e=0;e!==n;++e)r[e]=e;return r.sort(t),r}function Vv(e,t,n){let r=e.length,i=new e.constructor(r);for(let a=0,o=0;o!==r;++a){let r=n[a]*t;for(let n=0;n!==t;++n)i[o++]=e[r+n]}return i}function Hv(e,t,n,r){let i=1,a=e[0];for(;a!==void 0&&a[r]===void 0;)a=e[i++];if(a===void 0)return;let o=a[r];if(o!==void 0)if(Array.isArray(o))do o=a[r],o!==void 0&&(t.push(a.time),n.push(...o)),a=e[i++];while(a!==void 0);else if(o.toArray!==void 0)do o=a[r],o!==void 0&&(t.push(a.time),o.toArray(n,n.length)),a=e[i++];while(a!==void 0);else do o=a[r],o!==void 0&&(t.push(a.time),n.push(o)),a=e[i++];while(a!==void 0)}var Uv=class{constructor(e,t,n,r){this.parameterPositions=e,this._cachedIndex=0,this.resultBuffer=r===void 0?new t.constructor(n):r,this.sampleValues=t,this.valueSize=n,this.settings=null,this.DefaultSettings_={}}evaluate(e){let t=this.parameterPositions,n=this._cachedIndex,r=t[n],i=t[n-1];validate_interval:{seek:{let a;linear_scan:{forward_scan:if(!(e=i)){let o=t[1];e=i)break seek}a=n,n=0;break linear_scan}break validate_interval}for(;n>>1;et;)--a;if(++a,i!==0||a!==r){i>=a&&(a=Math.max(a,1),i=a-1);let e=this.getValueSize();this.times=n.slice(i,a),this.values=this.values.slice(i*e,a*e)}return this}validate(){let e=!0,t=this.getValueSize();t-Math.floor(t)!==0&&(jp(`KeyframeTrack: Invalid value size in track.`,this),e=!1);let n=this.times,r=this.values,i=n.length;i===0&&(jp(`KeyframeTrack: Track is empty.`,this),e=!1);let a=null;for(let t=0;t!==i;t++){let r=n[t];if(typeof r==`number`&&isNaN(r)){jp(`KeyframeTrack: Time is not a valid number.`,this,t,r),e=!1;break}if(a!==null&&a>r){jp(`KeyframeTrack: Out of order keys.`,this,t,r,a),e=!1;break}a=r}if(r!==void 0&&wp(r))for(let t=0,n=r.length;t!==n;++t){let n=r[t];if(isNaN(n)){jp(`KeyframeTrack: Value is not a valid number.`,this,t,n),e=!1;break}}return e}optimize(){let e=this.times.slice(),t=this.values.slice(),n=this.getValueSize(),r=this.getInterpolation()===cp,i=e.length-1,a=1;for(let o=1;o0){e[a]=e[i];for(let e=i*n,r=a*n,o=0;o!==n;++o)t[r+o]=t[e+o];++a}return a===e.length?(this.times=e,this.values=t):(this.times=e.slice(0,a),this.values=t.slice(0,a*n)),this}clone(){let e=this.times.slice(),t=this.values.slice(),n=this.constructor,r=new n(this.name,e,t);return r.createInterpolant=this.createInterpolant,zv(this.settings)&&(r.settings={inTangents:this.settings.inTangents.slice(),outTangents:this.settings.outTangents.slice()}),r}};function Qv(e,t){for(let n=0,r=e.length;n!==r;n+=2)e[n]*=t}Zv.prototype.ValueTypeName=``,Zv.prototype.TimeBufferType=Float32Array,Zv.prototype.ValueBufferType=Float32Array,Zv.prototype.DefaultInterpolation=sp;var $v=class extends Zv{constructor(e,t,n){super(e,t,n)}};$v.prototype.ValueTypeName=`bool`,$v.prototype.ValueBufferType=Array,$v.prototype.DefaultInterpolation=op,$v.prototype.InterpolantFactoryMethodLinear=void 0,$v.prototype.InterpolantFactoryMethodSmooth=void 0;var ey=class extends Zv{constructor(e,t,n,r){super(e,t,n,r)}};ey.prototype.ValueTypeName=`color`;var ty=class extends Zv{constructor(e,t,n,r){super(e,t,n,r)}};ty.prototype.ValueTypeName=`number`;var ny=class extends Uv{constructor(e,t,n,r){super(e,t,n,r)}interpolate_(e,t,n,r){let i=this.resultBuffer,a=this.sampleValues,o=this.valueSize,s=(n-t)/(r-t),c=e*o;for(let e=c+o;c!==e;c+=4)cm.slerpFlat(i,0,a,c-o,a,c,s);return i}},ry=class extends Zv{constructor(e,t,n,r){super(e,t,n,r)}InterpolantFactoryMethodLinear(e){return new ny(this.times,this.values,this.getValueSize(),e)}};ry.prototype.ValueTypeName=`quaternion`,ry.prototype.InterpolantFactoryMethodSmooth=void 0;var iy=class extends Zv{constructor(e,t,n){super(e,t,n)}};iy.prototype.ValueTypeName=`string`,iy.prototype.ValueBufferType=Array,iy.prototype.DefaultInterpolation=op,iy.prototype.InterpolantFactoryMethodLinear=void 0,iy.prototype.InterpolantFactoryMethodSmooth=void 0;var ay=class extends Zv{constructor(e,t,n,r){super(e,t,n,r)}};ay.prototype.ValueTypeName=`vector`;var oy=class{constructor(e=``,t=-1,n=[],r=pp){this.name=e,this.tracks=n,this.duration=t,this.blendMode=r,this.uuid=Bp(),this.userData={},this.duration<0&&this.resetDuration()}static parse(e){let t=[],n=e.tracks,r=1/(e.fps||1);for(let e=0,i=n.length;e!==i;++e)t.push(cy(n[e]).scale(r));let i=new this(e.name,e.duration,t,e.blendMode);return i.uuid=e.uuid,i.userData=JSON.parse(e.userData||`{}`),i}static toJSON(e){let t=[],n=e.tracks,r={name:e.name,duration:e.duration,tracks:t,uuid:e.uuid,blendMode:e.blendMode,userData:JSON.stringify(e.userData)};for(let e=0,r=n.length;e!==r;++e)t.push(Zv.toJSON(n[e]));return r}static CreateFromMorphTargetSequence(e,t,n,r){let i=t.length,a=[];for(let e=0;e1){let e=a[1],t=r[e];t||(r[e]=t=[]),t.push(n)}}let a=[];for(let e in r)a.push(this.CreateFromMorphTargetSequence(e,r[e],t,n));return a}resetDuration(){let e=this.tracks,t=0;for(let n=0,r=e.length;n!==r;++n){let e=this.tracks[n];t=Math.max(t,e.times[e.times.length-1])}return this.duration=t,this}trim(){for(let e=0;e{t&&t(i),this.manager.itemEnd(e)},0);return}if(py[e]!==void 0){py[e].push({onLoad:t,onProgress:n,onError:r});return}py[e]=[],py[e].push({onLoad:t,onProgress:n,onError:r});let a=new Request(e,{headers:new Headers(this.requestHeader),credentials:this.withCredentials?`include`:`same-origin`,signal:typeof AbortSignal.any==`function`?AbortSignal.any([this._abortController.signal,this.manager.abortController.signal]):this._abortController.signal}),o=this.mimeType,s=this.responseType;fetch(a).then(t=>{if(t.status===200||t.status===0){if(t.status===0&&Ap(`FileLoader: HTTP Status 0 received.`),typeof ReadableStream>`u`||t.body===void 0||t.body.getReader===void 0)return t;let n=py[e],r=t.body.getReader(),i=t.headers.get(`X-File-Size`)||t.headers.get(`Content-Length`),a=i?parseInt(i):0,o=a!==0,s=0,c=new ReadableStream({start(e){t();function t(){r.read().then(({done:r,value:i})=>{if(r)e.close();else{s+=i.byteLength;let r=new ProgressEvent(`progress`,{lengthComputable:o,loaded:s,total:a});for(let e=0,t=n.length;e{e.error(t)})}}});return new Response(c)}else throw new my(`fetch for "${t.url}" responded with ${t.status}: ${t.statusText}`,t)}).then(e=>{switch(s){case`arraybuffer`:return e.arrayBuffer();case`blob`:return e.blob();case`document`:return e.text().then(e=>new DOMParser().parseFromString(e,o));case`json`:return e.json();default:if(o===``)return e.text();{let t=/charset="?([^;"\s]*)"?/i.exec(o),n=t&&t[1]?t[1].toLowerCase():void 0,r=new TextDecoder(n);return e.arrayBuffer().then(e=>r.decode(e))}}}).then(t=>{ly.add(`file:${e}`,t);let n=py[e];delete py[e];for(let e=0,r=n.length;e{let n=py[e];if(n===void 0)throw this.manager.itemError(e),t;delete py[e];for(let e=0,r=n.length;e{this.manager.itemEnd(e)}),this.manager.itemStart(e)}setResponseType(e){return this.responseType=e,this}setMimeType(e){return this.mimeType=e,this}abort(){return this._abortController.abort(),this._abortController=new AbortController,this}},gy=new WeakMap,_y=class extends fy{constructor(e){super(e)}load(e,t,n,r){this.path!==void 0&&(e=this.path+e),e=this.manager.resolveURL(e);let i=this,a=ly.get(`image:${e}`);if(a!==void 0){if(a.complete===!0)i.manager.itemStart(e),setTimeout(function(){t&&t(a),i.manager.itemEnd(e)},0);else{let e=gy.get(a);e===void 0&&(e=[],gy.set(a,e)),e.push({onLoad:t,onError:r})}return a}let o=Tp(`img`);function s(){l(),t&&t(this);let n=gy.get(this)||[];for(let e=0;e`u`&&Ap(`ImageBitmapLoader: createImageBitmap() not supported.`),typeof fetch>`u`&&Ap(`ImageBitmapLoader: fetch() not supported.`),this.options={premultiplyAlpha:`none`},this._abortController=new AbortController}setOptions(e){return this.options=e,this}load(e,t,n,r){e===void 0&&(e=``),this.path!==void 0&&(e=this.path+e),e=this.manager.resolveURL(e);let i=this,a=ly.get(`image-bitmap:${e}`);if(a!==void 0){if(i.manager.itemStart(e),a.then){a.then(n=>{Vy.has(a)===!0?(r&&r(Vy.get(a)),i.manager.itemError(e),i.manager.itemEnd(e)):(t&&t(n),i.manager.itemEnd(e))});return}setTimeout(function(){t&&t(a),i.manager.itemEnd(e)},0);return}let o={};o.credentials=this.crossOrigin===`anonymous`?`same-origin`:`include`,o.headers=this.requestHeader,o.signal=typeof AbortSignal.any==`function`?AbortSignal.any([this._abortController.signal,this.manager.abortController.signal]):this._abortController.signal;let s=fetch(e,o).then(function(e){return e.blob()}).then(function(e){return createImageBitmap(e,Object.assign({},i.options,{colorSpaceConversion:`none`}))}).then(function(n){return ly.add(`image-bitmap:${e}`,n),t&&t(n),i.manager.itemEnd(e),n}).catch(function(t){r&&r(t),Vy.set(s,t),ly.remove(`image-bitmap:${e}`),i.manager.itemError(e),i.manager.itemEnd(e)});ly.add(`image-bitmap:${e}`,s),i.manager.itemStart(e)}abort(){return this._abortController.abort(),this._abortController=new AbortController,this}},Uy=-90,Wy=1,Gy=class extends ah{constructor(e,t,n){super(),this.type=`CubeCamera`,this.renderTarget=n,this.coordinateSystem=null,this.activeMipmapLevel=0;let r=new My(Uy,Wy,e,t);r.layers=this.layers,this.add(r);let i=new My(Uy,Wy,e,t);i.layers=this.layers,this.add(i);let a=new My(Uy,Wy,e,t);a.layers=this.layers,this.add(a);let o=new My(Uy,Wy,e,t);o.layers=this.layers,this.add(o);let s=new My(Uy,Wy,e,t);s.layers=this.layers,this.add(s);let c=new My(Uy,Wy,e,t);c.layers=this.layers,this.add(c)}updateCoordinateSystem(){let e=this.coordinateSystem,t=this.children.concat(),[n,r,i,a,o,s]=t;for(let e of t)this.remove(e);if(e===2e3)n.up.set(0,1,0),n.lookAt(1,0,0),r.up.set(0,1,0),r.lookAt(-1,0,0),i.up.set(0,0,-1),i.lookAt(0,1,0),a.up.set(0,0,1),a.lookAt(0,-1,0),o.up.set(0,1,0),o.lookAt(0,0,1),s.up.set(0,1,0),s.lookAt(0,0,-1);else if(e===2001)n.up.set(0,-1,0),n.lookAt(-1,0,0),r.up.set(0,-1,0),r.lookAt(1,0,0),i.up.set(0,0,1),i.lookAt(0,1,0),a.up.set(0,0,-1),a.lookAt(0,-1,0),o.up.set(0,-1,0),o.lookAt(0,0,1),s.up.set(0,-1,0),s.lookAt(0,0,-1);else throw Error(`THREE.CubeCamera.updateCoordinateSystem(): Invalid coordinate system: `+e);for(let e of t)this.add(e),e.updateMatrixWorld()}update(e,t){this.parent===null&&this.updateMatrixWorld();let{renderTarget:n,activeMipmapLevel:r}=this;this.coordinateSystem!==e.coordinateSystem&&(this.coordinateSystem=e.coordinateSystem,this.updateCoordinateSystem());let[i,a,o,s,c,l]=this.children,u=e.getRenderTarget(),d=e.getActiveCubeFace(),f=e.getActiveMipmapLevel(),p=e.xr.enabled;e.xr.enabled=!1;let m=n.texture.generateMipmaps;n.texture.generateMipmaps=!1;let h=!1;h=e.isWebGLRenderer===!0?e.state.buffers.depth.getReversed():e.reversedDepthBuffer,e.setRenderTarget(n,0,r),h&&e.autoClear===!1&&e.clearDepth(),e.render(t,i),e.setRenderTarget(n,1,r),h&&e.autoClear===!1&&e.clearDepth(),e.render(t,a),e.setRenderTarget(n,2,r),h&&e.autoClear===!1&&e.clearDepth(),e.render(t,o),e.setRenderTarget(n,3,r),h&&e.autoClear===!1&&e.clearDepth(),e.render(t,s),e.setRenderTarget(n,4,r),h&&e.autoClear===!1&&e.clearDepth(),e.render(t,c),n.texture.generateMipmaps=m,e.setRenderTarget(n,5,r),h&&e.autoClear===!1&&e.clearDepth(),e.render(t,l),e.setRenderTarget(u,d,f),e.xr.enabled=p,n.texture.needsPMREMUpdate=!0}},Ky=class extends My{constructor(e=[]){super(),this.isArrayCamera=!0,this.isMultiViewCamera=!1,this.cameras=e}},qy=class{constructor(){this._previousTime=0,this._currentTime=0,this._startTime=performance.now(),this._delta=0,this._elapsed=0,this._timescale=1,this._document=null,this._pageVisibilityHandler=null}connect(e){this._document=e,e.hidden!==void 0&&(this._pageVisibilityHandler=Jy.bind(this),e.addEventListener(`visibilitychange`,this._pageVisibilityHandler,!1))}disconnect(){this._pageVisibilityHandler!==null&&(this._document.removeEventListener(`visibilitychange`,this._pageVisibilityHandler),this._pageVisibilityHandler=null),this._document=null}getDelta(){return this._delta/1e3}getElapsed(){return this._elapsed/1e3}getTimescale(){return this._timescale}setTimescale(e){return this._timescale=e,this}reset(){return this._currentTime=performance.now()-this._startTime,this}dispose(){this.disconnect()}update(e){return this._pageVisibilityHandler!==null&&this._document.hidden===!0?this._delta=0:(this._previousTime=this._currentTime,this._currentTime=(e===void 0?performance.now():e)-this._startTime,this._delta=(this._currentTime-this._previousTime)*this._timescale,this._elapsed+=this._delta),this}};function Jy(){this._document.hidden===!1&&this.reset()}var Yy=`\\[\\]\\.:\\/`,Xy=RegExp(`[\\[\\]\\.:\\/]`,`g`),Zy=`[^\\[\\]\\.:\\/]`,Qy=`[^`+Yy.replace(`\\.`,``)+`]`,$y=`((?:WC+[\\/:])*)`.replace(`WC`,Zy),eb=`(WCOD+)?`.replace(`WCOD`,Qy),tb=`(?:\\.(WC+)(?:\\[(.+)\\])?)?`.replace(`WC`,Zy),nb=`\\.(WC+)(?:\\[(.+)\\])?`.replace(`WC`,Zy),rb=RegExp(`^`+$y+eb+tb+nb+`$`),ib=[`material`,`materials`,`bones`,`map`],ab=class{constructor(e,t,n){let r=n||ob.parseTrackName(t);this._targetGroup=e,this._bindings=e.subscribe_(t,r)}getValue(e,t){this.bind();let n=this._targetGroup.nCachedObjects_,r=this._bindings[n];r!==void 0&&r.getValue(e,t)}setValue(e,t){let n=this._bindings;for(let r=this._targetGroup.nCachedObjects_,i=n.length;r!==i;++r)n[r].setValue(e,t)}bind(){let e=this._bindings;for(let t=this._targetGroup.nCachedObjects_,n=e.length;t!==n;++t)e[t].bind()}unbind(){let e=this._bindings;for(let t=this._targetGroup.nCachedObjects_,n=e.length;t!==n;++t)e[t].unbind()}},ob=class e{constructor(t,n,r){this.path=n,this.parsedPath=r||e.parseTrackName(n),this.node=e.findNode(t,this.parsedPath.nodeName),this.rootNode=t,this.getValue=this._getValue_unbound,this.setValue=this._setValue_unbound}static create(t,n,r){return t&&t.isAnimationObjectGroup?new e.Composite(t,n,r):new e(t,n,r)}static sanitizeNodeName(e){return e.replace(/\s/g,`_`).replace(Xy,``)}static parseTrackName(e){let t=rb.exec(e);if(t===null)throw Error(`THREE.PropertyBinding: Cannot parse trackName: `+e);let n={nodeName:t[2],objectName:t[3],objectIndex:t[4],propertyName:t[5],propertyIndex:t[6]},r=n.nodeName&&n.nodeName.lastIndexOf(`.`);if(r!==void 0&&r!==-1){let e=n.nodeName.substring(r+1);ib.indexOf(e)!==-1&&(n.nodeName=n.nodeName.substring(0,r),n.objectName=e)}if(n.propertyName===null||n.propertyName.length===0)throw Error(`THREE.PropertyBinding: can not parse propertyName from trackName: `+e);return n}static findNode(e,t){if(t===void 0||t===``||t===`.`||t===-1||t===e.name||t===e.uuid)return e;if(e.skeleton){let n=e.skeleton.getBoneByName(t);if(n!==void 0)return n}if(e.children){let n=function(e){for(let r=0;r=this.min.x&&e.x<=this.max.x&&e.y>=this.min.y&&e.y<=this.max.y}containsBox(e){return this.min.x<=e.min.x&&e.max.x<=this.max.x&&this.min.y<=e.min.y&&e.max.y<=this.max.y}getParameter(e,t){return t.set((e.x-this.min.x)/(this.max.x-this.min.x),(e.y-this.min.y)/(this.max.y-this.min.y))}intersectsBox(e){return e.max.x>=this.min.x&&e.min.x<=this.max.x&&e.max.y>=this.min.y&&e.min.y<=this.max.y}clampPoint(e,t){return t.copy(e).clamp(this.min,this.max)}distanceToPoint(e){return this.clampPoint(e,sb).distanceTo(e)}intersect(e){return this.min.max(e.min),this.max.min(e.max),this.isEmpty()&&this.makeEmpty(),this}union(e){return this.min.min(e.min),this.max.max(e.max),this}translate(e){return this.min.add(e),this.max.add(e),this}equals(e){return e.min.equals(this.min)&&e.max.equals(this.max)}},lb=new J,ub=new Mm,db=new Mm,fb=class extends T_{constructor(e){let t=pb(e),n=new lg,r=[],i=[];for(let e=0;ee.start-t.start);let t=0;for(let e=1;e #include #include -}`},$={common:{diffuse:{value:new Q(16777215)},opacity:{value:1},map:{value:null},mapTransform:{value:new Ru},alphaMap:{value:null},alphaMapTransform:{value:new Ru},alphaTest:{value:0}},specularmap:{specularMap:{value:null},specularMapTransform:{value:new Ru}},envmap:{envMap:{value:null},envMapRotation:{value:new Ru},reflectivity:{value:1},ior:{value:1.5},refractionRatio:{value:.98},dfgLUT:{value:null}},aomap:{aoMap:{value:null},aoMapIntensity:{value:1},aoMapTransform:{value:new Ru}},lightmap:{lightMap:{value:null},lightMapIntensity:{value:1},lightMapTransform:{value:new Ru}},bumpmap:{bumpMap:{value:null},bumpMapTransform:{value:new Ru},bumpScale:{value:1}},normalmap:{normalMap:{value:null},normalMapTransform:{value:new Ru},normalScale:{value:new X(1,1)}},displacementmap:{displacementMap:{value:null},displacementMapTransform:{value:new Ru},displacementScale:{value:1},displacementBias:{value:0}},emissivemap:{emissiveMap:{value:null},emissiveMapTransform:{value:new Ru}},metalnessmap:{metalnessMap:{value:null},metalnessMapTransform:{value:new Ru}},roughnessmap:{roughnessMap:{value:null},roughnessMapTransform:{value:new Ru}},gradientmap:{gradientMap:{value:null}},fog:{fogDensity:{value:25e-5},fogNear:{value:1},fogFar:{value:2e3},fogColor:{value:new Q(16777215)}},lights:{ambientLightColor:{value:[]},lightProbe:{value:[]},sunLights:{value:[],properties:{direction:{},color:{}}},sunLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},sunShadowMatrix:{value:[]},sunShadowCascade:{value:[]},directionalLights:{value:[],properties:{direction:{},color:{}}},directionalLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},directionalShadowMatrix:{value:[]},spotLights:{value:[],properties:{color:{},position:{},direction:{},distance:{},coneCos:{},penumbraCos:{},decay:{}}},spotLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},spotLightMap:{value:[]},spotLightMatrix:{value:[]},pointLights:{value:[],properties:{color:{},position:{},decay:{},distance:{}}},pointLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{},shadowCameraNear:{},shadowCameraFar:{}}},pointShadowMatrix:{value:[]},hemisphereLights:{value:[],properties:{direction:{},skyColor:{},groundColor:{}}},rectAreaLights:{value:[],properties:{color:{},position:{},width:{},height:{}}},ltc_1:{value:null},ltc_2:{value:null},probesSH:{value:null},probesMin:{value:new Z},probesMax:{value:new Z},probesResolution:{value:new Z}},points:{diffuse:{value:new Q(16777215)},opacity:{value:1},size:{value:1},scale:{value:1},map:{value:null},alphaMap:{value:null},alphaMapTransform:{value:new Ru},alphaTest:{value:0},uvTransform:{value:new Ru}},sprite:{diffuse:{value:new Q(16777215)},opacity:{value:1},center:{value:new X(.5,.5)},rotation:{value:0},map:{value:null},mapTransform:{value:new Ru},alphaMap:{value:null},alphaMapTransform:{value:new Ru},alphaTest:{value:0}}},Gg={basic:{uniforms:Qm([$.common,$.specularmap,$.envmap,$.aomap,$.lightmap,$.fog]),vertexShader:Wg.meshbasic_vert,fragmentShader:Wg.meshbasic_frag},lambert:{uniforms:Qm([$.common,$.specularmap,$.envmap,$.aomap,$.lightmap,$.emissivemap,$.bumpmap,$.normalmap,$.displacementmap,$.fog,$.lights,{emissive:{value:new Q(0)},envMapIntensity:{value:1}}]),vertexShader:Wg.meshlambert_vert,fragmentShader:Wg.meshlambert_frag},phong:{uniforms:Qm([$.common,$.specularmap,$.envmap,$.aomap,$.lightmap,$.emissivemap,$.bumpmap,$.normalmap,$.displacementmap,$.fog,$.lights,{emissive:{value:new Q(0)},specular:{value:new Q(1118481)},shininess:{value:30},envMapIntensity:{value:1}}]),vertexShader:Wg.meshphong_vert,fragmentShader:Wg.meshphong_frag},standard:{uniforms:Qm([$.common,$.envmap,$.aomap,$.lightmap,$.emissivemap,$.bumpmap,$.normalmap,$.displacementmap,$.roughnessmap,$.metalnessmap,$.fog,$.lights,{emissive:{value:new Q(0)},roughness:{value:1},metalness:{value:0},envMapIntensity:{value:1}}]),vertexShader:Wg.meshphysical_vert,fragmentShader:Wg.meshphysical_frag},toon:{uniforms:Qm([$.common,$.aomap,$.lightmap,$.emissivemap,$.bumpmap,$.normalmap,$.displacementmap,$.gradientmap,$.fog,$.lights,{emissive:{value:new Q(0)}}]),vertexShader:Wg.meshtoon_vert,fragmentShader:Wg.meshtoon_frag},matcap:{uniforms:Qm([$.common,$.bumpmap,$.normalmap,$.displacementmap,$.fog,{matcap:{value:null}}]),vertexShader:Wg.meshmatcap_vert,fragmentShader:Wg.meshmatcap_frag},points:{uniforms:Qm([$.points,$.fog]),vertexShader:Wg.points_vert,fragmentShader:Wg.points_frag},dashed:{uniforms:Qm([$.common,$.fog,{scale:{value:1},dashSize:{value:1},totalSize:{value:2}}]),vertexShader:Wg.linedashed_vert,fragmentShader:Wg.linedashed_frag},depth:{uniforms:Qm([$.common,$.displacementmap]),vertexShader:Wg.depth_vert,fragmentShader:Wg.depth_frag},normal:{uniforms:Qm([$.common,$.bumpmap,$.normalmap,$.displacementmap,{opacity:{value:1}}]),vertexShader:Wg.meshnormal_vert,fragmentShader:Wg.meshnormal_frag},sprite:{uniforms:Qm([$.sprite,$.fog]),vertexShader:Wg.sprite_vert,fragmentShader:Wg.sprite_frag},background:{uniforms:{uvTransform:{value:new Ru},t2D:{value:null},backgroundIntensity:{value:1}},vertexShader:Wg.background_vert,fragmentShader:Wg.background_frag},backgroundCube:{uniforms:{envMap:{value:null},backgroundBlurriness:{value:0},backgroundIntensity:{value:1},backgroundRotation:{value:new Ru}},vertexShader:Wg.backgroundCube_vert,fragmentShader:Wg.backgroundCube_frag},cube:{uniforms:{tCube:{value:null},tFlip:{value:-1},opacity:{value:1}},vertexShader:Wg.cube_vert,fragmentShader:Wg.cube_frag},equirect:{uniforms:{tEquirect:{value:null}},vertexShader:Wg.equirect_vert,fragmentShader:Wg.equirect_frag},distance:{uniforms:Qm([$.common,$.displacementmap,{referencePosition:{value:new Z},nearDistance:{value:1},farDistance:{value:1e3}}]),vertexShader:Wg.distance_vert,fragmentShader:Wg.distance_frag},shadow:{uniforms:Qm([$.lights,$.fog,{color:{value:new Q(0)},opacity:{value:1}}]),vertexShader:Wg.shadow_vert,fragmentShader:Wg.shadow_frag}};Gg.physical={uniforms:Qm([Gg.standard.uniforms,{clearcoat:{value:0},clearcoatMap:{value:null},clearcoatMapTransform:{value:new Ru},clearcoatNormalMap:{value:null},clearcoatNormalMapTransform:{value:new Ru},clearcoatNormalScale:{value:new X(1,1)},clearcoatRoughness:{value:0},clearcoatRoughnessMap:{value:null},clearcoatRoughnessMapTransform:{value:new Ru},dispersion:{value:0},retroreflectivity:{value:0},iridescence:{value:0},iridescenceMap:{value:null},iridescenceMapTransform:{value:new Ru},iridescenceIOR:{value:1.3},iridescenceThicknessMinimum:{value:100},iridescenceThicknessMaximum:{value:400},iridescenceThicknessMap:{value:null},iridescenceThicknessMapTransform:{value:new Ru},sheen:{value:0},sheenColor:{value:new Q(0)},sheenColorMap:{value:null},sheenColorMapTransform:{value:new Ru},sheenRoughness:{value:1},sheenRoughnessMap:{value:null},sheenRoughnessMapTransform:{value:new Ru},transmission:{value:0},transmissionMap:{value:null},transmissionMapTransform:{value:new Ru},transmissionSamplerSize:{value:new X},transmissionSamplerMap:{value:null},thickness:{value:0},thicknessMap:{value:null},thicknessMapTransform:{value:new Ru},attenuationDistance:{value:0},attenuationColor:{value:new Q(0)},specularColor:{value:new Q(1,1,1)},specularColorMap:{value:null},specularColorMapTransform:{value:new Ru},specularIntensity:{value:1},specularIntensityMap:{value:null},specularIntensityMapTransform:{value:new Ru},anisotropyVector:{value:new X},anisotropyMap:{value:null},anisotropyMapTransform:{value:new Ru}}]),vertexShader:Wg.meshphysical_vert,fragmentShader:Wg.meshphysical_frag};var Kg={r:0,b:0,g:0},qg=new ad,Jg=new Ru;Jg.set(-1,0,0,0,1,0,0,0,1);function Yg(e,t,n,r,i,a){let o=new Q(0),s=i===!0?0:1,c,l,u=null,d=0,f=null;function p(e){let n=e.isScene===!0?e.background:null;if(n&&n.isTexture){let r=e.backgroundBlurriness>0;n=t.get(n,r)}return n}function m(t){let r=!1,i=p(t);i===null?g(o,s):i&&i.isColor&&(g(i,1),r=!0);let c=e.xr.getEnvironmentBlendMode();c===`additive`?n.buffers.color.setClear(0,0,0,1,a):c===`alpha-blend`&&n.buffers.color.setClear(0,0,0,0,a),(e.autoClear||r)&&(n.buffers.depth.setTest(!0),n.buffers.depth.setMask(!0),n.buffers.color.setMask(!0),e.clear(e.autoClearColor,e.autoClearDepth,e.autoClearStencil))}function h(t,n){let i=p(n);i&&(i.isCubeTexture||i.mapping===306)?(l===void 0&&(l=new up(new pm(1,1,1),new ah({name:`BackgroundCubeMaterial`,uniforms:Zm(Gg.backgroundCube.uniforms),vertexShader:Gg.backgroundCube.vertexShader,fragmentShader:Gg.backgroundCube.fragmentShader,side:1,depthTest:!1,depthWrite:!1,fog:!1,allowOverride:!1})),l.geometry.deleteAttribute(`normal`),l.geometry.deleteAttribute(`uv`),l.onBeforeRender=function(e,t,n){this.matrixWorld.copyPosition(n.matrixWorld)},Object.defineProperty(l.material,"envMap",{get:function(){return this.uniforms.envMap.value}}),r.update(l)),l.material.uniforms.envMap.value=i,l.material.uniforms.backgroundBlurriness.value=n.backgroundBlurriness,l.material.uniforms.backgroundIntensity.value=n.backgroundIntensity,l.material.uniforms.backgroundRotation.value.setFromMatrix4(qg.makeRotationFromEuler(n.backgroundRotation)).transpose(),i.isCubeTexture&&i.isRenderTargetTexture===!1&&l.material.uniforms.backgroundRotation.value.premultiply(Jg),l.material.toneMapped=Uu.getTransfer(i.colorSpace)!==Wl,(u!==i||d!==i.version||f!==e.toneMapping)&&(l.material.needsUpdate=!0,u=i,d=i.version,f=e.toneMapping),l.layers.enableAll(),t.unshift(l,l.geometry,l.material,0,0,null)):i&&i.isTexture&&(c===void 0&&(c=new up(new qm(2,2),new ah({name:`BackgroundMaterial`,uniforms:Zm(Gg.background.uniforms),vertexShader:Gg.background.vertexShader,fragmentShader:Gg.background.fragmentShader,side:0,depthTest:!1,depthWrite:!1,fog:!1,allowOverride:!1})),c.geometry.deleteAttribute(`normal`),Object.defineProperty(c.material,"map",{get:function(){return this.uniforms.t2D.value}}),r.update(c)),c.material.uniforms.t2D.value=i,c.material.uniforms.backgroundIntensity.value=n.backgroundIntensity,c.material.toneMapped=Uu.getTransfer(i.colorSpace)!==Wl,i.matrixAutoUpdate===!0&&i.updateMatrix(),c.material.uniforms.uvTransform.value.copy(i.matrix),(u!==i||d!==i.version||f!==e.toneMapping)&&(c.material.needsUpdate=!0,u=i,d=i.version,f=e.toneMapping),c.layers.enableAll(),t.unshift(c,c.geometry,c.material,0,0,null))}function g(t,r){t.getRGB(Kg,th(e)),n.buffers.color.setClear(Kg.r,Kg.g,Kg.b,r,a)}function _(){l!==void 0&&(l.geometry.dispose(),l.material.dispose(),l=void 0),c!==void 0&&(c.geometry.dispose(),c.material.dispose(),c=void 0)}return{getClearColor:function(){return o},setClearColor:function(e,t=1){o.set(e),s=t,g(o,s)},getClearAlpha:function(){return s},setClearAlpha:function(e){s=e,g(o,s)},render:m,addToRenderList:h,dispose:_}}function Xg(e,t){let n=e.getParameter(e.MAX_VERTEX_ATTRIBS),r={},i=f(null),a=i,o=!1;function s(n,r,i,s,c){let u=!1,f=d(n,s,i,r);a!==f&&(a=f,l(a.object)),u=p(n,s,i,c),u&&m(n,s,i,c),c!==null&&t.update(c,e.ELEMENT_ARRAY_BUFFER),(u||o)&&(o=!1,b(n,r,i,s),c!==null&&e.bindBuffer(e.ELEMENT_ARRAY_BUFFER,t.get(c).buffer))}function c(){return e.createVertexArray()}function l(t){return e.bindVertexArray(t)}function u(t){return e.deleteVertexArray(t)}function d(e,t,n,i){let a=i.wireframe===!0,o=r[t.id];o===void 0&&(o={},r[t.id]=o);let s=e.isInstancedMesh===!0?e.id:0,l=o[s];l===void 0&&(l={},o[s]=l);let u=l[n.id];u===void 0&&(u={},l[n.id]=u);let d=u[a];return d===void 0&&(d=f(c()),u[a]=d),d}function f(e){let t=[],r=[],i=[];for(let e=0;e=0){let n=i[t],r=o[t];if(r===void 0&&(t===`instanceMatrix`&&e.instanceMatrix&&(r=e.instanceMatrix),t===`instanceColor`&&e.instanceColor&&(r=e.instanceColor)),n===void 0||n.attribute!==r||r&&n.data!==r.data)return!0;s++}return a.attributesNum!==s||a.index!==r}function m(e,t,n,r){let i={},o=t.attributes,s=0,c=n.getAttributes();for(let t in c)if(c[t].location>=0){let n=o[t];n===void 0&&(t===`instanceMatrix`&&e.instanceMatrix&&(n=e.instanceMatrix),t===`instanceColor`&&e.instanceColor&&(n=e.instanceColor));let r={};r.attribute=n,n&&n.data&&(r.data=n.data),i[t]=r,s++}a.attributes=i,a.attributesNum=s,a.index=r}function h(){let e=a.newAttributes;for(let t=0,n=e.length;t=0){let s=o[r];if(s===void 0&&(r===`instanceMatrix`&&n.instanceMatrix&&(s=n.instanceMatrix),r===`instanceColor`&&n.instanceColor&&(s=n.instanceColor)),s!==void 0){let r=s.normalized,o=s.itemSize,c=t.get(s);if(c===void 0)continue;let l=c.buffer,u=c.type,d=c.bytesPerElement,f=u===e.INT||u===e.UNSIGNED_INT||s.gpuType===1013;if(s.isInterleavedBufferAttribute){let t=s.data,c=t.stride,p=s.offset;if(t.isInstancedInterleavedBuffer){for(let e=0;e0&&e.getShaderPrecisionFormat(e.FRAGMENT_SHADER,e.HIGH_FLOAT).precision>0)return`highp`;t=`mediump`}return t===`mediump`&&e.getShaderPrecisionFormat(e.VERTEX_SHADER,e.MEDIUM_FLOAT).precision>0&&e.getShaderPrecisionFormat(e.FRAGMENT_SHADER,e.MEDIUM_FLOAT).precision>0?`mediump`:`lowp`}let l=n.precision===void 0?`highp`:n.precision,u=c(l);u!==l&&(nu(`WebGLRenderer:`,l,`not supported, using`,u,`instead.`),l=u);let d=n.logarithmicDepthBuffer===!0,f=n.reversedDepthBuffer===!0&&t.has(`EXT_clip_control`);n.reversedDepthBuffer===!0&&f===!1&&nu(`WebGLRenderer: Unable to use reversed depth buffer due to missing EXT_clip_control extension. Fallback to default depth buffer.`);let p=e.getParameter(e.MAX_TEXTURE_IMAGE_UNITS),m=e.getParameter(e.MAX_VERTEX_TEXTURE_IMAGE_UNITS),h=e.getParameter(e.MAX_TEXTURE_SIZE),g=e.getParameter(e.MAX_CUBE_MAP_TEXTURE_SIZE),_=e.getParameter(e.MAX_VERTEX_ATTRIBS),v=e.getParameter(e.MAX_VERTEX_UNIFORM_VECTORS),y=e.getParameter(e.MAX_VARYING_VECTORS),b=e.getParameter(e.MAX_FRAGMENT_UNIFORM_VECTORS),x=e.getParameter(e.MAX_SAMPLES),S=e.getParameter(e.SAMPLES);return{isWebGL2:!0,getMaxAnisotropy:a,getMaxPrecision:c,textureFormatReadable:o,textureTypeReadable:s,precision:l,logarithmicDepthBuffer:d,reversedDepthBuffer:f,maxTextures:p,maxVertexTextures:m,maxTextureSize:h,maxCubemapSize:g,maxAttributes:_,maxVertexUniforms:v,maxVaryings:y,maxFragmentUniforms:b,maxSamples:x,samples:S}}function $g(e){let t=this,n=null,r=0,i=!1,a=!1,o=new Wf,s=new Ru,c={value:null,needsUpdate:!1};this.uniform=c,this.numPlanes=0,this.numIntersection=0,this.init=function(e,t){let n=e.length!==0||t||r!==0||i;return i=t,r=e.length,n},this.beginShadows=function(){a=!0,u(null)},this.endShadows=function(){a=!1},this.setGlobalState=function(e,t){n=u(e,t,0)},this.setState=function(t,o,s){let d=t.clippingPlanes,f=t.clipIntersection,p=t.clipShadows,m=e.get(t);if(!i||d===null||d.length===0||a&&!p)a?u(null):l();else{let e=a?0:r,t=e*4,i=m.clippingState||null;c.value=i,i=u(d,o,t,s);for(let e=0;e!==t;++e)i[e]=n[e];m.clippingState=i,this.numIntersection=f?this.numPlanes:0,this.numPlanes+=e}};function l(){c.value!==n&&(c.value=n,c.needsUpdate=r>0),t.numPlanes=r,t.numIntersection=0}function u(e,n,r,i){let a=e===null?0:e.length,l=null;if(a!==0){if(l=c.value,i!==!0||l===null){let t=r+a*4,i=n.matrixWorldInverse;s.getNormalMatrix(i),(l===null||l.length0&&this._blur(s,0,0,t),this._applyPMREM(s),this._cleanup(s),s}fromEquirectangular(e,t=null){return this._fromTexture(e,t)}fromCubemap(e,t=null){return this._fromTexture(e,t)}compileCubemapShader(){this._cubemapMaterial===null&&(this._cubemapMaterial=y_(),this._compileMaterial(this._cubemapMaterial))}compileEquirectangularShader(){this._equirectMaterial===null&&(this._equirectMaterial=v_(),this._compileMaterial(this._equirectMaterial))}dispose(){this._dispose(),this._cubemapMaterial!==null&&this._cubemapMaterial.dispose(),this._equirectMaterial!==null&&this._equirectMaterial.dispose(),this._backgroundBox!==null&&(this._backgroundBox.geometry.dispose(),this._backgroundBox.material.dispose())}_setSize(e){this._lodMax=Math.floor(Math.log2(e)),this._cubeSize=2**this._lodMax}_dispose(){this._blurMaterial!==null&&this._blurMaterial.dispose(),this._ggxMaterial!==null&&this._ggxMaterial.dispose(),this._pingPongRenderTarget!==null&&this._pingPongRenderTarget.dispose();for(let e=0;e2?l:0,l,l),c.setRenderTarget(r),p&&c.render(d,a),c.render(e,a)}c.toneMapping=u,c.autoClear=l,e.background=m}_textureToCubeUV(e,t){let n=this._renderer,r=e.mapping===301||e.mapping===302;r?(this._cubemapMaterial===null&&(this._cubemapMaterial=y_()),this._cubemapMaterial.uniforms.flipEnvMap.value=e.isRenderTargetTexture===!1?-1:1):this._equirectMaterial===null&&(this._equirectMaterial=v_());let i=r?this._cubemapMaterial:this._equirectMaterial,a=this._lodMeshes[0];a.material=i;let o=i.uniforms;o.envMap.value=e;let s=this._cubeSize;h_(t,0,0,3*s,2*s),n.setRenderTarget(t),n.render(a,i_)}_applyPMREM(e){let t=this._renderer,n=t.autoClear;t.autoClear=!1;let r=this._lodMeshes.length;for(let t=1;td-e_?n-d+e_:0),m=4*(this._cubeSize-f);s.envMap.value=e.texture,s.roughness.value=u,s.mipInt.value=d-t,h_(i,p,m,3*f,2*f),r.setRenderTarget(i),r.render(o,i_),s.envMap.value=i.texture,s.roughness.value=0,s.mipInt.value=d-n,h_(e,p,m,3*f,2*f),r.setRenderTarget(e),r.render(o,i_)}_blur(e,t,n,r){let i=this._pingPongRenderTarget,a=Math.min(r,Math.PI)/Math.SQRT2;this._blurPass(e,i,t,n,a),this._blurPass(i,e,n,n,a)}_blurPass(e,t,n,r,i){let a=this._renderer,o=this._blurMaterial,s=this._lodMeshes[r];s.material=o;let c=o.uniforms;c.envMap.value=e.texture,c.sigma.value=i,c.mipInt.value=this._lodMax-n;let l=this._sizeLods[r];h_(t,3*l*(r>this._lodMax-e_?r-this._lodMax+e_:0),4*(this._cubeSize-l),3*l,2*l),a.setRenderTarget(t),a.render(s,i_)}};function p_(e){let t=[],n=[],r=e,i=e-e_+1+t_;for(let e=0;e2?0:-1,r=[t,n,0,t+2/3,n,0,t+2/3,n+1,0,t,n,0,t+2/3,n+1,0,t,n+1,0];c.set(r,18*e);for(let t=0;t<6;t++){let n=s[t*2]*2-1,r=s[t*2+1]*2-1;e===0?d_.set(1,r,n):e===1?d_.set(-n,1,-r):e===2?d_.set(-n,r,1):e===3?d_.set(-1,r,-n):e===4?d_.set(-n,-1,r):d_.set(n,r,-1),d_.toArray(l,(e*6+t)*3)}}let u=new Lf;u.setAttribute(`position`,new Sf(c,3)),u.setAttribute(`outputDirection`,new Sf(l,3)),n.push(new up(u,null)),r>e_&&r--}return{lodMeshes:n,sizeLods:t}}function m_(e,t,n){let r=new nd(e,t,n);return r.texture.mapping=306,r.texture.name=`PMREM.cubeUv`,r.scissorTest=!0,r}function h_(e,t,n,r,i){e.viewport.set(t,n,r,i),e.scissor.set(t,n,r,i)}function g_(e,t,n){return new ah({name:`PMREMGGXConvolution`,defines:{GGX_SAMPLES:r_,CUBEUV_TEXEL_WIDTH:1/t,CUBEUV_TEXEL_HEIGHT:1/n,CUBEUV_MAX_MIP:`${e}.0`},uniforms:{envMap:{value:null},roughness:{value:0},mipInt:{value:0}},vertexShader:b_(),fragmentShader:` +}`},X={common:{diffuse:{value:new Y(16777215)},opacity:{value:1},map:{value:null},mapTransform:{value:new dm},alphaMap:{value:null},alphaMapTransform:{value:new dm},alphaTest:{value:0}},specularmap:{specularMap:{value:null},specularMapTransform:{value:new dm}},envmap:{envMap:{value:null},envMapRotation:{value:new dm},reflectivity:{value:1},ior:{value:1.5},refractionRatio:{value:.98},dfgLUT:{value:null}},aomap:{aoMap:{value:null},aoMapIntensity:{value:1},aoMapTransform:{value:new dm}},lightmap:{lightMap:{value:null},lightMapIntensity:{value:1},lightMapTransform:{value:new dm}},bumpmap:{bumpMap:{value:null},bumpMapTransform:{value:new dm},bumpScale:{value:1}},normalmap:{normalMap:{value:null},normalMapTransform:{value:new dm},normalScale:{value:new q(1,1)}},displacementmap:{displacementMap:{value:null},displacementMapTransform:{value:new dm},displacementScale:{value:1},displacementBias:{value:0}},emissivemap:{emissiveMap:{value:null},emissiveMapTransform:{value:new dm}},metalnessmap:{metalnessMap:{value:null},metalnessMapTransform:{value:new dm}},roughnessmap:{roughnessMap:{value:null},roughnessMapTransform:{value:new dm}},gradientmap:{gradientMap:{value:null}},fog:{fogDensity:{value:25e-5},fogNear:{value:1},fogFar:{value:2e3},fogColor:{value:new Y(16777215)}},lights:{ambientLightColor:{value:[]},lightProbe:{value:[]},sunLights:{value:[],properties:{direction:{},color:{}}},sunLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},sunShadowMatrix:{value:[]},sunShadowCascade:{value:[]},directionalLights:{value:[],properties:{direction:{},color:{}}},directionalLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},directionalShadowMatrix:{value:[]},spotLights:{value:[],properties:{color:{},position:{},direction:{},distance:{},coneCos:{},penumbraCos:{},decay:{}}},spotLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},spotLightMap:{value:[]},spotLightMatrix:{value:[]},pointLights:{value:[],properties:{color:{},position:{},decay:{},distance:{}}},pointLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{},shadowCameraNear:{},shadowCameraFar:{}}},pointShadowMatrix:{value:[]},hemisphereLights:{value:[],properties:{direction:{},skyColor:{},groundColor:{}}},rectAreaLights:{value:[],properties:{color:{},position:{},width:{},height:{}}},ltc_1:{value:null},ltc_2:{value:null},probesSH:{value:null},probesMin:{value:new J},probesMax:{value:new J},probesResolution:{value:new J}},points:{diffuse:{value:new Y(16777215)},opacity:{value:1},size:{value:1},scale:{value:1},map:{value:null},alphaMap:{value:null},alphaMapTransform:{value:new dm},alphaTest:{value:0},uvTransform:{value:new dm}},sprite:{diffuse:{value:new Y(16777215)},opacity:{value:1},center:{value:new q(.5,.5)},rotation:{value:0},map:{value:null},mapTransform:{value:new dm},alphaMap:{value:null},alphaMapTransform:{value:new dm},alphaTest:{value:0}}},bb={basic:{uniforms:Tv([X.common,X.specularmap,X.envmap,X.aomap,X.lightmap,X.fog]),vertexShader:yb.meshbasic_vert,fragmentShader:yb.meshbasic_frag},lambert:{uniforms:Tv([X.common,X.specularmap,X.envmap,X.aomap,X.lightmap,X.emissivemap,X.bumpmap,X.normalmap,X.displacementmap,X.fog,X.lights,{emissive:{value:new Y(0)},envMapIntensity:{value:1}}]),vertexShader:yb.meshlambert_vert,fragmentShader:yb.meshlambert_frag},phong:{uniforms:Tv([X.common,X.specularmap,X.envmap,X.aomap,X.lightmap,X.emissivemap,X.bumpmap,X.normalmap,X.displacementmap,X.fog,X.lights,{emissive:{value:new Y(0)},specular:{value:new Y(1118481)},shininess:{value:30},envMapIntensity:{value:1}}]),vertexShader:yb.meshphong_vert,fragmentShader:yb.meshphong_frag},standard:{uniforms:Tv([X.common,X.envmap,X.aomap,X.lightmap,X.emissivemap,X.bumpmap,X.normalmap,X.displacementmap,X.roughnessmap,X.metalnessmap,X.fog,X.lights,{emissive:{value:new Y(0)},roughness:{value:1},metalness:{value:0},envMapIntensity:{value:1}}]),vertexShader:yb.meshphysical_vert,fragmentShader:yb.meshphysical_frag},toon:{uniforms:Tv([X.common,X.aomap,X.lightmap,X.emissivemap,X.bumpmap,X.normalmap,X.displacementmap,X.gradientmap,X.fog,X.lights,{emissive:{value:new Y(0)}}]),vertexShader:yb.meshtoon_vert,fragmentShader:yb.meshtoon_frag},matcap:{uniforms:Tv([X.common,X.bumpmap,X.normalmap,X.displacementmap,X.fog,{matcap:{value:null}}]),vertexShader:yb.meshmatcap_vert,fragmentShader:yb.meshmatcap_frag},points:{uniforms:Tv([X.points,X.fog]),vertexShader:yb.points_vert,fragmentShader:yb.points_frag},dashed:{uniforms:Tv([X.common,X.fog,{scale:{value:1},dashSize:{value:1},totalSize:{value:2}}]),vertexShader:yb.linedashed_vert,fragmentShader:yb.linedashed_frag},depth:{uniforms:Tv([X.common,X.displacementmap]),vertexShader:yb.depth_vert,fragmentShader:yb.depth_frag},normal:{uniforms:Tv([X.common,X.bumpmap,X.normalmap,X.displacementmap,{opacity:{value:1}}]),vertexShader:yb.meshnormal_vert,fragmentShader:yb.meshnormal_frag},sprite:{uniforms:Tv([X.sprite,X.fog]),vertexShader:yb.sprite_vert,fragmentShader:yb.sprite_frag},background:{uniforms:{uvTransform:{value:new dm},t2D:{value:null},backgroundIntensity:{value:1}},vertexShader:yb.background_vert,fragmentShader:yb.background_frag},backgroundCube:{uniforms:{envMap:{value:null},backgroundBlurriness:{value:0},backgroundIntensity:{value:1},backgroundRotation:{value:new dm}},vertexShader:yb.backgroundCube_vert,fragmentShader:yb.backgroundCube_frag},cube:{uniforms:{tCube:{value:null},tFlip:{value:-1},opacity:{value:1}},vertexShader:yb.cube_vert,fragmentShader:yb.cube_frag},equirect:{uniforms:{tEquirect:{value:null}},vertexShader:yb.equirect_vert,fragmentShader:yb.equirect_frag},distance:{uniforms:Tv([X.common,X.displacementmap,{referencePosition:{value:new J},nearDistance:{value:1},farDistance:{value:1e3}}]),vertexShader:yb.distance_vert,fragmentShader:yb.distance_frag},shadow:{uniforms:Tv([X.lights,X.fog,{color:{value:new Y(0)},opacity:{value:1}}]),vertexShader:yb.shadow_vert,fragmentShader:yb.shadow_frag}};bb.physical={uniforms:Tv([bb.standard.uniforms,{clearcoat:{value:0},clearcoatMap:{value:null},clearcoatMapTransform:{value:new dm},clearcoatNormalMap:{value:null},clearcoatNormalMapTransform:{value:new dm},clearcoatNormalScale:{value:new q(1,1)},clearcoatRoughness:{value:0},clearcoatRoughnessMap:{value:null},clearcoatRoughnessMapTransform:{value:new dm},dispersion:{value:0},retroreflectivity:{value:0},iridescence:{value:0},iridescenceMap:{value:null},iridescenceMapTransform:{value:new dm},iridescenceIOR:{value:1.3},iridescenceThicknessMinimum:{value:100},iridescenceThicknessMaximum:{value:400},iridescenceThicknessMap:{value:null},iridescenceThicknessMapTransform:{value:new dm},sheen:{value:0},sheenColor:{value:new Y(0)},sheenColorMap:{value:null},sheenColorMapTransform:{value:new dm},sheenRoughness:{value:1},sheenRoughnessMap:{value:null},sheenRoughnessMapTransform:{value:new dm},transmission:{value:0},transmissionMap:{value:null},transmissionMapTransform:{value:new dm},transmissionSamplerSize:{value:new q},transmissionSamplerMap:{value:null},thickness:{value:0},thicknessMap:{value:null},thicknessMapTransform:{value:new dm},attenuationDistance:{value:0},attenuationColor:{value:new Y(0)},specularColor:{value:new Y(1,1,1)},specularColorMap:{value:null},specularColorMapTransform:{value:new dm},specularIntensity:{value:1},specularIntensityMap:{value:null},specularIntensityMapTransform:{value:new dm},anisotropyVector:{value:new q},anisotropyMap:{value:null},anisotropyMapTransform:{value:new dm}}]),vertexShader:yb.meshphysical_vert,fragmentShader:yb.meshphysical_frag};var xb={r:0,b:0,g:0},Sb=new Mm,Cb=new dm;Cb.set(-1,0,0,0,1,0,0,0,1);function wb(e,t,n,r,i,a){let o=new Y(0),s=i===!0?0:1,c,l,u=null,d=0,f=null;function p(e){let n=e.isScene===!0?e.background:null;if(n&&n.isTexture){let r=e.backgroundBlurriness>0;n=t.get(n,r)}return n}function m(t){let r=!1,i=p(t);i===null?g(o,s):i&&i.isColor&&(g(i,1),r=!0);let c=e.xr.getEnvironmentBlendMode();c===`additive`?n.buffers.color.setClear(0,0,0,1,a):c===`alpha-blend`&&n.buffers.color.setClear(0,0,0,0,a),(e.autoClear||r)&&(n.buffers.depth.setTest(!0),n.buffers.depth.setMask(!0),n.buffers.color.setMask(!0),e.clear(e.autoClearColor,e.autoClearDepth,e.autoClearStencil))}function h(t,n){let i=p(n);i&&(i.isCubeTexture||i.mapping===306)?(l===void 0&&(l=new Ig(new z_(1,1,1),new Mv({name:`BackgroundCubeMaterial`,uniforms:wv(bb.backgroundCube.uniforms),vertexShader:bb.backgroundCube.vertexShader,fragmentShader:bb.backgroundCube.fragmentShader,side:1,depthTest:!1,depthWrite:!1,fog:!1,allowOverride:!1})),l.geometry.deleteAttribute(`normal`),l.geometry.deleteAttribute(`uv`),l.onBeforeRender=function(e,t,n){this.matrixWorld.copyPosition(n.matrixWorld)},Object.defineProperty(l.material,"envMap",{get:function(){return this.uniforms.envMap.value}}),r.update(l)),l.material.uniforms.envMap.value=i,l.material.uniforms.backgroundBlurriness.value=n.backgroundBlurriness,l.material.uniforms.backgroundIntensity.value=n.backgroundIntensity,l.material.uniforms.backgroundRotation.value.setFromMatrix4(Sb.makeRotationFromEuler(n.backgroundRotation)).transpose(),i.isCubeTexture&&i.isRenderTargetTexture===!1&&l.material.uniforms.backgroundRotation.value.premultiply(Cb),l.material.toneMapped=gm.getTransfer(i.colorSpace)!==vp,(u!==i||d!==i.version||f!==e.toneMapping)&&(l.material.needsUpdate=!0,u=i,d=i.version,f=e.toneMapping),l.layers.enableAll(),t.unshift(l,l.geometry,l.material,0,0,null)):i&&i.isTexture&&(c===void 0&&(c=new Ig(new bv(2,2),new Mv({name:`BackgroundMaterial`,uniforms:wv(bb.background.uniforms),vertexShader:bb.background.vertexShader,fragmentShader:bb.background.fragmentShader,side:0,depthTest:!1,depthWrite:!1,fog:!1,allowOverride:!1})),c.geometry.deleteAttribute(`normal`),Object.defineProperty(c.material,"map",{get:function(){return this.uniforms.t2D.value}}),r.update(c)),c.material.uniforms.t2D.value=i,c.material.uniforms.backgroundIntensity.value=n.backgroundIntensity,c.material.toneMapped=gm.getTransfer(i.colorSpace)!==vp,i.matrixAutoUpdate===!0&&i.updateMatrix(),c.material.uniforms.uvTransform.value.copy(i.matrix),(u!==i||d!==i.version||f!==e.toneMapping)&&(c.material.needsUpdate=!0,u=i,d=i.version,f=e.toneMapping),c.layers.enableAll(),t.unshift(c,c.geometry,c.material,0,0,null))}function g(t,r){t.getRGB(xb,Ov(e)),n.buffers.color.setClear(xb.r,xb.g,xb.b,r,a)}function _(){l!==void 0&&(l.geometry.dispose(),l.material.dispose(),l=void 0),c!==void 0&&(c.geometry.dispose(),c.material.dispose(),c=void 0)}return{getClearColor:function(){return o},setClearColor:function(e,t=1){o.set(e),s=t,g(o,s)},getClearAlpha:function(){return s},setClearAlpha:function(e){s=e,g(o,s)},render:m,addToRenderList:h,dispose:_}}function Tb(e,t){let n=e.getParameter(e.MAX_VERTEX_ATTRIBS),r={},i=f(null),a=i,o=!1;function s(n,r,i,s,c){let u=!1,f=d(n,s,i,r);a!==f&&(a=f,l(a.object)),u=p(n,s,i,c),u&&m(n,s,i,c),c!==null&&t.update(c,e.ELEMENT_ARRAY_BUFFER),(u||o)&&(o=!1,b(n,r,i,s),c!==null&&e.bindBuffer(e.ELEMENT_ARRAY_BUFFER,t.get(c).buffer))}function c(){return e.createVertexArray()}function l(t){return e.bindVertexArray(t)}function u(t){return e.deleteVertexArray(t)}function d(e,t,n,i){let a=i.wireframe===!0,o=r[t.id];o===void 0&&(o={},r[t.id]=o);let s=e.isInstancedMesh===!0?e.id:0,l=o[s];l===void 0&&(l={},o[s]=l);let u=l[n.id];u===void 0&&(u={},l[n.id]=u);let d=u[a];return d===void 0&&(d=f(c()),u[a]=d),d}function f(e){let t=[],r=[],i=[];for(let e=0;e=0){let n=i[t],r=o[t];if(r===void 0&&(t===`instanceMatrix`&&e.instanceMatrix&&(r=e.instanceMatrix),t===`instanceColor`&&e.instanceColor&&(r=e.instanceColor)),n===void 0||n.attribute!==r||r&&n.data!==r.data)return!0;s++}return a.attributesNum!==s||a.index!==r}function m(e,t,n,r){let i={},o=t.attributes,s=0,c=n.getAttributes();for(let t in c)if(c[t].location>=0){let n=o[t];n===void 0&&(t===`instanceMatrix`&&e.instanceMatrix&&(n=e.instanceMatrix),t===`instanceColor`&&e.instanceColor&&(n=e.instanceColor));let r={};r.attribute=n,n&&n.data&&(r.data=n.data),i[t]=r,s++}a.attributes=i,a.attributesNum=s,a.index=r}function h(){let e=a.newAttributes;for(let t=0,n=e.length;t=0){let s=o[r];if(s===void 0&&(r===`instanceMatrix`&&n.instanceMatrix&&(s=n.instanceMatrix),r===`instanceColor`&&n.instanceColor&&(s=n.instanceColor)),s!==void 0){let r=s.normalized,o=s.itemSize,c=t.get(s);if(c===void 0)continue;let l=c.buffer,u=c.type,d=c.bytesPerElement,f=u===e.INT||u===e.UNSIGNED_INT||s.gpuType===1013;if(s.isInterleavedBufferAttribute){let t=s.data,c=t.stride,p=s.offset;if(t.isInstancedInterleavedBuffer){for(let e=0;e0&&e.getShaderPrecisionFormat(e.FRAGMENT_SHADER,e.HIGH_FLOAT).precision>0)return`highp`;t=`mediump`}return t===`mediump`&&e.getShaderPrecisionFormat(e.VERTEX_SHADER,e.MEDIUM_FLOAT).precision>0&&e.getShaderPrecisionFormat(e.FRAGMENT_SHADER,e.MEDIUM_FLOAT).precision>0?`mediump`:`lowp`}let l=n.precision===void 0?`highp`:n.precision,u=c(l);u!==l&&(Ap(`WebGLRenderer:`,l,`not supported, using`,u,`instead.`),l=u);let d=n.logarithmicDepthBuffer===!0,f=n.reversedDepthBuffer===!0&&t.has(`EXT_clip_control`);n.reversedDepthBuffer===!0&&f===!1&&Ap(`WebGLRenderer: Unable to use reversed depth buffer due to missing EXT_clip_control extension. Fallback to default depth buffer.`);let p=e.getParameter(e.MAX_TEXTURE_IMAGE_UNITS),m=e.getParameter(e.MAX_VERTEX_TEXTURE_IMAGE_UNITS),h=e.getParameter(e.MAX_TEXTURE_SIZE),g=e.getParameter(e.MAX_CUBE_MAP_TEXTURE_SIZE),_=e.getParameter(e.MAX_VERTEX_ATTRIBS),v=e.getParameter(e.MAX_VERTEX_UNIFORM_VECTORS),y=e.getParameter(e.MAX_VARYING_VECTORS),b=e.getParameter(e.MAX_FRAGMENT_UNIFORM_VECTORS),x=e.getParameter(e.MAX_SAMPLES),S=e.getParameter(e.SAMPLES);return{isWebGL2:!0,getMaxAnisotropy:a,getMaxPrecision:c,textureFormatReadable:o,textureTypeReadable:s,precision:l,logarithmicDepthBuffer:d,reversedDepthBuffer:f,maxTextures:p,maxVertexTextures:m,maxTextureSize:h,maxCubemapSize:g,maxAttributes:_,maxVertexUniforms:v,maxVaryings:y,maxFragmentUniforms:b,maxSamples:x,samples:S}}function Ob(e){let t=this,n=null,r=0,i=!1,a=!1,o=new gg,s=new dm,c={value:null,needsUpdate:!1};this.uniform=c,this.numPlanes=0,this.numIntersection=0,this.init=function(e,t){let n=e.length!==0||t||r!==0||i;return i=t,r=e.length,n},this.beginShadows=function(){a=!0,u(null)},this.endShadows=function(){a=!1},this.setGlobalState=function(e,t){n=u(e,t,0)},this.setState=function(t,o,s){let d=t.clippingPlanes,f=t.clipIntersection,p=t.clipShadows,m=e.get(t);if(!i||d===null||d.length===0||a&&!p)a?u(null):l();else{let e=a?0:r,t=e*4,i=m.clippingState||null;c.value=i,i=u(d,o,t,s);for(let e=0;e!==t;++e)i[e]=n[e];m.clippingState=i,this.numIntersection=f?this.numPlanes:0,this.numPlanes+=e}};function l(){c.value!==n&&(c.value=n,c.needsUpdate=r>0),t.numPlanes=r,t.numIntersection=0}function u(e,n,r,i){let a=e===null?0:e.length,l=null;if(a!==0){if(l=c.value,i!==!0||l===null){let t=r+a*4,i=n.matrixWorldInverse;s.getNormalMatrix(i),(l===null||l.length0&&this._blur(s,0,0,t),this._applyPMREM(s),this._cleanup(s),s}fromEquirectangular(e,t=null){return this._fromTexture(e,t)}fromCubemap(e,t=null){return this._fromTexture(e,t)}compileCubemapShader(){this._cubemapMaterial===null&&(this._cubemapMaterial=Jb(),this._compileMaterial(this._cubemapMaterial))}compileEquirectangularShader(){this._equirectMaterial===null&&(this._equirectMaterial=qb(),this._compileMaterial(this._equirectMaterial))}dispose(){this._dispose(),this._cubemapMaterial!==null&&this._cubemapMaterial.dispose(),this._equirectMaterial!==null&&this._equirectMaterial.dispose(),this._backgroundBox!==null&&(this._backgroundBox.geometry.dispose(),this._backgroundBox.material.dispose())}_setSize(e){this._lodMax=Math.floor(Math.log2(e)),this._cubeSize=2**this._lodMax}_dispose(){this._blurMaterial!==null&&this._blurMaterial.dispose(),this._ggxMaterial!==null&&this._ggxMaterial.dispose(),this._pingPongRenderTarget!==null&&this._pingPongRenderTarget.dispose();for(let e=0;e2?l:0,l,l),c.setRenderTarget(r),p&&c.render(d,a),c.render(e,a)}c.toneMapping=u,c.autoClear=l,e.background=m}_textureToCubeUV(e,t){let n=this._renderer,r=e.mapping===301||e.mapping===302;r?(this._cubemapMaterial===null&&(this._cubemapMaterial=Jb()),this._cubemapMaterial.uniforms.flipEnvMap.value=e.isRenderTargetTexture===!1?-1:1):this._equirectMaterial===null&&(this._equirectMaterial=qb());let i=r?this._cubemapMaterial:this._equirectMaterial,a=this._lodMeshes[0];a.material=i;let o=i.uniforms;o.envMap.value=e;let s=this._cubeSize;Wb(t,0,0,3*s,2*s),n.setRenderTarget(t),n.render(a,Nb)}_applyPMREM(e){let t=this._renderer,n=t.autoClear;t.autoClear=!1;let r=this._lodMeshes.length;for(let t=1;td-kb?n-d+kb:0),m=4*(this._cubeSize-f);s.envMap.value=e.texture,s.roughness.value=u,s.mipInt.value=d-t,Wb(i,p,m,3*f,2*f),r.setRenderTarget(i),r.render(o,Nb),s.envMap.value=i.texture,s.roughness.value=0,s.mipInt.value=d-n,Wb(e,p,m,3*f,2*f),r.setRenderTarget(e),r.render(o,Nb)}_blur(e,t,n,r){let i=this._pingPongRenderTarget,a=Math.min(r,Math.PI)/Math.SQRT2;this._blurPass(e,i,t,n,a),this._blurPass(i,e,n,n,a)}_blurPass(e,t,n,r,i){let a=this._renderer,o=this._blurMaterial,s=this._lodMeshes[r];s.material=o;let c=o.uniforms;c.envMap.value=e.texture,c.sigma.value=i,c.mipInt.value=this._lodMax-n;let l=this._sizeLods[r];Wb(t,3*l*(r>this._lodMax-kb?r-this._lodMax+kb:0),4*(this._cubeSize-l),3*l,2*l),a.setRenderTarget(t),a.render(s,Nb)}};function Hb(e){let t=[],n=[],r=e,i=e-kb+1+Ab;for(let e=0;e2?0:-1,r=[t,n,0,t+2/3,n,0,t+2/3,n+1,0,t,n,0,t+2/3,n+1,0,t,n+1,0];c.set(r,18*e);for(let t=0;t<6;t++){let n=s[t*2]*2-1,r=s[t*2+1]*2-1;e===0?Bb.set(1,r,n):e===1?Bb.set(-n,1,-r):e===2?Bb.set(-n,r,1):e===3?Bb.set(-1,r,-n):e===4?Bb.set(-n,-1,r):Bb.set(n,r,-1),Bb.toArray(l,(e*6+t)*3)}}let u=new lg;u.setAttribute(`position`,new Jh(c,3)),u.setAttribute(`outputDirection`,new Jh(l,3)),n.push(new Ig(u,null)),r>kb&&r--}return{lodMeshes:n,sizeLods:t}}function Ub(e,t,n){let r=new km(e,t,n);return r.texture.mapping=306,r.texture.name=`PMREM.cubeUv`,r.scissorTest=!0,r}function Wb(e,t,n,r,i){e.viewport.set(t,n,r,i),e.scissor.set(t,n,r,i)}function Gb(e,t,n){return new Mv({name:`PMREMGGXConvolution`,defines:{GGX_SAMPLES:Mb,CUBEUV_TEXEL_WIDTH:1/t,CUBEUV_TEXEL_HEIGHT:1/n,CUBEUV_MAX_MIP:`${e}.0`},uniforms:{envMap:{value:null},roughness:{value:0},mipInt:{value:0}},vertexShader:Yb(),fragmentShader:` precision highp float; precision highp int; @@ -3934,7 +3934,7 @@ void main() { gl_FragColor = vec4(prefilteredColor, 1.0); } - `,blending:0,depthTest:!1,depthWrite:!1})}function __(e,t,n){return new ah({name:`SphericalGaussianBlur`,defines:{SAMPLES:n_,CUBEUV_TEXEL_WIDTH:1/t,CUBEUV_TEXEL_HEIGHT:1/n,CUBEUV_MAX_MIP:`${e}.0`},uniforms:{envMap:{value:null},sigma:{value:0},mipInt:{value:0}},vertexShader:b_(),fragmentShader:` + `,blending:0,depthTest:!1,depthWrite:!1})}function Kb(e,t,n){return new Mv({name:`SphericalGaussianBlur`,defines:{SAMPLES:jb,CUBEUV_TEXEL_WIDTH:1/t,CUBEUV_TEXEL_HEIGHT:1/n,CUBEUV_MAX_MIP:`${e}.0`},uniforms:{envMap:{value:null},sigma:{value:0},mipInt:{value:0}},vertexShader:Yb(),fragmentShader:` precision highp float; precision highp int; @@ -3994,7 +3994,7 @@ void main() { gl_FragColor = vec4( accumColor / accumWeight, 1.0 ); } - `,blending:0,depthTest:!1,depthWrite:!1})}function v_(){return new ah({name:`EquirectangularToCubeUV`,uniforms:{envMap:{value:null}},vertexShader:b_(),fragmentShader:` + `,blending:0,depthTest:!1,depthWrite:!1})}function qb(){return new Mv({name:`EquirectangularToCubeUV`,uniforms:{envMap:{value:null}},vertexShader:Yb(),fragmentShader:` precision mediump float; precision mediump int; @@ -4013,7 +4013,7 @@ void main() { gl_FragColor = vec4( texture2D ( envMap, uv ).rgb, 1.0 ); } - `,blending:0,depthTest:!1,depthWrite:!1})}function y_(){return new ah({name:`CubemapToCubeUV`,uniforms:{envMap:{value:null},flipEnvMap:{value:-1}},vertexShader:b_(),fragmentShader:` + `,blending:0,depthTest:!1,depthWrite:!1})}function Jb(){return new Mv({name:`CubemapToCubeUV`,uniforms:{envMap:{value:null},flipEnvMap:{value:-1}},vertexShader:Yb(),fragmentShader:` precision mediump float; precision mediump int; @@ -4029,7 +4029,7 @@ void main() { gl_FragColor = textureCube( envMap, vec3( flipEnvMap * vOutputDirection.x, vOutputDirection.yz ) ); } - `,blending:0,depthTest:!1,depthWrite:!1})}function b_(){return` + `,blending:0,depthTest:!1,depthWrite:!1})}function Yb(){return` precision mediump float; precision mediump int; @@ -4044,7 +4044,7 @@ void main() { gl_Position = vec4( position, 1.0 ); } - `}var x_=class extends nd{constructor(e=1,t={}){super(e,e,t),this.isWebGLCubeRenderTarget=!0;let n={width:e,height:e,depth:1},r=[n,n,n,n,n,n];this.texture=new cm(r),this._setTextureOptions(t),this.texture.isRenderTargetTexture=!0}fromEquirectangularTexture(e,t){this.texture.type=t.type,this.texture.colorSpace=t.colorSpace,this.texture.generateMipmaps=t.generateMipmaps,this.texture.minFilter=t.minFilter,this.texture.magFilter=t.magFilter;let n={uniforms:{tEquirect:{value:null}},vertexShader:` + `}var Xb=class extends km{constructor(e=1,t={}){super(e,e,t),this.isWebGLCubeRenderTarget=!0;let n={width:e,height:e,depth:1},r=[n,n,n,n,n,n];this.texture=new P_(r),this._setTextureOptions(t),this.texture.isRenderTargetTexture=!0}fromEquirectangularTexture(e,t){this.texture.type=t.type,this.texture.colorSpace=t.colorSpace,this.texture.generateMipmaps=t.generateMipmaps,this.texture.minFilter=t.minFilter,this.texture.magFilter=t.magFilter;let n={uniforms:{tEquirect:{value:null}},vertexShader:` varying vec3 vWorldDirection; @@ -4079,7 +4079,7 @@ void main() { gl_FragColor = texture2D( tEquirect, sampleUV ); } - `},r=new pm(5,5,5),i=new ah({name:`CubemapFromEquirect`,uniforms:Zm(n.uniforms),vertexShader:n.vertexShader,fragmentShader:n.fragmentShader,side:1,blending:0});i.uniforms.tEquirect.value=t;let a=new up(r,i),o=t.minFilter;return t.minFilter===1008&&(t.minFilter=wc),new vg(1,10,this).update(e,a),t.minFilter=o,a.geometry.dispose(),a.material.dispose(),this}clear(e,t=!0,n=!0,r=!0){let i=e.getRenderTarget();for(let i=0;i<6;i++)e.setRenderTarget(this,i),e.clear(t,n,r);e.setRenderTarget(i)}};function S_(e){let t=new WeakMap,n=new WeakMap,r=null;function i(e,t=!1){return e==null?null:t?o(e):a(e)}function a(n){if(n&&n.isTexture){let r=n.mapping;if(r===303||r===304)if(t.has(n)){let e=t.get(n).texture;return s(e,n.mapping)}else{let r=n.image;if(r&&r.height>0){let i=new x_(r.height);return i.fromEquirectangularTexture(e,n),t.set(n,i),n.addEventListener(`dispose`,l),s(i.texture,n.mapping)}else return null}}return n}function o(t){if(t&&t.isTexture){let i=t.mapping,a=i===303||i===304,o=i===301||i===302;if(a||o){let i=n.get(t),s=i===void 0?0:i.texture.pmremVersion;if(t.isRenderTargetTexture&&t.pmremVersion!==s)return r===null&&(r=new f_(e)),i=a?r.fromEquirectangular(t,i):r.fromCubemap(t,i),i.texture.pmremVersion=t.pmremVersion,n.set(t,i),i.texture;if(i!==void 0)return i.texture;{let s=t.image;return a&&s&&s.height>0||o&&s&&c(s)?(r===null&&(r=new f_(e)),i=a?r.fromEquirectangular(t):r.fromCubemap(t),i.texture.pmremVersion=t.pmremVersion,n.set(t,i),t.addEventListener(`dispose`,u),i.texture):null}}}return t}function s(e,t){return t===303?e.mapping=301:t===304&&(e.mapping=302),e}function c(e){let t=0;for(let n=0;n<6;n++)e[n]!==void 0&&t++;return t===6}function l(e){let n=e.target;n.removeEventListener(`dispose`,l);let r=t.get(n);r!==void 0&&(t.delete(n),r.dispose())}function u(e){let t=e.target;t.removeEventListener(`dispose`,u);let r=n.get(t);r!==void 0&&(n.delete(t),r.dispose())}function d(){t=new WeakMap,n=new WeakMap,r!==null&&(r.dispose(),r=null)}return{get:i,dispose:d}}function C_(e){let t={};function n(n){if(t[n]!==void 0)return t[n];let r=e.getExtension(n);return t[n]=r,r}return{has:function(e){return n(e)!==null},init:function(){n(`EXT_color_buffer_float`),n(`WEBGL_clip_cull_distance`),n(`OES_texture_float_linear`),n(`EXT_color_buffer_half_float`),n(`WEBGL_multisampled_render_to_texture`),n(`WEBGL_render_shared_exponent`)},get:function(e){let t=n(e);return t===null&&iu(`WebGLRenderer: `+e+` extension not supported.`),t}}}function w_(e,t,n,r){let i={},a=new WeakMap;function o(e){let s=e.target;s.index!==null&&t.remove(s.index);for(let e in s.attributes)t.remove(s.attributes[e]);s.removeEventListener(`dispose`,o),delete i[s.id];let c=a.get(s);c&&(t.remove(c),a.delete(s)),r.releaseStatesOfGeometry(s),s.isInstancedBufferGeometry===!0&&delete s._maxInstanceCount,n.memory.geometries--}function s(e,t){return i[t.id]===!0?t:(t.addEventListener(`dispose`,o),i[t.id]=!0,n.memory.geometries++,t)}function c(n){let r=n.attributes;for(let n in r)t.update(r[n],e.ARRAY_BUFFER)}function l(e){let n=[],r=e.index,i=e.attributes.position,o=0;if(i===void 0)return;if(r!==null){let e=r.array;o=r.version;for(let t=0,r=e.length;t=65535?wf:Cf)(n,1);s.version=o;let c=a.get(e);c&&t.remove(c),a.set(e,s)}function u(e){let t=a.get(e);if(t){let n=e.index;n!==null&&t.versiont.maxTextureSize&&(m=Math.ceil(p/t.maxTextureSize),p=t.maxTextureSize);let h=new Float32Array(p*m*4*u),g=new rd(h,p,m,u);g.type=Nc,g.needsUpdate=!0;let _=f*4;for(let t=0;t0){let i=new Xb(r.height);return i.fromEquirectangularTexture(e,n),t.set(n,i),n.addEventListener(`dispose`,l),s(i.texture,n.mapping)}else return null}}return n}function o(t){if(t&&t.isTexture){let i=t.mapping,a=i===303||i===304,o=i===301||i===302;if(a||o){let i=n.get(t),s=i===void 0?0:i.texture.pmremVersion;if(t.isRenderTargetTexture&&t.pmremVersion!==s)return r===null&&(r=new Vb(e)),i=a?r.fromEquirectangular(t,i):r.fromCubemap(t,i),i.texture.pmremVersion=t.pmremVersion,n.set(t,i),i.texture;if(i!==void 0)return i.texture;{let s=t.image;return a&&s&&s.height>0||o&&s&&c(s)?(r===null&&(r=new Vb(e)),i=a?r.fromEquirectangular(t):r.fromCubemap(t),i.texture.pmremVersion=t.pmremVersion,n.set(t,i),t.addEventListener(`dispose`,u),i.texture):null}}}return t}function s(e,t){return t===303?e.mapping=301:t===304&&(e.mapping=302),e}function c(e){let t=0;for(let n=0;n<6;n++)e[n]!==void 0&&t++;return t===6}function l(e){let n=e.target;n.removeEventListener(`dispose`,l);let r=t.get(n);r!==void 0&&(t.delete(n),r.dispose())}function u(e){let t=e.target;t.removeEventListener(`dispose`,u);let r=n.get(t);r!==void 0&&(n.delete(t),r.dispose())}function d(){t=new WeakMap,n=new WeakMap,r!==null&&(r.dispose(),r=null)}return{get:i,dispose:d}}function Qb(e){let t={};function n(n){if(t[n]!==void 0)return t[n];let r=e.getExtension(n);return t[n]=r,r}return{has:function(e){return n(e)!==null},init:function(){n(`EXT_color_buffer_float`),n(`WEBGL_clip_cull_distance`),n(`OES_texture_float_linear`),n(`EXT_color_buffer_half_float`),n(`WEBGL_multisampled_render_to_texture`),n(`WEBGL_render_shared_exponent`)},get:function(e){let t=n(e);return t===null&&Mp(`WebGLRenderer: `+e+` extension not supported.`),t}}}function $b(e,t,n,r){let i={},a=new WeakMap;function o(e){let s=e.target;s.index!==null&&t.remove(s.index);for(let e in s.attributes)t.remove(s.attributes[e]);s.removeEventListener(`dispose`,o),delete i[s.id];let c=a.get(s);c&&(t.remove(c),a.delete(s)),r.releaseStatesOfGeometry(s),s.isInstancedBufferGeometry===!0&&delete s._maxInstanceCount,n.memory.geometries--}function s(e,t){return i[t.id]===!0?t:(t.addEventListener(`dispose`,o),i[t.id]=!0,n.memory.geometries++,t)}function c(n){let r=n.attributes;for(let n in r)t.update(r[n],e.ARRAY_BUFFER)}function l(e){let n=[],r=e.index,i=e.attributes.position,o=0;if(i===void 0)return;if(r!==null){let e=r.array;o=r.version;for(let t=0,r=e.length;t=65535?Xh:Yh)(n,1);s.version=o;let c=a.get(e);c&&t.remove(c),a.set(e,s)}function u(e){let t=a.get(e);if(t){let n=e.index;n!==null&&t.versiont.maxTextureSize&&(m=Math.ceil(p/t.maxTextureSize),p=t.maxTextureSize);let h=new Float32Array(p*m*4*u),g=new Am(h,p,m,u);g.type=sf,g.needsUpdate=!0;let _=f*4;for(let t=0;t0&&v[0].isRenderPass===!0;let t=o.width,n=o.height;v.length>0&&s===null&&(s=new nd(t,n,{type:Pc,depthBuffer:!1,stencilBuffer:!1}),c=new nd(t,n,{type:Pc,depthBuffer:!1,stencilBuffer:!1}));for(let e=0;e0)return e;let i=t*n,a=I_[i];if(a===void 0&&(a=new Float32Array(i),I_[i]=a),t!==0){r.toArray(a,0);for(let r=1,i=0;r!==t;++r)i+=n,e[r].toArray(a,i)}return a}function H_(e,t){if(e.length!==t.length)return!1;for(let n=0,r=e.length;n0&&(this.seq=r.concat(i))}setValue(e,t,n,r){let i=this.map[t];i!==void 0&&i.setValue(e,n,r)}setOptional(e,t,n){let r=t[n];r!==void 0&&this.setValue(e,n,r)}static upload(e,t,n,r){for(let i=0,a=t.length;i!==a;++i){let a=t[i],o=n[a.id];o.needsUpdate!==!1&&a.setValue(e,o.value,r)}}static seqWithValue(e,t){let n=[];for(let r=0,i=e.length;r!==i;++r){let i=e[r];i.id in t&&n.push(i)}return n}};function Rv(e,t,n){let r=e.createShader(t);return e.shaderSource(r,n),e.compileShader(r),r}var zv=37297,Bv=0;function Vv(e,t){let n=e.split(` + }`,depthTest:!1,depthWrite:!1}),d=new Ig(l,u),f=new Ly(-1,1,1,-1,0,1),p=null,m=null,h=!1,g,_=null,v=[],y=!1;this.setSize=function(e,t){o.setSize(e,t),s!==null&&s.setSize(e,t),c!==null&&c.setSize(e,t);for(let n=0;n0&&v[0].isRenderPass===!0;let t=o.width,n=o.height;v.length>0&&s===null&&(s=new km(t,n,{type:cf,depthBuffer:!1,stencilBuffer:!1}),c=new km(t,n,{type:cf,depthBuffer:!1,stencilBuffer:!1}));for(let e=0;e0)return e;let i=t*n,a=dx[i];if(a===void 0&&(a=new Float32Array(i),dx[i]=a),t!==0){r.toArray(a,0);for(let r=1,i=0;r!==t;++r)i+=n,e[r].toArray(a,i)}return a}function _x(e,t){if(e.length!==t.length)return!1;for(let n=0,r=e.length;n0&&(this.seq=r.concat(i))}setValue(e,t,n,r){let i=this.map[t];i!==void 0&&i.setValue(e,n,r)}setOptional(e,t,n){let r=t[n];r!==void 0&&this.setValue(e,n,r)}static upload(e,t,n,r){for(let i=0,a=t.length;i!==a;++i){let a=t[i],o=n[a.id];o.needsUpdate!==!1&&a.setValue(e,o.value,r)}}static seqWithValue(e,t){let n=[];for(let r=0,i=e.length;r!==i;++r){let i=e[r];i.id in t&&n.push(i)}return n}};function pS(e,t,n){let r=e.createShader(t);return e.shaderSource(r,n),e.compileShader(r),r}var mS=37297,hS=0;function gS(e,t){let n=e.split(` `),r=[],i=Math.max(t-6,0),a=Math.min(t+6,n.length);for(let e=i;e`:` `} ${i}: ${n[e]}`)}return r.join(` -`)}var Hv=new Ru;function Uv(e){Uu._getMatrix(Hv,Uu.workingColorSpace,e);let t=`mat3( ${Hv.elements.map(e=>e.toFixed(4))} )`;switch(Uu.getTransfer(e)){case Ul:return[t,`LinearTransferOETF`];case Wl:return[t,`sRGBTransferOETF`];default:return nu(`WebGLProgram: Unsupported color space: `,e),[t,`LinearTransferOETF`]}}function Wv(e,t,n){let r=e.getShaderParameter(t,e.COMPILE_STATUS),i=(e.getShaderInfoLog(t)||``).trim();if(r&&i===``)return``;let a=/ERROR: 0:(\d+)/.exec(i);if(a){let r=parseInt(a[1]);return n.toUpperCase()+` +`)}var _S=new dm;function vS(e){gm._getMatrix(_S,gm.workingColorSpace,e);let t=`mat3( ${_S.elements.map(e=>e.toFixed(4))} )`;switch(gm.getTransfer(e)){case _p:return[t,`LinearTransferOETF`];case vp:return[t,`sRGBTransferOETF`];default:return Ap(`WebGLProgram: Unsupported color space: `,e),[t,`LinearTransferOETF`]}}function yS(e,t,n){let r=e.getShaderParameter(t,e.COMPILE_STATUS),i=(e.getShaderInfoLog(t)||``).trim();if(r&&i===``)return``;let a=/ERROR: 0:(\d+)/.exec(i);if(a){let r=parseInt(a[1]);return n.toUpperCase()+` `+i+` -`+Vv(e.getShaderSource(t),r)}else return i}function Gv(e,t){let n=Uv(t);return[`vec4 ${e}( vec4 value ) {`,` return ${n[1]}( vec4( value.rgb * ${n[0]}, value.a ) );`,`}`].join(` -`)}var Kv={1:`Linear`,2:`Reinhard`,3:`Cineon`,4:`ACESFilmic`,6:`AgX`,7:`Neutral`,5:`Custom`};function qv(e,t){let n=Kv[t];return n===void 0?(nu(`WebGLProgram: Unsupported toneMapping:`,t),`vec3 `+e+`( vec3 color ) { return LinearToneMapping( color ); }`):`vec3 `+e+`( vec3 color ) { return `+n+`ToneMapping( color ); }`}var Jv=new Z;function Yv(){return Uu.getLuminanceCoefficients(Jv),[`float luminance( const in vec3 rgb ) {`,` const vec3 weights = vec3( ${Jv.x.toFixed(4)}, ${Jv.y.toFixed(4)}, ${Jv.z.toFixed(4)} );`,` return dot( weights, rgb );`,`}`].join(` -`)}function Xv(e){return[e.extensionClipCullDistance?`#extension GL_ANGLE_clip_cull_distance : require`:``,e.extensionMultiDraw?`#extension GL_ANGLE_multi_draw : require`:``].filter($v).join(` -`)}function Zv(e){let t=[];for(let n in e){let r=e[n];r!==!1&&t.push(`#define `+n+` `+r)}return t.join(` -`)}function Qv(e,t){let n={},r=e.getProgramParameter(t,e.ACTIVE_ATTRIBUTES);for(let i=0;i/gm;function ry(e){return e.replace(ny,ay)}var iy=new Map;function ay(e,t){let n=Wg[t];if(n===void 0){let e=iy.get(t);if(e!==void 0)n=Wg[e],nu(`WebGLRenderer: Shader chunk "%s" has been deprecated. Use "%s" instead.`,t,e);else throw Error(`THREE.WebGLProgram: Can not resolve #include <`+t+`>`)}return ry(n)}var oy=/#pragma unroll_loop_start\s+for\s*\(\s*int\s+i\s*=\s*(\d+)\s*;\s*i\s*<\s*(\d+)\s*;\s*i\s*\+\+\s*\)\s*{([\s\S]+?)}\s+#pragma unroll_loop_end/g;function sy(e){return e.replace(oy,cy)}function cy(e,t,n,r){let i=``;for(let e=parseInt(t);e/gm;function MS(e){return e.replace(jS,PS)}var NS=new Map;function PS(e,t){let n=yb[t];if(n===void 0){let e=NS.get(t);if(e!==void 0)n=yb[e],Ap(`WebGLRenderer: Shader chunk "%s" has been deprecated. Use "%s" instead.`,t,e);else throw Error(`THREE.WebGLProgram: Can not resolve #include <`+t+`>`)}return MS(n)}var FS=/#pragma unroll_loop_start\s+for\s*\(\s*int\s+i\s*=\s*(\d+)\s*;\s*i\s*<\s*(\d+)\s*;\s*i\s*\+\+\s*\)\s*{([\s\S]+?)}\s+#pragma unroll_loop_end/g;function IS(e){return e.replace(FS,LS)}function LS(e,t,n,r){let i=``;for(let e=parseInt(t);e0&&(g+=` -`),_=[`#define SHADER_TYPE `+n.shaderType,`#define SHADER_NAME `+n.shaderName,m].filter($v).join(` +`),_=[`#define SHADER_TYPE `+n.shaderType,`#define SHADER_NAME `+n.shaderName,m].filter(OS).join(` `),_.length>0&&(_+=` -`)):(g=[ly(n),`#define SHADER_TYPE `+n.shaderType,`#define SHADER_NAME `+n.shaderName,m,n.extensionClipCullDistance?`#define USE_CLIP_DISTANCE`:``,n.batching?`#define USE_BATCHING`:``,n.batchingColor?`#define USE_BATCHING_COLOR`:``,n.instancing?`#define USE_INSTANCING`:``,n.instancingColor?`#define USE_INSTANCING_COLOR`:``,n.instancingMorph?`#define USE_INSTANCING_MORPH`:``,n.useFog&&n.fog?`#define USE_FOG`:``,n.useFog&&n.fogExp2?`#define FOG_EXP2`:``,n.map?`#define USE_MAP`:``,n.envMap?`#define USE_ENVMAP`:``,n.envMap?`#define `+u:``,n.lightMap?`#define USE_LIGHTMAP`:``,n.aoMap?`#define USE_AOMAP`:``,n.bumpMap?`#define USE_BUMPMAP`:``,n.normalMap?`#define USE_NORMALMAP`:``,n.normalMapObjectSpace?`#define USE_NORMALMAP_OBJECTSPACE`:``,n.normalMapTangentSpace?`#define USE_NORMALMAP_TANGENTSPACE`:``,n.displacementMap?`#define USE_DISPLACEMENTMAP`:``,n.emissiveMap?`#define USE_EMISSIVEMAP`:``,n.anisotropy?`#define USE_ANISOTROPY`:``,n.anisotropyMap?`#define USE_ANISOTROPYMAP`:``,n.clearcoatMap?`#define USE_CLEARCOATMAP`:``,n.clearcoatRoughnessMap?`#define USE_CLEARCOAT_ROUGHNESSMAP`:``,n.clearcoatNormalMap?`#define USE_CLEARCOAT_NORMALMAP`:``,n.iridescenceMap?`#define USE_IRIDESCENCEMAP`:``,n.iridescenceThicknessMap?`#define USE_IRIDESCENCE_THICKNESSMAP`:``,n.specularMap?`#define USE_SPECULARMAP`:``,n.specularColorMap?`#define USE_SPECULAR_COLORMAP`:``,n.specularIntensityMap?`#define USE_SPECULAR_INTENSITYMAP`:``,n.roughnessMap?`#define USE_ROUGHNESSMAP`:``,n.metalnessMap?`#define USE_METALNESSMAP`:``,n.alphaMap?`#define USE_ALPHAMAP`:``,n.alphaHash?`#define USE_ALPHAHASH`:``,n.transmission?`#define USE_TRANSMISSION`:``,n.transmissionMap?`#define USE_TRANSMISSIONMAP`:``,n.thicknessMap?`#define USE_THICKNESSMAP`:``,n.sheenColorMap?`#define USE_SHEEN_COLORMAP`:``,n.sheenRoughnessMap?`#define USE_SHEEN_ROUGHNESSMAP`:``,n.mapUv?`#define MAP_UV `+n.mapUv:``,n.alphaMapUv?`#define ALPHAMAP_UV `+n.alphaMapUv:``,n.lightMapUv?`#define LIGHTMAP_UV `+n.lightMapUv:``,n.aoMapUv?`#define AOMAP_UV `+n.aoMapUv:``,n.emissiveMapUv?`#define EMISSIVEMAP_UV `+n.emissiveMapUv:``,n.bumpMapUv?`#define BUMPMAP_UV `+n.bumpMapUv:``,n.normalMapUv?`#define NORMALMAP_UV `+n.normalMapUv:``,n.displacementMapUv?`#define DISPLACEMENTMAP_UV `+n.displacementMapUv:``,n.metalnessMapUv?`#define METALNESSMAP_UV `+n.metalnessMapUv:``,n.roughnessMapUv?`#define ROUGHNESSMAP_UV `+n.roughnessMapUv:``,n.anisotropyMapUv?`#define ANISOTROPYMAP_UV `+n.anisotropyMapUv:``,n.clearcoatMapUv?`#define CLEARCOATMAP_UV `+n.clearcoatMapUv:``,n.clearcoatNormalMapUv?`#define CLEARCOAT_NORMALMAP_UV `+n.clearcoatNormalMapUv:``,n.clearcoatRoughnessMapUv?`#define CLEARCOAT_ROUGHNESSMAP_UV `+n.clearcoatRoughnessMapUv:``,n.iridescenceMapUv?`#define IRIDESCENCEMAP_UV `+n.iridescenceMapUv:``,n.iridescenceThicknessMapUv?`#define IRIDESCENCE_THICKNESSMAP_UV `+n.iridescenceThicknessMapUv:``,n.sheenColorMapUv?`#define SHEEN_COLORMAP_UV `+n.sheenColorMapUv:``,n.sheenRoughnessMapUv?`#define SHEEN_ROUGHNESSMAP_UV `+n.sheenRoughnessMapUv:``,n.specularMapUv?`#define SPECULARMAP_UV `+n.specularMapUv:``,n.specularColorMapUv?`#define SPECULAR_COLORMAP_UV `+n.specularColorMapUv:``,n.specularIntensityMapUv?`#define SPECULAR_INTENSITYMAP_UV `+n.specularIntensityMapUv:``,n.transmissionMapUv?`#define TRANSMISSIONMAP_UV `+n.transmissionMapUv:``,n.thicknessMapUv?`#define THICKNESSMAP_UV `+n.thicknessMapUv:``,n.vertexTangents&&n.flatShading===!1?`#define USE_TANGENT`:``,n.vertexNormals?`#define HAS_NORMAL`:``,n.vertexColors?`#define USE_COLOR`:``,n.vertexAlphas?`#define USE_COLOR_ALPHA`:``,n.vertexUv1s?`#define USE_UV1`:``,n.vertexUv2s?`#define USE_UV2`:``,n.vertexUv3s?`#define USE_UV3`:``,n.pointsUvs?`#define USE_POINTS_UV`:``,n.flatShading?`#define FLAT_SHADED`:``,n.skinning?`#define USE_SKINNING`:``,n.morphTargets?`#define USE_MORPHTARGETS`:``,n.morphNormals&&n.flatShading===!1?`#define USE_MORPHNORMALS`:``,n.morphColors?`#define USE_MORPHCOLORS`:``,n.morphTargetsCount>0?`#define MORPHTARGETS_TEXTURE_STRIDE `+n.morphTextureStride:``,n.morphTargetsCount>0?`#define MORPHTARGETS_COUNT `+n.morphTargetsCount:``,n.doubleSided?`#define DOUBLE_SIDED`:``,n.flipSided?`#define FLIP_SIDED`:``,n.shadowMapEnabled?`#define USE_SHADOWMAP`:``,n.shadowMapEnabled?`#define `+c:``,n.sizeAttenuation?`#define USE_SIZEATTENUATION`:``,n.numLightProbes>0?`#define USE_LIGHT_PROBES`:``,n.logarithmicDepthBuffer?`#define USE_LOGARITHMIC_DEPTH_BUFFER`:``,n.reversedDepthBuffer?`#define USE_REVERSED_DEPTH_BUFFER`:``,`uniform mat4 modelMatrix;`,`uniform mat4 modelViewMatrix;`,`uniform mat4 projectionMatrix;`,`uniform mat4 viewMatrix;`,`uniform mat3 normalMatrix;`,`uniform vec3 cameraPosition;`,`uniform bool isOrthographic;`,`#ifdef USE_INSTANCING`,` attribute mat4 instanceMatrix;`,`#endif`,`#ifdef USE_INSTANCING_COLOR`,` attribute vec3 instanceColor;`,`#endif`,`#ifdef USE_INSTANCING_MORPH`,` uniform sampler2D morphTexture;`,`#endif`,`attribute vec3 position;`,`attribute vec3 normal;`,`attribute vec2 uv;`,`#ifdef USE_UV1`,` attribute vec2 uv1;`,`#endif`,`#ifdef USE_UV2`,` attribute vec2 uv2;`,`#endif`,`#ifdef USE_UV3`,` attribute vec2 uv3;`,`#endif`,`#ifdef USE_TANGENT`,` attribute vec4 tangent;`,`#endif`,`#if defined( USE_COLOR_ALPHA )`,` attribute vec4 color;`,`#elif defined( USE_COLOR )`,` attribute vec3 color;`,`#endif`,`#ifdef USE_SKINNING`,` attribute vec4 skinIndex;`,` attribute vec4 skinWeight;`,`#endif`,` -`].filter($v).join(` -`),_=[ly(n),`#define SHADER_TYPE `+n.shaderType,`#define SHADER_NAME `+n.shaderName,m,n.useFog&&n.fog?`#define USE_FOG`:``,n.useFog&&n.fogExp2?`#define FOG_EXP2`:``,n.alphaToCoverage?`#define ALPHA_TO_COVERAGE`:``,n.map?`#define USE_MAP`:``,n.matcap?`#define USE_MATCAP`:``,n.envMap?`#define USE_ENVMAP`:``,n.envMap?`#define `+l:``,n.envMap?`#define `+u:``,n.envMap?`#define `+d:``,f?`#define CUBEUV_TEXEL_WIDTH `+f.texelWidth:``,f?`#define CUBEUV_TEXEL_HEIGHT `+f.texelHeight:``,f?`#define CUBEUV_MAX_MIP `+f.maxMip+`.0`:``,n.lightMap?`#define USE_LIGHTMAP`:``,n.aoMap?`#define USE_AOMAP`:``,n.bumpMap?`#define USE_BUMPMAP`:``,n.normalMap?`#define USE_NORMALMAP`:``,n.normalMapObjectSpace?`#define USE_NORMALMAP_OBJECTSPACE`:``,n.normalMapTangentSpace?`#define USE_NORMALMAP_TANGENTSPACE`:``,n.packedNormalMap?`#define USE_PACKED_NORMALMAP`:``,n.emissiveMap?`#define USE_EMISSIVEMAP`:``,n.anisotropy?`#define USE_ANISOTROPY`:``,n.anisotropyMap?`#define USE_ANISOTROPYMAP`:``,n.clearcoat?`#define USE_CLEARCOAT`:``,n.clearcoatMap?`#define USE_CLEARCOATMAP`:``,n.clearcoatRoughnessMap?`#define USE_CLEARCOAT_ROUGHNESSMAP`:``,n.clearcoatNormalMap?`#define USE_CLEARCOAT_NORMALMAP`:``,n.dispersion?`#define USE_DISPERSION`:``,n.retroreflection?`#define USE_RETROREFLECTION`:``,n.iridescence?`#define USE_IRIDESCENCE`:``,n.iridescenceMap?`#define USE_IRIDESCENCEMAP`:``,n.iridescenceThicknessMap?`#define USE_IRIDESCENCE_THICKNESSMAP`:``,n.specularMap?`#define USE_SPECULARMAP`:``,n.specularColorMap?`#define USE_SPECULAR_COLORMAP`:``,n.specularIntensityMap?`#define USE_SPECULAR_INTENSITYMAP`:``,n.roughnessMap?`#define USE_ROUGHNESSMAP`:``,n.metalnessMap?`#define USE_METALNESSMAP`:``,n.alphaMap?`#define USE_ALPHAMAP`:``,n.alphaTest?`#define USE_ALPHATEST`:``,n.alphaHash?`#define USE_ALPHAHASH`:``,n.sheen?`#define USE_SHEEN`:``,n.sheenColorMap?`#define USE_SHEEN_COLORMAP`:``,n.sheenRoughnessMap?`#define USE_SHEEN_ROUGHNESSMAP`:``,n.transmission?`#define USE_TRANSMISSION`:``,n.transmissionMap?`#define USE_TRANSMISSIONMAP`:``,n.thicknessMap?`#define USE_THICKNESSMAP`:``,n.vertexTangents&&n.flatShading===!1?`#define USE_TANGENT`:``,n.vertexColors||n.instancingColor?`#define USE_COLOR`:``,n.vertexAlphas||n.batchingColor?`#define USE_COLOR_ALPHA`:``,n.vertexUv1s?`#define USE_UV1`:``,n.vertexUv2s?`#define USE_UV2`:``,n.vertexUv3s?`#define USE_UV3`:``,n.pointsUvs?`#define USE_POINTS_UV`:``,n.gradientMap?`#define USE_GRADIENTMAP`:``,n.flatShading?`#define FLAT_SHADED`:``,n.doubleSided?`#define DOUBLE_SIDED`:``,n.flipSided?`#define FLIP_SIDED`:``,n.shadowMapEnabled?`#define USE_SHADOWMAP`:``,n.shadowMapEnabled?`#define `+c:``,n.premultipliedAlpha?`#define PREMULTIPLIED_ALPHA`:``,n.numLightProbes>0?`#define USE_LIGHT_PROBES`:``,n.numLightProbeGrids>0?`#define USE_LIGHT_PROBES_GRID`:``,n.decodeVideoTexture?`#define DECODE_VIDEO_TEXTURE`:``,n.decodeVideoTextureEmissive?`#define DECODE_VIDEO_TEXTURE_EMISSIVE`:``,n.logarithmicDepthBuffer?`#define USE_LOGARITHMIC_DEPTH_BUFFER`:``,n.reversedDepthBuffer?`#define USE_REVERSED_DEPTH_BUFFER`:``,`uniform mat4 viewMatrix;`,`uniform vec3 cameraPosition;`,`uniform bool isOrthographic;`,n.toneMapping===0?``:`#define TONE_MAPPING`,n.toneMapping===0?``:Wg.tonemapping_pars_fragment,n.toneMapping===0?``:qv(`toneMapping`,n.toneMapping),n.dithering?`#define DITHERING`:``,n.opaque?`#define OPAQUE`:``,Wg.colorspace_pars_fragment,Gv(`linearToOutputTexel`,n.outputColorSpace),Yv(),n.useDepthPacking?`#define DEPTH_PACKING `+n.depthPacking:``,` -`].filter($v).join(` -`)),o=ry(o),o=ey(o,n),o=ty(o,n),s=ry(s),s=ey(s,n),s=ty(s,n),o=sy(o),s=sy(s),n.isRawShaderMaterial!==!0&&(v=`#version 300 es +`)):(g=[RS(n),`#define SHADER_TYPE `+n.shaderType,`#define SHADER_NAME `+n.shaderName,m,n.extensionClipCullDistance?`#define USE_CLIP_DISTANCE`:``,n.batching?`#define USE_BATCHING`:``,n.batchingColor?`#define USE_BATCHING_COLOR`:``,n.instancing?`#define USE_INSTANCING`:``,n.instancingColor?`#define USE_INSTANCING_COLOR`:``,n.instancingMorph?`#define USE_INSTANCING_MORPH`:``,n.useFog&&n.fog?`#define USE_FOG`:``,n.useFog&&n.fogExp2?`#define FOG_EXP2`:``,n.map?`#define USE_MAP`:``,n.envMap?`#define USE_ENVMAP`:``,n.envMap?`#define `+u:``,n.lightMap?`#define USE_LIGHTMAP`:``,n.aoMap?`#define USE_AOMAP`:``,n.bumpMap?`#define USE_BUMPMAP`:``,n.normalMap?`#define USE_NORMALMAP`:``,n.normalMapObjectSpace?`#define USE_NORMALMAP_OBJECTSPACE`:``,n.normalMapTangentSpace?`#define USE_NORMALMAP_TANGENTSPACE`:``,n.displacementMap?`#define USE_DISPLACEMENTMAP`:``,n.emissiveMap?`#define USE_EMISSIVEMAP`:``,n.anisotropy?`#define USE_ANISOTROPY`:``,n.anisotropyMap?`#define USE_ANISOTROPYMAP`:``,n.clearcoatMap?`#define USE_CLEARCOATMAP`:``,n.clearcoatRoughnessMap?`#define USE_CLEARCOAT_ROUGHNESSMAP`:``,n.clearcoatNormalMap?`#define USE_CLEARCOAT_NORMALMAP`:``,n.iridescenceMap?`#define USE_IRIDESCENCEMAP`:``,n.iridescenceThicknessMap?`#define USE_IRIDESCENCE_THICKNESSMAP`:``,n.specularMap?`#define USE_SPECULARMAP`:``,n.specularColorMap?`#define USE_SPECULAR_COLORMAP`:``,n.specularIntensityMap?`#define USE_SPECULAR_INTENSITYMAP`:``,n.roughnessMap?`#define USE_ROUGHNESSMAP`:``,n.metalnessMap?`#define USE_METALNESSMAP`:``,n.alphaMap?`#define USE_ALPHAMAP`:``,n.alphaHash?`#define USE_ALPHAHASH`:``,n.transmission?`#define USE_TRANSMISSION`:``,n.transmissionMap?`#define USE_TRANSMISSIONMAP`:``,n.thicknessMap?`#define USE_THICKNESSMAP`:``,n.sheenColorMap?`#define USE_SHEEN_COLORMAP`:``,n.sheenRoughnessMap?`#define USE_SHEEN_ROUGHNESSMAP`:``,n.mapUv?`#define MAP_UV `+n.mapUv:``,n.alphaMapUv?`#define ALPHAMAP_UV `+n.alphaMapUv:``,n.lightMapUv?`#define LIGHTMAP_UV `+n.lightMapUv:``,n.aoMapUv?`#define AOMAP_UV `+n.aoMapUv:``,n.emissiveMapUv?`#define EMISSIVEMAP_UV `+n.emissiveMapUv:``,n.bumpMapUv?`#define BUMPMAP_UV `+n.bumpMapUv:``,n.normalMapUv?`#define NORMALMAP_UV `+n.normalMapUv:``,n.displacementMapUv?`#define DISPLACEMENTMAP_UV `+n.displacementMapUv:``,n.metalnessMapUv?`#define METALNESSMAP_UV `+n.metalnessMapUv:``,n.roughnessMapUv?`#define ROUGHNESSMAP_UV `+n.roughnessMapUv:``,n.anisotropyMapUv?`#define ANISOTROPYMAP_UV `+n.anisotropyMapUv:``,n.clearcoatMapUv?`#define CLEARCOATMAP_UV `+n.clearcoatMapUv:``,n.clearcoatNormalMapUv?`#define CLEARCOAT_NORMALMAP_UV `+n.clearcoatNormalMapUv:``,n.clearcoatRoughnessMapUv?`#define CLEARCOAT_ROUGHNESSMAP_UV `+n.clearcoatRoughnessMapUv:``,n.iridescenceMapUv?`#define IRIDESCENCEMAP_UV `+n.iridescenceMapUv:``,n.iridescenceThicknessMapUv?`#define IRIDESCENCE_THICKNESSMAP_UV `+n.iridescenceThicknessMapUv:``,n.sheenColorMapUv?`#define SHEEN_COLORMAP_UV `+n.sheenColorMapUv:``,n.sheenRoughnessMapUv?`#define SHEEN_ROUGHNESSMAP_UV `+n.sheenRoughnessMapUv:``,n.specularMapUv?`#define SPECULARMAP_UV `+n.specularMapUv:``,n.specularColorMapUv?`#define SPECULAR_COLORMAP_UV `+n.specularColorMapUv:``,n.specularIntensityMapUv?`#define SPECULAR_INTENSITYMAP_UV `+n.specularIntensityMapUv:``,n.transmissionMapUv?`#define TRANSMISSIONMAP_UV `+n.transmissionMapUv:``,n.thicknessMapUv?`#define THICKNESSMAP_UV `+n.thicknessMapUv:``,n.vertexTangents&&n.flatShading===!1?`#define USE_TANGENT`:``,n.vertexNormals?`#define HAS_NORMAL`:``,n.vertexColors?`#define USE_COLOR`:``,n.vertexAlphas?`#define USE_COLOR_ALPHA`:``,n.vertexUv1s?`#define USE_UV1`:``,n.vertexUv2s?`#define USE_UV2`:``,n.vertexUv3s?`#define USE_UV3`:``,n.pointsUvs?`#define USE_POINTS_UV`:``,n.flatShading?`#define FLAT_SHADED`:``,n.skinning?`#define USE_SKINNING`:``,n.morphTargets?`#define USE_MORPHTARGETS`:``,n.morphNormals&&n.flatShading===!1?`#define USE_MORPHNORMALS`:``,n.morphColors?`#define USE_MORPHCOLORS`:``,n.morphTargetsCount>0?`#define MORPHTARGETS_TEXTURE_STRIDE `+n.morphTextureStride:``,n.morphTargetsCount>0?`#define MORPHTARGETS_COUNT `+n.morphTargetsCount:``,n.doubleSided?`#define DOUBLE_SIDED`:``,n.flipSided?`#define FLIP_SIDED`:``,n.shadowMapEnabled?`#define USE_SHADOWMAP`:``,n.shadowMapEnabled?`#define `+c:``,n.sizeAttenuation?`#define USE_SIZEATTENUATION`:``,n.numLightProbes>0?`#define USE_LIGHT_PROBES`:``,n.logarithmicDepthBuffer?`#define USE_LOGARITHMIC_DEPTH_BUFFER`:``,n.reversedDepthBuffer?`#define USE_REVERSED_DEPTH_BUFFER`:``,`uniform mat4 modelMatrix;`,`uniform mat4 modelViewMatrix;`,`uniform mat4 projectionMatrix;`,`uniform mat4 viewMatrix;`,`uniform mat3 normalMatrix;`,`uniform vec3 cameraPosition;`,`uniform bool isOrthographic;`,`#ifdef USE_INSTANCING`,` attribute mat4 instanceMatrix;`,`#endif`,`#ifdef USE_INSTANCING_COLOR`,` attribute vec3 instanceColor;`,`#endif`,`#ifdef USE_INSTANCING_MORPH`,` uniform sampler2D morphTexture;`,`#endif`,`attribute vec3 position;`,`attribute vec3 normal;`,`attribute vec2 uv;`,`#ifdef USE_UV1`,` attribute vec2 uv1;`,`#endif`,`#ifdef USE_UV2`,` attribute vec2 uv2;`,`#endif`,`#ifdef USE_UV3`,` attribute vec2 uv3;`,`#endif`,`#ifdef USE_TANGENT`,` attribute vec4 tangent;`,`#endif`,`#if defined( USE_COLOR_ALPHA )`,` attribute vec4 color;`,`#elif defined( USE_COLOR )`,` attribute vec3 color;`,`#endif`,`#ifdef USE_SKINNING`,` attribute vec4 skinIndex;`,` attribute vec4 skinWeight;`,`#endif`,` +`].filter(OS).join(` +`),_=[RS(n),`#define SHADER_TYPE `+n.shaderType,`#define SHADER_NAME `+n.shaderName,m,n.useFog&&n.fog?`#define USE_FOG`:``,n.useFog&&n.fogExp2?`#define FOG_EXP2`:``,n.alphaToCoverage?`#define ALPHA_TO_COVERAGE`:``,n.map?`#define USE_MAP`:``,n.matcap?`#define USE_MATCAP`:``,n.envMap?`#define USE_ENVMAP`:``,n.envMap?`#define `+l:``,n.envMap?`#define `+u:``,n.envMap?`#define `+d:``,f?`#define CUBEUV_TEXEL_WIDTH `+f.texelWidth:``,f?`#define CUBEUV_TEXEL_HEIGHT `+f.texelHeight:``,f?`#define CUBEUV_MAX_MIP `+f.maxMip+`.0`:``,n.lightMap?`#define USE_LIGHTMAP`:``,n.aoMap?`#define USE_AOMAP`:``,n.bumpMap?`#define USE_BUMPMAP`:``,n.normalMap?`#define USE_NORMALMAP`:``,n.normalMapObjectSpace?`#define USE_NORMALMAP_OBJECTSPACE`:``,n.normalMapTangentSpace?`#define USE_NORMALMAP_TANGENTSPACE`:``,n.packedNormalMap?`#define USE_PACKED_NORMALMAP`:``,n.emissiveMap?`#define USE_EMISSIVEMAP`:``,n.anisotropy?`#define USE_ANISOTROPY`:``,n.anisotropyMap?`#define USE_ANISOTROPYMAP`:``,n.clearcoat?`#define USE_CLEARCOAT`:``,n.clearcoatMap?`#define USE_CLEARCOATMAP`:``,n.clearcoatRoughnessMap?`#define USE_CLEARCOAT_ROUGHNESSMAP`:``,n.clearcoatNormalMap?`#define USE_CLEARCOAT_NORMALMAP`:``,n.dispersion?`#define USE_DISPERSION`:``,n.retroreflection?`#define USE_RETROREFLECTION`:``,n.iridescence?`#define USE_IRIDESCENCE`:``,n.iridescenceMap?`#define USE_IRIDESCENCEMAP`:``,n.iridescenceThicknessMap?`#define USE_IRIDESCENCE_THICKNESSMAP`:``,n.specularMap?`#define USE_SPECULARMAP`:``,n.specularColorMap?`#define USE_SPECULAR_COLORMAP`:``,n.specularIntensityMap?`#define USE_SPECULAR_INTENSITYMAP`:``,n.roughnessMap?`#define USE_ROUGHNESSMAP`:``,n.metalnessMap?`#define USE_METALNESSMAP`:``,n.alphaMap?`#define USE_ALPHAMAP`:``,n.alphaTest?`#define USE_ALPHATEST`:``,n.alphaHash?`#define USE_ALPHAHASH`:``,n.sheen?`#define USE_SHEEN`:``,n.sheenColorMap?`#define USE_SHEEN_COLORMAP`:``,n.sheenRoughnessMap?`#define USE_SHEEN_ROUGHNESSMAP`:``,n.transmission?`#define USE_TRANSMISSION`:``,n.transmissionMap?`#define USE_TRANSMISSIONMAP`:``,n.thicknessMap?`#define USE_THICKNESSMAP`:``,n.vertexTangents&&n.flatShading===!1?`#define USE_TANGENT`:``,n.vertexColors||n.instancingColor?`#define USE_COLOR`:``,n.vertexAlphas||n.batchingColor?`#define USE_COLOR_ALPHA`:``,n.vertexUv1s?`#define USE_UV1`:``,n.vertexUv2s?`#define USE_UV2`:``,n.vertexUv3s?`#define USE_UV3`:``,n.pointsUvs?`#define USE_POINTS_UV`:``,n.gradientMap?`#define USE_GRADIENTMAP`:``,n.flatShading?`#define FLAT_SHADED`:``,n.doubleSided?`#define DOUBLE_SIDED`:``,n.flipSided?`#define FLIP_SIDED`:``,n.shadowMapEnabled?`#define USE_SHADOWMAP`:``,n.shadowMapEnabled?`#define `+c:``,n.premultipliedAlpha?`#define PREMULTIPLIED_ALPHA`:``,n.numLightProbes>0?`#define USE_LIGHT_PROBES`:``,n.numLightProbeGrids>0?`#define USE_LIGHT_PROBES_GRID`:``,n.decodeVideoTexture?`#define DECODE_VIDEO_TEXTURE`:``,n.decodeVideoTextureEmissive?`#define DECODE_VIDEO_TEXTURE_EMISSIVE`:``,n.logarithmicDepthBuffer?`#define USE_LOGARITHMIC_DEPTH_BUFFER`:``,n.reversedDepthBuffer?`#define USE_REVERSED_DEPTH_BUFFER`:``,`uniform mat4 viewMatrix;`,`uniform vec3 cameraPosition;`,`uniform bool isOrthographic;`,n.toneMapping===0?``:`#define TONE_MAPPING`,n.toneMapping===0?``:yb.tonemapping_pars_fragment,n.toneMapping===0?``:SS(`toneMapping`,n.toneMapping),n.dithering?`#define DITHERING`:``,n.opaque?`#define OPAQUE`:``,yb.colorspace_pars_fragment,bS(`linearToOutputTexel`,n.outputColorSpace),wS(),n.useDepthPacking?`#define DEPTH_PACKING `+n.depthPacking:``,` +`].filter(OS).join(` +`)),o=MS(o),o=kS(o,n),o=AS(o,n),s=MS(s),s=kS(s,n),s=AS(s,n),o=IS(o),s=IS(s),n.isRawShaderMaterial!==!0&&(v=`#version 300 es `,g=[p,`#define attribute in`,`#define varying out`,`#define texture2D texture`].join(` `)+` `+g,_=[`#define varying in`,n.glslVersion===`300 es`?``:`layout(location = 0) out highp vec4 pc_fragColor;`,n.glslVersion===`300 es`?``:`#define gl_FragColor pc_fragColor`,`#define gl_FragDepthEXT gl_FragDepth`,`#define texture2D texture`,`#define textureCube texture`,`#define texture2DProj textureProj`,`#define texture2DLodEXT textureLod`,`#define texture2DProjLodEXT textureProjLod`,`#define textureCubeLodEXT textureLod`,`#define texture2DGradEXT textureGrad`,`#define texture2DProjGradEXT textureProjGrad`,`#define textureCubeGradEXT textureGrad`].join(` `)+` -`+_);let y=v+g+o,b=v+_+s,x=Rv(i,i.VERTEX_SHADER,y),S=Rv(i,i.FRAGMENT_SHADER,b);i.attachShader(h,x),i.attachShader(h,S),n.index0AttributeName===void 0?n.hasPositionAttribute===!0&&i.bindAttribLocation(h,0,`position`):i.bindAttribLocation(h,0,n.index0AttributeName),i.linkProgram(h);function C(t){if(e.debug.checkShaderErrors){let n=i.getProgramInfoLog(h)||``,r=i.getShaderInfoLog(x)||``,a=i.getShaderInfoLog(S)||``,o=n.trim(),s=r.trim(),c=a.trim(),l=!0,u=!0;if(i.getProgramParameter(h,i.LINK_STATUS)===!1)if(l=!1,typeof e.debug.onShaderError==`function`)e.debug.onShaderError(i,h,x,S);else{let e=Wv(i,x,`vertex`),n=Wv(i,S,`fragment`);ru(`WebGLProgram: Shader Error `+i.getError()+` - VALIDATE_STATUS `+i.getProgramParameter(h,i.VALIDATE_STATUS)+` +`+_);let y=v+g+o,b=v+_+s,x=pS(i,i.VERTEX_SHADER,y),S=pS(i,i.FRAGMENT_SHADER,b);i.attachShader(h,x),i.attachShader(h,S),n.index0AttributeName===void 0?n.hasPositionAttribute===!0&&i.bindAttribLocation(h,0,`position`):i.bindAttribLocation(h,0,n.index0AttributeName),i.linkProgram(h);function C(t){if(e.debug.checkShaderErrors){let n=i.getProgramInfoLog(h)||``,r=i.getShaderInfoLog(x)||``,a=i.getShaderInfoLog(S)||``,o=n.trim(),s=r.trim(),c=a.trim(),l=!0,u=!0;if(i.getProgramParameter(h,i.LINK_STATUS)===!1)if(l=!1,typeof e.debug.onShaderError==`function`)e.debug.onShaderError(i,h,x,S);else{let e=yS(i,x,`vertex`),n=yS(i,S,`fragment`);jp(`WebGLProgram: Shader Error `+i.getError()+` - VALIDATE_STATUS `+i.getProgramParameter(h,i.VALIDATE_STATUS)+` Material Name: `+t.name+` Material Type: `+t.type+` Program Info Log: `+o+` `+e+` -`+n)}else o===``?(s===``||c===``)&&(u=!1):nu(`WebGLProgram: Program Info Log:`,o);u&&(t.diagnostics={runnable:l,programLog:o,vertexShader:{log:s,prefix:g},fragmentShader:{log:c,prefix:_}})}i.deleteShader(x),i.deleteShader(S),w=new Lv(i,h),T=Qv(i,h)}let w;this.getUniforms=function(){return w===void 0&&C(this),w};let T;this.getAttributes=function(){return T===void 0&&C(this),T};let E=n.rendererExtensionParallelShaderCompile===!1;return this.isReady=function(){return E===!1&&(E=i.getProgramParameter(h,zv)),E},this.destroy=function(){r.releaseStatesOfProgram(this),i.deleteProgram(h),this.program=void 0},this.type=n.shaderType,this.name=n.shaderName,this.id=Bv++,this.cacheKey=t,this.usedTimes=1,this.program=h,this.vertexShader=x,this.fragmentShader=S,this}var by=0,xy=class{constructor(){this.shaderCache=new Map,this.materialCache=new Map}update(e,t,n){let r=this._getShaderCacheForMaterial(e);return r.has(t)===!1&&(r.add(t),t.usedTimes++),r.has(n)===!1&&(r.add(n),n.usedTimes++),this}remove(e){let t=this.materialCache.get(e);for(let e of t)e.usedTimes--,e.usedTimes===0&&this.shaderCache.delete(e.code);return this.materialCache.delete(e),this}getVertexShaderStage(e){return this._getShaderStage(e.vertexShader)}getFragmentShaderStage(e){return this._getShaderStage(e.fragmentShader)}dispose(){this.shaderCache.clear(),this.materialCache.clear()}_getShaderCacheForMaterial(e){let t=this.materialCache,n=t.get(e);return n===void 0&&(n=new Set,t.set(e,n)),n}_getShaderStage(e){let t=this.shaderCache,n=t.get(e);return n===void 0&&(n=new Sy(e),t.set(e,n)),n}},Sy=class{constructor(e){this.id=by++,this.code=e,this.usedTimes=0}};function Cy(e){return e===1030||e===37490||e===36285}function wy(e,t,n,r,i,a){let o=new gd,s=new xy,c=new Set,l=[],u=new Map,d=r.logarithmicDepthBuffer,f=r.precision,p={MeshDepthMaterial:`depth`,MeshDistanceMaterial:`distance`,MeshNormalMaterial:`normal`,MeshBasicMaterial:`basic`,MeshLambertMaterial:`lambert`,MeshPhongMaterial:`phong`,MeshToonMaterial:`toon`,MeshStandardMaterial:`physical`,MeshPhysicalMaterial:`physical`,MeshMatcapMaterial:`matcap`,LineBasicMaterial:`basic`,LineDashedMaterial:`dashed`,PointsMaterial:`points`,ShadowMaterial:`shadow`,SpriteMaterial:`sprite`};function m(e){return c.add(e),e===0?`uv`:`uv${e}`}function h(i,o,l,u,h,g){let _=u.fog,v=h.geometry,y=i.isMeshStandardMaterial||i.isMeshLambertMaterial||i.isMeshPhongMaterial?u.environment:null,b=i.isMeshStandardMaterial||i.isMeshLambertMaterial&&!i.envMap||i.isMeshPhongMaterial&&!i.envMap,x=t.get(i.envMap||y,b),S=x&&x.mapping===306?x.image.height:null,C=p[i.type];i.precision!==null&&(f=r.getMaxPrecision(i.precision),f!==i.precision&&nu(`WebGLProgram.getParameters:`,i.precision,`not supported, using`,f,`instead.`));let w=v.morphAttributes.position||v.morphAttributes.normal||v.morphAttributes.color,T=w===void 0?0:w.length,E=0;v.morphAttributes.position!==void 0&&(E=1),v.morphAttributes.normal!==void 0&&(E=2),v.morphAttributes.color!==void 0&&(E=3);let D,O,ee,k;if(C){let e=Gg[C];D=e.vertexShader,O=e.fragmentShader}else{D=i.vertexShader,O=i.fragmentShader;let e=s.getVertexShaderStage(i),t=s.getFragmentShaderStage(i);s.update(i,e,t),ee=e.id,k=t.id}let A=e.getRenderTarget(),te=e.state.buffers.depth.getReversed(),j=h.isInstancedMesh===!0,M=h.isBatchedMesh===!0,ne=!!i.map,N=!!i.matcap,P=!!x,F=!!i.aoMap,re=!!i.lightMap,ie=!!i.bumpMap&&i.wireframe===!1,ae=!!i.normalMap,I=!!i.displacementMap,L=!!i.emissiveMap,oe=!!i.metalnessMap,se=!!i.roughnessMap,ce=i.anisotropy>0,le=i.clearcoat>0,ue=i.dispersion>0,de=i.retroreflectivity>0,fe=i.iridescence>0,pe=i.sheen>0,me=i.transmission>0,he=ce&&!!i.anisotropyMap,ge=le&&!!i.clearcoatMap,_e=le&&!!i.clearcoatNormalMap,ve=le&&!!i.clearcoatRoughnessMap,ye=fe&&!!i.iridescenceMap,R=fe&&!!i.iridescenceThicknessMap,be=pe&&!!i.sheenColorMap,xe=pe&&!!i.sheenRoughnessMap,Se=!!i.specularMap,z=!!i.specularColorMap,Ce=!!i.specularIntensityMap,we=me&&!!i.transmissionMap,Te=me&&!!i.thicknessMap,Ee=!!i.gradientMap,De=!!i.alphaMap,Oe=i.alphaTest>0,ke=!!i.alphaHash,Ae=!!i.extensions,je=0;i.toneMapped&&(A===null||A.isXRRenderTarget===!0)&&(je=e.toneMapping);let Me={shaderID:C,shaderType:i.type,shaderName:i.name,vertexShader:D,fragmentShader:O,defines:i.defines,customVertexShaderID:ee,customFragmentShaderID:k,isRawShaderMaterial:i.isRawShaderMaterial===!0,glslVersion:i.glslVersion,precision:f,batching:M,batchingColor:M&&h._colorsTexture!==null,instancing:j,instancingColor:j&&h.instanceColor!==null,instancingMorph:j&&h.morphTexture!==null,outputColorSpace:A===null?e.outputColorSpace:A.isXRRenderTarget===!0?A.texture.colorSpace:Uu.workingColorSpace,alphaToCoverage:!!i.alphaToCoverage,map:ne,matcap:N,envMap:P,envMapMode:P&&x.mapping,envMapCubeUVHeight:S,aoMap:F,lightMap:re,bumpMap:ie,normalMap:ae,displacementMap:I,emissiveMap:L,normalMapObjectSpace:ae&&i.normalMapType===1,normalMapTangentSpace:ae&&i.normalMapType===0,packedNormalMap:ae&&i.normalMapType===0&&Cy(i.normalMap.format),metalnessMap:oe,roughnessMap:se,anisotropy:ce,anisotropyMap:he,clearcoat:le,clearcoatMap:ge,clearcoatNormalMap:_e,clearcoatRoughnessMap:ve,dispersion:ue,retroreflection:de,iridescence:fe,iridescenceMap:ye,iridescenceThicknessMap:R,sheen:pe,sheenColorMap:be,sheenRoughnessMap:xe,specularMap:Se,specularColorMap:z,specularIntensityMap:Ce,transmission:me,transmissionMap:we,thicknessMap:Te,gradientMap:Ee,opaque:i.transparent===!1&&i.blending===1&&i.alphaToCoverage===!1,alphaMap:De,alphaTest:Oe,alphaHash:ke,combine:i.combine,mapUv:ne&&m(i.map.channel),aoMapUv:F&&m(i.aoMap.channel),lightMapUv:re&&m(i.lightMap.channel),bumpMapUv:ie&&m(i.bumpMap.channel),normalMapUv:ae&&m(i.normalMap.channel),displacementMapUv:I&&m(i.displacementMap.channel),emissiveMapUv:L&&m(i.emissiveMap.channel),metalnessMapUv:oe&&m(i.metalnessMap.channel),roughnessMapUv:se&&m(i.roughnessMap.channel),anisotropyMapUv:he&&m(i.anisotropyMap.channel),clearcoatMapUv:ge&&m(i.clearcoatMap.channel),clearcoatNormalMapUv:_e&&m(i.clearcoatNormalMap.channel),clearcoatRoughnessMapUv:ve&&m(i.clearcoatRoughnessMap.channel),iridescenceMapUv:ye&&m(i.iridescenceMap.channel),iridescenceThicknessMapUv:R&&m(i.iridescenceThicknessMap.channel),sheenColorMapUv:be&&m(i.sheenColorMap.channel),sheenRoughnessMapUv:xe&&m(i.sheenRoughnessMap.channel),specularMapUv:Se&&m(i.specularMap.channel),specularColorMapUv:z&&m(i.specularColorMap.channel),specularIntensityMapUv:Ce&&m(i.specularIntensityMap.channel),transmissionMapUv:we&&m(i.transmissionMap.channel),thicknessMapUv:Te&&m(i.thicknessMap.channel),alphaMapUv:De&&m(i.alphaMap.channel),vertexTangents:!!v.attributes.tangent&&(ae||ce),vertexNormals:!!v.attributes.normal,vertexColors:i.vertexColors,vertexAlphas:i.vertexColors===!0&&!!v.attributes.color&&v.attributes.color.itemSize===4,pointsUvs:h.isPoints===!0&&!!v.attributes.uv&&(ne||De),fog:!!_,useFog:i.fog===!0,fogExp2:!!_&&_.isFogExp2,flatShading:i.wireframe===!1&&(i.flatShading===!0||v.attributes.normal===void 0&&ae===!1&&(i.isMeshLambertMaterial||i.isMeshPhongMaterial||i.isMeshStandardMaterial||i.isMeshPhysicalMaterial)),sizeAttenuation:i.sizeAttenuation===!0,logarithmicDepthBuffer:d,reversedDepthBuffer:te,skinning:h.isSkinnedMesh===!0,hasPositionAttribute:v.attributes.position!==void 0,morphTargets:v.morphAttributes.position!==void 0,morphNormals:v.morphAttributes.normal!==void 0,morphColors:v.morphAttributes.color!==void 0,morphTargetsCount:T,morphTextureStride:E,numSunLights:o.sun.length,numDirLights:o.directional.length,numPointLights:o.point.length,numSpotLights:o.spot.length,numSpotLightMaps:o.spotLightMap.length,numRectAreaLights:o.rectArea.length,numHemiLights:o.hemi.length,numSunLightShadows:o.sunShadowMap.length,numDirLightShadows:o.directionalShadowMap.length,numPointLightShadows:o.pointShadowMap.length,numSpotLightShadows:o.spotShadowMap.length,numSpotLightShadowsWithMaps:o.numSpotLightShadowsWithMaps,numLightProbes:o.numLightProbes,numLightProbeGrids:g.length,numClippingPlanes:a.numPlanes,numClipIntersection:a.numIntersection,dithering:i.dithering,shadowMapEnabled:e.shadowMap.enabled&&l.length>0,shadowMapType:e.shadowMap.type,toneMapping:je,decodeVideoTexture:ne&&i.map.isVideoTexture===!0&&Uu.getTransfer(i.map.colorSpace)===`srgb`,decodeVideoTextureEmissive:L&&i.emissiveMap.isVideoTexture===!0&&Uu.getTransfer(i.emissiveMap.colorSpace)===`srgb`,premultipliedAlpha:i.premultipliedAlpha,doubleSided:i.side===2,flipSided:i.side===1,useDepthPacking:i.depthPacking>=0,depthPacking:i.depthPacking||0,index0AttributeName:i.index0AttributeName,extensionClipCullDistance:Ae&&i.extensions.clipCullDistance===!0&&n.has(`WEBGL_clip_cull_distance`),extensionMultiDraw:(Ae&&i.extensions.multiDraw===!0||M)&&n.has(`WEBGL_multi_draw`),rendererExtensionParallelShaderCompile:n.has(`KHR_parallel_shader_compile`),customProgramCacheKey:i.customProgramCacheKey()};return Me.vertexUv1s=c.has(1),Me.vertexUv2s=c.has(2),Me.vertexUv3s=c.has(3),c.clear(),Me}function g(t){let n=[];if(t.shaderID?n.push(t.shaderID):(n.push(t.customVertexShaderID),n.push(t.customFragmentShaderID)),t.defines!==void 0)for(let e in t.defines)n.push(e),n.push(t.defines[e]);return t.isRawShaderMaterial===!1&&(_(n,t),v(n,t),n.push(e.outputColorSpace)),n.push(t.customProgramCacheKey),n.join()}function _(e,t){e.push(t.precision),e.push(t.outputColorSpace),e.push(t.envMapMode),e.push(t.envMapCubeUVHeight),e.push(t.mapUv),e.push(t.alphaMapUv),e.push(t.lightMapUv),e.push(t.aoMapUv),e.push(t.bumpMapUv),e.push(t.normalMapUv),e.push(t.displacementMapUv),e.push(t.emissiveMapUv),e.push(t.metalnessMapUv),e.push(t.roughnessMapUv),e.push(t.anisotropyMapUv),e.push(t.clearcoatMapUv),e.push(t.clearcoatNormalMapUv),e.push(t.clearcoatRoughnessMapUv),e.push(t.iridescenceMapUv),e.push(t.iridescenceThicknessMapUv),e.push(t.sheenColorMapUv),e.push(t.sheenRoughnessMapUv),e.push(t.specularMapUv),e.push(t.specularColorMapUv),e.push(t.specularIntensityMapUv),e.push(t.transmissionMapUv),e.push(t.thicknessMapUv),e.push(t.combine),e.push(t.fogExp2),e.push(t.sizeAttenuation),e.push(t.morphTargetsCount),e.push(t.morphAttributeCount),e.push(t.numSunLights),e.push(t.numDirLights),e.push(t.numPointLights),e.push(t.numSpotLights),e.push(t.numSpotLightMaps),e.push(t.numHemiLights),e.push(t.numRectAreaLights),e.push(t.numSunLightShadows),e.push(t.numDirLightShadows),e.push(t.numPointLightShadows),e.push(t.numSpotLightShadows),e.push(t.numSpotLightShadowsWithMaps),e.push(t.numLightProbes),e.push(t.shadowMapType),e.push(t.toneMapping),e.push(t.numClippingPlanes),e.push(t.numClipIntersection),e.push(t.depthPacking)}function v(e,t){o.disableAll(),t.instancing&&o.enable(0),t.instancingColor&&o.enable(1),t.instancingMorph&&o.enable(2),t.matcap&&o.enable(3),t.envMap&&o.enable(4),t.normalMapObjectSpace&&o.enable(5),t.normalMapTangentSpace&&o.enable(6),t.clearcoat&&o.enable(7),t.iridescence&&o.enable(8),t.alphaTest&&o.enable(9),t.vertexColors&&o.enable(10),t.vertexAlphas&&o.enable(11),t.vertexUv1s&&o.enable(12),t.vertexUv2s&&o.enable(13),t.vertexUv3s&&o.enable(14),t.vertexTangents&&o.enable(15),t.anisotropy&&o.enable(16),t.alphaHash&&o.enable(17),t.batching&&o.enable(18),t.dispersion&&o.enable(19),t.retroreflection&&o.enable(24),t.batchingColor&&o.enable(20),t.gradientMap&&o.enable(21),t.packedNormalMap&&o.enable(22),t.vertexNormals&&o.enable(23),e.push(o.mask),o.disableAll(),t.fog&&o.enable(0),t.useFog&&o.enable(1),t.flatShading&&o.enable(2),t.logarithmicDepthBuffer&&o.enable(3),t.reversedDepthBuffer&&o.enable(4),t.skinning&&o.enable(5),t.morphTargets&&o.enable(6),t.morphNormals&&o.enable(7),t.morphColors&&o.enable(8),t.premultipliedAlpha&&o.enable(9),t.shadowMapEnabled&&o.enable(10),t.doubleSided&&o.enable(11),t.flipSided&&o.enable(12),t.useDepthPacking&&o.enable(13),t.dithering&&o.enable(14),t.transmission&&o.enable(15),t.sheen&&o.enable(16),t.opaque&&o.enable(17),t.pointsUvs&&o.enable(18),t.decodeVideoTexture&&o.enable(19),t.decodeVideoTextureEmissive&&o.enable(20),t.alphaToCoverage&&o.enable(21),t.numLightProbeGrids>0&&o.enable(22),t.hasPositionAttribute&&o.enable(23),e.push(o.mask)}function y(e){let t=p[e.type],n;if(t){let e=Gg[t];n=nh.clone(e.uniforms)}else n=e.uniforms;return n}function b(t,n){let r=u.get(n);return r===void 0?(r=new yy(e,n,t,i),l.push(r),u.set(n,r)):++r.usedTimes,r}function x(e){if(--e.usedTimes===0){let t=l.indexOf(e);l[t]=l[l.length-1],l.pop(),u.delete(e.cacheKey),e.destroy()}}function S(e){s.remove(e)}function C(){s.dispose()}return{getParameters:h,getProgramCacheKey:g,getUniforms:y,acquireProgram:b,releaseProgram:x,releaseShaderCache:S,programs:l,dispose:C}}function Ty(){let e=new WeakMap;function t(t){return e.has(t)}function n(t){let n=e.get(t);return n===void 0&&(n={},e.set(t,n)),n}function r(t){e.delete(t)}function i(t,n,r){e.get(t)[n]=r}function a(){e=new WeakMap}return{has:t,get:n,remove:r,update:i,dispose:a}}function Ey(e,t){return e.groupOrder===t.groupOrder?e.renderOrder===t.renderOrder?e.material.id===t.material.id?e.materialVariant===t.materialVariant?e.z===t.z?e.id-t.id:e.z-t.z:e.materialVariant-t.materialVariant:e.material.id-t.material.id:e.renderOrder-t.renderOrder:e.groupOrder-t.groupOrder}function Dy(e,t){return e.groupOrder===t.groupOrder?e.renderOrder===t.renderOrder?e.z===t.z?e.id-t.id:t.z-e.z:e.renderOrder-t.renderOrder:e.groupOrder-t.groupOrder}function Oy(){let e=[],t=0,n=[],r=[],i=[];function a(){t=0,n.length=0,r.length=0,i.length=0}function o(e){let t=0;return e.isInstancedMesh&&(t+=2),e.isSkinnedMesh&&(t+=1),t}function s(n,r,i,a,s,c){let l=e[t];return l===void 0?(l={id:n.id,object:n,geometry:r,material:i,materialVariant:o(n),groupOrder:a,renderOrder:n.renderOrder,z:s,group:c},e[t]=l):(l.id=n.id,l.object=n,l.geometry=r,l.material=i,l.materialVariant=o(n),l.groupOrder=a,l.renderOrder=n.renderOrder,l.z=s,l.group=c),t++,l}function c(e,t,a,o,c,l,u){u.reversedDepth===!0&&(c=-c);let d=s(e,t,a,o,c,l);a.transmission>0?r.push(d):a.transparent===!0?i.push(d):n.push(d)}function l(e,t,a,o,c,l){let u=s(e,t,a,o,c,l);a.transmission>0?r.unshift(u):a.transparent===!0?i.unshift(u):n.unshift(u)}function u(e,t){n.length>1&&n.sort(e||Ey),r.length>1&&r.sort(t||Dy),i.length>1&&i.sort(t||Dy)}function d(){for(let n=t,r=e.length;n=r.length?(i=new Oy,r.push(i)):i=r[n],i}function n(){e=new WeakMap}return{get:t,dispose:n}}function Ay(){let e={};return{get:function(t){if(e[t.id]!==void 0)return e[t.id];let n;switch(t.type){case`SunLight`:case`DirectionalLight`:n={direction:new Z,color:new Q};break;case`SpotLight`:n={position:new Z,direction:new Z,color:new Q,distance:0,coneCos:0,penumbraCos:0,decay:0};break;case`PointLight`:n={position:new Z,color:new Q,distance:0,decay:0};break;case`HemisphereLight`:n={direction:new Z,skyColor:new Q,groundColor:new Q};break;case`RectAreaLight`:n={color:new Q,position:new Z,halfWidth:new Z,halfHeight:new Z};break}return e[t.id]=n,n}}}function jy(){let e={};return{get:function(t){if(e[t.id]!==void 0)return e[t.id];let n;switch(t.type){case`SunLight`:case`DirectionalLight`:n={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new X};break;case`SpotLight`:n={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new X};break;case`PointLight`:n={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new X,shadowCameraNear:1,shadowCameraFar:1e3};break}return e[t.id]=n,n}}}var My=0;function Ny(e,t){return(t.castShadow?2:0)-(e.castShadow?2:0)+ +!!t.map-!!e.map}function Py(e){let t=new Ay,n=jy(),r={version:0,hash:{sunLength:-1,directionalLength:-1,pointLength:-1,spotLength:-1,rectAreaLength:-1,hemiLength:-1,numSunShadows:-1,numDirectionalShadows:-1,numPointShadows:-1,numSpotShadows:-1,numSpotMaps:-1,numLightProbes:-1},ambient:[0,0,0],probe:[],sun:[],sunShadow:[],sunShadowMap:[],sunShadowMatrix:[],sunShadowCascade:[],directional:[],directionalShadow:[],directionalShadowMap:[],directionalShadowMatrix:[],spot:[],spotLightMap:[],spotShadow:[],spotShadowMap:[],spotLightMatrix:[],rectArea:[],rectAreaLTC1:null,rectAreaLTC2:null,point:[],pointShadow:[],pointShadowMap:[],pointShadowMatrix:[],hemi:[],numSpotLightShadowsWithMaps:0,numLightProbes:0};for(let e=0;e<9;e++)r.probe.push(new Z);let i=new Z,a=new ad,o=new ad;function s(i){let a=0,o=0,s=0;for(let e=0;e<9;e++)r.probe[e].set(0,0,0);let c=0,l=0,u=0,d=0,f=0,p=0,m=0,h=0,g=0,_=0,v=0,y=0,b=0,x=0;i.sort(Ny);for(let e=0,S=i.length;e0&&(e.has(`OES_texture_float_linear`)===!0?(r.rectAreaLTC1=$.LTC_FLOAT_1,r.rectAreaLTC2=$.LTC_FLOAT_2):(r.rectAreaLTC1=$.LTC_HALF_1,r.rectAreaLTC2=$.LTC_HALF_2)),r.ambient[0]=a,r.ambient[1]=o,r.ambient[2]=s;let S=r.hash;(S.sunLength!==c||S.directionalLength!==d||S.pointLength!==f||S.spotLength!==p||S.rectAreaLength!==m||S.hemiLength!==h||S.numSunShadows!==l||S.numDirectionalShadows!==g||S.numPointShadows!==_||S.numSpotShadows!==v||S.numSpotMaps!==y||S.numLightProbes!==x)&&(r.sun.length=c,r.directional.length=d,r.spot.length=p,r.rectArea.length=m,r.point.length=f,r.hemi.length=h,r.sunShadow.length=l,r.sunShadowMap.length=l,r.sunShadowMatrix.length=u,r.sunShadowCascade.length=u,r.directionalShadow.length=g,r.directionalShadowMap.length=g,r.directionalShadowMatrix.length=g,r.pointShadow.length=_,r.pointShadowMap.length=_,r.pointShadowMatrix.length=_,r.spotShadow.length=v,r.spotShadowMap.length=v,r.spotLightMatrix.length=v+y-b,r.spotLightMap.length=y,r.numSpotLightShadowsWithMaps=b,r.numLightProbes=x,S.sunLength=c,S.directionalLength=d,S.pointLength=f,S.spotLength=p,S.rectAreaLength=m,S.hemiLength=h,S.numSunShadows=l,S.numDirectionalShadows=g,S.numPointShadows=_,S.numSpotShadows=v,S.numSpotMaps=y,S.numLightProbes=x,r.version=My++)}function c(e,t){let n=0,s=0,c=0,l=0,u=0,d=0,f=t.matrixWorldInverse;for(let t=0,p=e.length;t=i.length?(a=new Fy(e),i.push(a)):a=i[r],a}function r(){t=new WeakMap}return{get:n,dispose:r}}var Ly=`void main() { +`+n)}else o===``?(s===``||c===``)&&(u=!1):Ap(`WebGLProgram: Program Info Log:`,o);u&&(t.diagnostics={runnable:l,programLog:o,vertexShader:{log:s,prefix:g},fragmentShader:{log:c,prefix:_}})}i.deleteShader(x),i.deleteShader(S),w=new fS(i,h),T=DS(i,h)}let w;this.getUniforms=function(){return w===void 0&&C(this),w};let T;this.getAttributes=function(){return T===void 0&&C(this),T};let E=n.rendererExtensionParallelShaderCompile===!1;return this.isReady=function(){return E===!1&&(E=i.getProgramParameter(h,mS)),E},this.destroy=function(){r.releaseStatesOfProgram(this),i.deleteProgram(h),this.program=void 0},this.type=n.shaderType,this.name=n.shaderName,this.id=hS++,this.cacheKey=t,this.usedTimes=1,this.program=h,this.vertexShader=x,this.fragmentShader=S,this}var YS=0,XS=class{constructor(){this.shaderCache=new Map,this.materialCache=new Map}update(e,t,n){let r=this._getShaderCacheForMaterial(e);return r.has(t)===!1&&(r.add(t),t.usedTimes++),r.has(n)===!1&&(r.add(n),n.usedTimes++),this}remove(e){let t=this.materialCache.get(e);for(let e of t)e.usedTimes--,e.usedTimes===0&&this.shaderCache.delete(e.code);return this.materialCache.delete(e),this}getVertexShaderStage(e){return this._getShaderStage(e.vertexShader)}getFragmentShaderStage(e){return this._getShaderStage(e.fragmentShader)}dispose(){this.shaderCache.clear(),this.materialCache.clear()}_getShaderCacheForMaterial(e){let t=this.materialCache,n=t.get(e);return n===void 0&&(n=new Set,t.set(e,n)),n}_getShaderStage(e){let t=this.shaderCache,n=t.get(e);return n===void 0&&(n=new ZS(e),t.set(e,n)),n}},ZS=class{constructor(e){this.id=YS++,this.code=e,this.usedTimes=0}};function QS(e){return e===1030||e===37490||e===36285}function $S(e,t,n,r,i,a){let o=new Um,s=new XS,c=new Set,l=[],u=new Map,d=r.logarithmicDepthBuffer,f=r.precision,p={MeshDepthMaterial:`depth`,MeshDistanceMaterial:`distance`,MeshNormalMaterial:`normal`,MeshBasicMaterial:`basic`,MeshLambertMaterial:`lambert`,MeshPhongMaterial:`phong`,MeshToonMaterial:`toon`,MeshStandardMaterial:`physical`,MeshPhysicalMaterial:`physical`,MeshMatcapMaterial:`matcap`,LineBasicMaterial:`basic`,LineDashedMaterial:`dashed`,PointsMaterial:`points`,ShadowMaterial:`shadow`,SpriteMaterial:`sprite`};function m(e){return c.add(e),e===0?`uv`:`uv${e}`}function h(i,o,l,u,h,g){let _=u.fog,v=h.geometry,y=i.isMeshStandardMaterial||i.isMeshLambertMaterial||i.isMeshPhongMaterial?u.environment:null,b=i.isMeshStandardMaterial||i.isMeshLambertMaterial&&!i.envMap||i.isMeshPhongMaterial&&!i.envMap,x=t.get(i.envMap||y,b),S=x&&x.mapping===306?x.image.height:null,C=p[i.type];i.precision!==null&&(f=r.getMaxPrecision(i.precision),f!==i.precision&&Ap(`WebGLProgram.getParameters:`,i.precision,`not supported, using`,f,`instead.`));let w=v.morphAttributes.position||v.morphAttributes.normal||v.morphAttributes.color,T=w===void 0?0:w.length,E=0;v.morphAttributes.position!==void 0&&(E=1),v.morphAttributes.normal!==void 0&&(E=2),v.morphAttributes.color!==void 0&&(E=3);let D,O,ee,te;if(C){let e=bb[C];D=e.vertexShader,O=e.fragmentShader}else{D=i.vertexShader,O=i.fragmentShader;let e=s.getVertexShaderStage(i),t=s.getFragmentShaderStage(i);s.update(i,e,t),ee=e.id,te=t.id}let k=e.getRenderTarget(),ne=e.state.buffers.depth.getReversed(),A=h.isInstancedMesh===!0,re=h.isBatchedMesh===!0,ie=!!i.map,ae=!!i.matcap,oe=!!x,j=!!i.aoMap,se=!!i.lightMap,ce=!!i.bumpMap&&i.wireframe===!1,le=!!i.normalMap,ue=!!i.displacementMap,M=!!i.emissiveMap,N=!!i.metalnessMap,de=!!i.roughnessMap,fe=i.anisotropy>0,pe=i.clearcoat>0,me=i.dispersion>0,P=i.retroreflectivity>0,F=i.iridescence>0,he=i.sheen>0,ge=i.transmission>0,_e=fe&&!!i.anisotropyMap,ve=pe&&!!i.clearcoatMap,ye=pe&&!!i.clearcoatNormalMap,be=pe&&!!i.clearcoatRoughnessMap,xe=F&&!!i.iridescenceMap,I=F&&!!i.iridescenceThicknessMap,Se=he&&!!i.sheenColorMap,Ce=he&&!!i.sheenRoughnessMap,we=!!i.specularMap,L=!!i.specularColorMap,Te=!!i.specularIntensityMap,Ee=ge&&!!i.transmissionMap,De=ge&&!!i.thicknessMap,Oe=!!i.gradientMap,ke=!!i.alphaMap,Ae=i.alphaTest>0,je=!!i.alphaHash,Me=!!i.extensions,Ne=0;i.toneMapped&&(k===null||k.isXRRenderTarget===!0)&&(Ne=e.toneMapping);let Pe={shaderID:C,shaderType:i.type,shaderName:i.name,vertexShader:D,fragmentShader:O,defines:i.defines,customVertexShaderID:ee,customFragmentShaderID:te,isRawShaderMaterial:i.isRawShaderMaterial===!0,glslVersion:i.glslVersion,precision:f,batching:re,batchingColor:re&&h._colorsTexture!==null,instancing:A,instancingColor:A&&h.instanceColor!==null,instancingMorph:A&&h.morphTexture!==null,outputColorSpace:k===null?e.outputColorSpace:k.isXRRenderTarget===!0?k.texture.colorSpace:gm.workingColorSpace,alphaToCoverage:!!i.alphaToCoverage,map:ie,matcap:ae,envMap:oe,envMapMode:oe&&x.mapping,envMapCubeUVHeight:S,aoMap:j,lightMap:se,bumpMap:ce,normalMap:le,displacementMap:ue,emissiveMap:M,normalMapObjectSpace:le&&i.normalMapType===1,normalMapTangentSpace:le&&i.normalMapType===0,packedNormalMap:le&&i.normalMapType===0&&QS(i.normalMap.format),metalnessMap:N,roughnessMap:de,anisotropy:fe,anisotropyMap:_e,clearcoat:pe,clearcoatMap:ve,clearcoatNormalMap:ye,clearcoatRoughnessMap:be,dispersion:me,retroreflection:P,iridescence:F,iridescenceMap:xe,iridescenceThicknessMap:I,sheen:he,sheenColorMap:Se,sheenRoughnessMap:Ce,specularMap:we,specularColorMap:L,specularIntensityMap:Te,transmission:ge,transmissionMap:Ee,thicknessMap:De,gradientMap:Oe,opaque:i.transparent===!1&&i.blending===1&&i.alphaToCoverage===!1,alphaMap:ke,alphaTest:Ae,alphaHash:je,combine:i.combine,mapUv:ie&&m(i.map.channel),aoMapUv:j&&m(i.aoMap.channel),lightMapUv:se&&m(i.lightMap.channel),bumpMapUv:ce&&m(i.bumpMap.channel),normalMapUv:le&&m(i.normalMap.channel),displacementMapUv:ue&&m(i.displacementMap.channel),emissiveMapUv:M&&m(i.emissiveMap.channel),metalnessMapUv:N&&m(i.metalnessMap.channel),roughnessMapUv:de&&m(i.roughnessMap.channel),anisotropyMapUv:_e&&m(i.anisotropyMap.channel),clearcoatMapUv:ve&&m(i.clearcoatMap.channel),clearcoatNormalMapUv:ye&&m(i.clearcoatNormalMap.channel),clearcoatRoughnessMapUv:be&&m(i.clearcoatRoughnessMap.channel),iridescenceMapUv:xe&&m(i.iridescenceMap.channel),iridescenceThicknessMapUv:I&&m(i.iridescenceThicknessMap.channel),sheenColorMapUv:Se&&m(i.sheenColorMap.channel),sheenRoughnessMapUv:Ce&&m(i.sheenRoughnessMap.channel),specularMapUv:we&&m(i.specularMap.channel),specularColorMapUv:L&&m(i.specularColorMap.channel),specularIntensityMapUv:Te&&m(i.specularIntensityMap.channel),transmissionMapUv:Ee&&m(i.transmissionMap.channel),thicknessMapUv:De&&m(i.thicknessMap.channel),alphaMapUv:ke&&m(i.alphaMap.channel),vertexTangents:!!v.attributes.tangent&&(le||fe),vertexNormals:!!v.attributes.normal,vertexColors:i.vertexColors,vertexAlphas:i.vertexColors===!0&&!!v.attributes.color&&v.attributes.color.itemSize===4,pointsUvs:h.isPoints===!0&&!!v.attributes.uv&&(ie||ke),fog:!!_,useFog:i.fog===!0,fogExp2:!!_&&_.isFogExp2,flatShading:i.wireframe===!1&&(i.flatShading===!0||v.attributes.normal===void 0&&le===!1&&(i.isMeshLambertMaterial||i.isMeshPhongMaterial||i.isMeshStandardMaterial||i.isMeshPhysicalMaterial)),sizeAttenuation:i.sizeAttenuation===!0,logarithmicDepthBuffer:d,reversedDepthBuffer:ne,skinning:h.isSkinnedMesh===!0,hasPositionAttribute:v.attributes.position!==void 0,morphTargets:v.morphAttributes.position!==void 0,morphNormals:v.morphAttributes.normal!==void 0,morphColors:v.morphAttributes.color!==void 0,morphTargetsCount:T,morphTextureStride:E,numSunLights:o.sun.length,numDirLights:o.directional.length,numPointLights:o.point.length,numSpotLights:o.spot.length,numSpotLightMaps:o.spotLightMap.length,numRectAreaLights:o.rectArea.length,numHemiLights:o.hemi.length,numSunLightShadows:o.sunShadowMap.length,numDirLightShadows:o.directionalShadowMap.length,numPointLightShadows:o.pointShadowMap.length,numSpotLightShadows:o.spotShadowMap.length,numSpotLightShadowsWithMaps:o.numSpotLightShadowsWithMaps,numLightProbes:o.numLightProbes,numLightProbeGrids:g.length,numClippingPlanes:a.numPlanes,numClipIntersection:a.numIntersection,dithering:i.dithering,shadowMapEnabled:e.shadowMap.enabled&&l.length>0,shadowMapType:e.shadowMap.type,toneMapping:Ne,decodeVideoTexture:ie&&i.map.isVideoTexture===!0&&gm.getTransfer(i.map.colorSpace)===`srgb`,decodeVideoTextureEmissive:M&&i.emissiveMap.isVideoTexture===!0&&gm.getTransfer(i.emissiveMap.colorSpace)===`srgb`,premultipliedAlpha:i.premultipliedAlpha,doubleSided:i.side===2,flipSided:i.side===1,useDepthPacking:i.depthPacking>=0,depthPacking:i.depthPacking||0,index0AttributeName:i.index0AttributeName,extensionClipCullDistance:Me&&i.extensions.clipCullDistance===!0&&n.has(`WEBGL_clip_cull_distance`),extensionMultiDraw:(Me&&i.extensions.multiDraw===!0||re)&&n.has(`WEBGL_multi_draw`),rendererExtensionParallelShaderCompile:n.has(`KHR_parallel_shader_compile`),customProgramCacheKey:i.customProgramCacheKey()};return Pe.vertexUv1s=c.has(1),Pe.vertexUv2s=c.has(2),Pe.vertexUv3s=c.has(3),c.clear(),Pe}function g(t){let n=[];if(t.shaderID?n.push(t.shaderID):(n.push(t.customVertexShaderID),n.push(t.customFragmentShaderID)),t.defines!==void 0)for(let e in t.defines)n.push(e),n.push(t.defines[e]);return t.isRawShaderMaterial===!1&&(_(n,t),v(n,t),n.push(e.outputColorSpace)),n.push(t.customProgramCacheKey),n.join()}function _(e,t){e.push(t.precision),e.push(t.outputColorSpace),e.push(t.envMapMode),e.push(t.envMapCubeUVHeight),e.push(t.mapUv),e.push(t.alphaMapUv),e.push(t.lightMapUv),e.push(t.aoMapUv),e.push(t.bumpMapUv),e.push(t.normalMapUv),e.push(t.displacementMapUv),e.push(t.emissiveMapUv),e.push(t.metalnessMapUv),e.push(t.roughnessMapUv),e.push(t.anisotropyMapUv),e.push(t.clearcoatMapUv),e.push(t.clearcoatNormalMapUv),e.push(t.clearcoatRoughnessMapUv),e.push(t.iridescenceMapUv),e.push(t.iridescenceThicknessMapUv),e.push(t.sheenColorMapUv),e.push(t.sheenRoughnessMapUv),e.push(t.specularMapUv),e.push(t.specularColorMapUv),e.push(t.specularIntensityMapUv),e.push(t.transmissionMapUv),e.push(t.thicknessMapUv),e.push(t.combine),e.push(t.fogExp2),e.push(t.sizeAttenuation),e.push(t.morphTargetsCount),e.push(t.morphAttributeCount),e.push(t.numSunLights),e.push(t.numDirLights),e.push(t.numPointLights),e.push(t.numSpotLights),e.push(t.numSpotLightMaps),e.push(t.numHemiLights),e.push(t.numRectAreaLights),e.push(t.numSunLightShadows),e.push(t.numDirLightShadows),e.push(t.numPointLightShadows),e.push(t.numSpotLightShadows),e.push(t.numSpotLightShadowsWithMaps),e.push(t.numLightProbes),e.push(t.shadowMapType),e.push(t.toneMapping),e.push(t.numClippingPlanes),e.push(t.numClipIntersection),e.push(t.depthPacking)}function v(e,t){o.disableAll(),t.instancing&&o.enable(0),t.instancingColor&&o.enable(1),t.instancingMorph&&o.enable(2),t.matcap&&o.enable(3),t.envMap&&o.enable(4),t.normalMapObjectSpace&&o.enable(5),t.normalMapTangentSpace&&o.enable(6),t.clearcoat&&o.enable(7),t.iridescence&&o.enable(8),t.alphaTest&&o.enable(9),t.vertexColors&&o.enable(10),t.vertexAlphas&&o.enable(11),t.vertexUv1s&&o.enable(12),t.vertexUv2s&&o.enable(13),t.vertexUv3s&&o.enable(14),t.vertexTangents&&o.enable(15),t.anisotropy&&o.enable(16),t.alphaHash&&o.enable(17),t.batching&&o.enable(18),t.dispersion&&o.enable(19),t.retroreflection&&o.enable(24),t.batchingColor&&o.enable(20),t.gradientMap&&o.enable(21),t.packedNormalMap&&o.enable(22),t.vertexNormals&&o.enable(23),e.push(o.mask),o.disableAll(),t.fog&&o.enable(0),t.useFog&&o.enable(1),t.flatShading&&o.enable(2),t.logarithmicDepthBuffer&&o.enable(3),t.reversedDepthBuffer&&o.enable(4),t.skinning&&o.enable(5),t.morphTargets&&o.enable(6),t.morphNormals&&o.enable(7),t.morphColors&&o.enable(8),t.premultipliedAlpha&&o.enable(9),t.shadowMapEnabled&&o.enable(10),t.doubleSided&&o.enable(11),t.flipSided&&o.enable(12),t.useDepthPacking&&o.enable(13),t.dithering&&o.enable(14),t.transmission&&o.enable(15),t.sheen&&o.enable(16),t.opaque&&o.enable(17),t.pointsUvs&&o.enable(18),t.decodeVideoTexture&&o.enable(19),t.decodeVideoTextureEmissive&&o.enable(20),t.alphaToCoverage&&o.enable(21),t.numLightProbeGrids>0&&o.enable(22),t.hasPositionAttribute&&o.enable(23),e.push(o.mask)}function y(e){let t=p[e.type],n;if(t){let e=bb[t];n=kv.clone(e.uniforms)}else n=e.uniforms;return n}function b(t,n){let r=u.get(n);return r===void 0?(r=new JS(e,n,t,i),l.push(r),u.set(n,r)):++r.usedTimes,r}function x(e){if(--e.usedTimes===0){let t=l.indexOf(e);l[t]=l[l.length-1],l.pop(),u.delete(e.cacheKey),e.destroy()}}function S(e){s.remove(e)}function C(){s.dispose()}return{getParameters:h,getProgramCacheKey:g,getUniforms:y,acquireProgram:b,releaseProgram:x,releaseShaderCache:S,programs:l,dispose:C}}function eC(){let e=new WeakMap;function t(t){return e.has(t)}function n(t){let n=e.get(t);return n===void 0&&(n={},e.set(t,n)),n}function r(t){e.delete(t)}function i(t,n,r){e.get(t)[n]=r}function a(){e=new WeakMap}return{has:t,get:n,remove:r,update:i,dispose:a}}function tC(e,t){return e.groupOrder===t.groupOrder?e.renderOrder===t.renderOrder?e.material.id===t.material.id?e.materialVariant===t.materialVariant?e.z===t.z?e.id-t.id:e.z-t.z:e.materialVariant-t.materialVariant:e.material.id-t.material.id:e.renderOrder-t.renderOrder:e.groupOrder-t.groupOrder}function nC(e,t){return e.groupOrder===t.groupOrder?e.renderOrder===t.renderOrder?e.z===t.z?e.id-t.id:t.z-e.z:e.renderOrder-t.renderOrder:e.groupOrder-t.groupOrder}function rC(){let e=[],t=0,n=[],r=[],i=[];function a(){t=0,n.length=0,r.length=0,i.length=0}function o(e){let t=0;return e.isInstancedMesh&&(t+=2),e.isSkinnedMesh&&(t+=1),t}function s(n,r,i,a,s,c){let l=e[t];return l===void 0?(l={id:n.id,object:n,geometry:r,material:i,materialVariant:o(n),groupOrder:a,renderOrder:n.renderOrder,z:s,group:c},e[t]=l):(l.id=n.id,l.object=n,l.geometry=r,l.material=i,l.materialVariant=o(n),l.groupOrder=a,l.renderOrder=n.renderOrder,l.z=s,l.group=c),t++,l}function c(e,t,a,o,c,l,u){u.reversedDepth===!0&&(c=-c);let d=s(e,t,a,o,c,l);a.transmission>0?r.push(d):a.transparent===!0?i.push(d):n.push(d)}function l(e,t,a,o,c,l){let u=s(e,t,a,o,c,l);a.transmission>0?r.unshift(u):a.transparent===!0?i.unshift(u):n.unshift(u)}function u(e,t){n.length>1&&n.sort(e||tC),r.length>1&&r.sort(t||nC),i.length>1&&i.sort(t||nC)}function d(){for(let n=t,r=e.length;n=r.length?(i=new rC,r.push(i)):i=r[n],i}function n(){e=new WeakMap}return{get:t,dispose:n}}function aC(){let e={};return{get:function(t){if(e[t.id]!==void 0)return e[t.id];let n;switch(t.type){case`SunLight`:case`DirectionalLight`:n={direction:new J,color:new Y};break;case`SpotLight`:n={position:new J,direction:new J,color:new Y,distance:0,coneCos:0,penumbraCos:0,decay:0};break;case`PointLight`:n={position:new J,color:new Y,distance:0,decay:0};break;case`HemisphereLight`:n={direction:new J,skyColor:new Y,groundColor:new Y};break;case`RectAreaLight`:n={color:new Y,position:new J,halfWidth:new J,halfHeight:new J};break}return e[t.id]=n,n}}}function oC(){let e={};return{get:function(t){if(e[t.id]!==void 0)return e[t.id];let n;switch(t.type){case`SunLight`:case`DirectionalLight`:n={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new q};break;case`SpotLight`:n={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new q};break;case`PointLight`:n={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new q,shadowCameraNear:1,shadowCameraFar:1e3};break}return e[t.id]=n,n}}}var sC=0;function cC(e,t){return(t.castShadow?2:0)-(e.castShadow?2:0)+ +!!t.map-!!e.map}function lC(e){let t=new aC,n=oC(),r={version:0,hash:{sunLength:-1,directionalLength:-1,pointLength:-1,spotLength:-1,rectAreaLength:-1,hemiLength:-1,numSunShadows:-1,numDirectionalShadows:-1,numPointShadows:-1,numSpotShadows:-1,numSpotMaps:-1,numLightProbes:-1},ambient:[0,0,0],probe:[],sun:[],sunShadow:[],sunShadowMap:[],sunShadowMatrix:[],sunShadowCascade:[],directional:[],directionalShadow:[],directionalShadowMap:[],directionalShadowMatrix:[],spot:[],spotLightMap:[],spotShadow:[],spotShadowMap:[],spotLightMatrix:[],rectArea:[],rectAreaLTC1:null,rectAreaLTC2:null,point:[],pointShadow:[],pointShadowMap:[],pointShadowMatrix:[],hemi:[],numSpotLightShadowsWithMaps:0,numLightProbes:0};for(let e=0;e<9;e++)r.probe.push(new J);let i=new J,a=new Mm,o=new Mm;function s(i){let a=0,o=0,s=0;for(let e=0;e<9;e++)r.probe[e].set(0,0,0);let c=0,l=0,u=0,d=0,f=0,p=0,m=0,h=0,g=0,_=0,v=0,y=0,b=0,x=0;i.sort(cC);for(let e=0,S=i.length;e0&&(e.has(`OES_texture_float_linear`)===!0?(r.rectAreaLTC1=X.LTC_FLOAT_1,r.rectAreaLTC2=X.LTC_FLOAT_2):(r.rectAreaLTC1=X.LTC_HALF_1,r.rectAreaLTC2=X.LTC_HALF_2)),r.ambient[0]=a,r.ambient[1]=o,r.ambient[2]=s;let S=r.hash;(S.sunLength!==c||S.directionalLength!==d||S.pointLength!==f||S.spotLength!==p||S.rectAreaLength!==m||S.hemiLength!==h||S.numSunShadows!==l||S.numDirectionalShadows!==g||S.numPointShadows!==_||S.numSpotShadows!==v||S.numSpotMaps!==y||S.numLightProbes!==x)&&(r.sun.length=c,r.directional.length=d,r.spot.length=p,r.rectArea.length=m,r.point.length=f,r.hemi.length=h,r.sunShadow.length=l,r.sunShadowMap.length=l,r.sunShadowMatrix.length=u,r.sunShadowCascade.length=u,r.directionalShadow.length=g,r.directionalShadowMap.length=g,r.directionalShadowMatrix.length=g,r.pointShadow.length=_,r.pointShadowMap.length=_,r.pointShadowMatrix.length=_,r.spotShadow.length=v,r.spotShadowMap.length=v,r.spotLightMatrix.length=v+y-b,r.spotLightMap.length=y,r.numSpotLightShadowsWithMaps=b,r.numLightProbes=x,S.sunLength=c,S.directionalLength=d,S.pointLength=f,S.spotLength=p,S.rectAreaLength=m,S.hemiLength=h,S.numSunShadows=l,S.numDirectionalShadows=g,S.numPointShadows=_,S.numSpotShadows=v,S.numSpotMaps=y,S.numLightProbes=x,r.version=sC++)}function c(e,t){let n=0,s=0,c=0,l=0,u=0,d=0,f=t.matrixWorldInverse;for(let t=0,p=e.length;t=i.length?(a=new uC(e),i.push(a)):a=i[r],a}function r(){t=new WeakMap}return{get:n,dispose:r}}var fC=`void main() { gl_Position = vec4( position, 1.0 ); -}`,Ry=`uniform sampler2D shadow_pass; +}`,pC=`uniform sampler2D shadow_pass; uniform vec2 resolution; uniform float radius; void main() { @@ -4203,12 +4203,12 @@ void main() { squared_mean = squared_mean / samples; float std_dev = sqrt( max( 0.0, squared_mean - mean * mean ) ); gl_FragColor = vec4( mean, std_dev, 0.0, 1.0 ); -}`,zy=[new Z(1,0,0),new Z(-1,0,0),new Z(0,1,0),new Z(0,-1,0),new Z(0,0,1),new Z(0,0,-1)],By=[new Z(0,-1,0),new Z(0,-1,0),new Z(0,0,1),new Z(0,0,-1),new Z(0,-1,0),new Z(0,-1,0)],Vy=new ad,Hy=new Z,Uy=new Z;function Wy(e,t,n){let r=new Bp,i=new X,a=new X,o=new ed,s=new lh,c=new uh,l={},u=n.maxTextureSize,d={0:1,1:0,2:2},f=new ah({defines:{VSM_SAMPLES:8},uniforms:{shadow_pass:{value:null},resolution:{value:new X},radius:{value:4}},vertexShader:Ly,fragmentShader:Ry}),p=f.clone();p.defines.HORIZONTAL_PASS=1;let m=new Lf;m.setAttribute(`position`,new Sf(new Float32Array([-1,-1,.5,3,-1,.5,-1,3,.5]),3));let h=new up(m,f),g=this;this.enabled=!1,this.autoUpdate=!0,this.needsUpdate=!1,this.type=1;let _=this.type;this.render=function(t,n,s){if(g.enabled===!1||g.autoUpdate===!1&&g.needsUpdate===!1||t.length===0)return;this.type===2&&(nu(`WebGLShadowMap: PCFSoftShadowMap has been removed. Using PCFShadowMap instead.`),this.type=1);let c=e.getRenderTarget(),l=e.getActiveCubeFace(),d=e.getActiveMipmapLevel(),f=e.state;f.setBlending(0),f.buffers.depth.getReversed()===!0?f.buffers.color.setClear(0,0,0,0):f.buffers.color.setClear(1,1,1,1),f.buffers.depth.setTest(!0),f.setScissorTest(!1);let p=_!==this.type;p&&n.traverse(function(e){e.material&&(Array.isArray(e.material)?e.material.forEach(e=>e.needsUpdate=!0):e.material.needsUpdate=!0)});for(let c=0,l=t.length;cu||i.y>u)&&(i.x>u&&(a.x=Math.floor(u/m.x),i.x=a.x*m.x,d.mapSize.x=a.x),i.y>u&&(a.y=Math.floor(u/m.y),i.y=a.y*m.y,d.mapSize.y=a.y));let h=e.state.buffers.depth.getReversed();if(d.camera._reversedDepth=h,d.map===null||p===!0){if(d.map!==null&&(d.map.depthTexture!==null&&(d.map.depthTexture.dispose(),d.map.depthTexture=null),d.map.dispose()),this.type===3){if(l.isPointLight){nu(`WebGLShadowMap: VSM shadow maps are not supported for PointLights. Use PCF or BasicShadowMap instead.`);continue}d.map=new nd(i.x,i.y,{format:qc,type:Pc,minFilter:wc,magFilter:wc,generateMipmaps:!1}),d.map.texture.name=l.name+`.shadowMap`,d.map.depthTexture=new um(i.x,i.y,Nc),d.map.depthTexture.name=l.name+`.shadowMapDepth`,d.map.depthTexture.format=Uc,d.map.depthTexture.compareFunction=null,d.map.depthTexture.minFilter=xc,d.map.depthTexture.magFilter=xc}else l.isPointLight?(d.map=new x_(i.x),d.map.depthTexture=new dm(i.x,Mc)):(d.map=new nd(i.x,i.y),d.map.depthTexture=new um(i.x,i.y,Mc)),d.map.depthTexture.name=l.name+`.shadowMap`,d.map.depthTexture.format=Uc,this.type===1?(d.map.depthTexture.compareFunction=h?518:515,d.map.depthTexture.minFilter=wc,d.map.depthTexture.magFilter=wc):(d.map.depthTexture.compareFunction=null,d.map.depthTexture.minFilter=xc,d.map.depthTexture.magFilter=xc);d.camera.updateProjectionMatrix()}d.map.isWebGLCubeRenderTarget!==!0&&(d.map.width!==i.x||d.map.height!==i.y)&&d.map.setSize(i.x,i.y);let g=d.map.isWebGLCubeRenderTarget?6:d.getViewportCount();l.isPointLight!==!0&&d.updateMatrices(l,s);for(let t=0;t0||n.map&&n.alphaTest>0||n.alphaToCoverage===!0){let e=a.uuid,t=n.uuid,r=l[e];r===void 0&&(r={},l[e]=r);let i=r[t];i===void 0&&(i=a.clone(),r[t]=i,n.addEventListener(`dispose`,x)),a=i}if(a.visible=n.visible,a.wireframe=n.wireframe,i===3?a.side=n.shadowSide===null?n.side:n.shadowSide:a.side=n.shadowSide===null?d[n.side]:n.shadowSide,a.alphaMap=n.alphaMap,a.alphaTest=n.alphaToCoverage===!0?.5:n.alphaTest,a.map=n.map,a.clipShadows=n.clipShadows,a.clippingPlanes=n.clippingPlanes,a.clipIntersection=n.clipIntersection,a.displacementMap=n.displacementMap,a.displacementScale=n.displacementScale,a.displacementBias=n.displacementBias,a.wireframeLinewidth=n.wireframeLinewidth,a.linewidth=n.linewidth,r.isPointLight===!0&&a.isMeshDistanceMaterial===!0){let t=e.properties.get(a);t.light=r}return a}function b(n,i,a,o,s){if(n.visible===!1)return;if(n.layers.test(i.layers)&&(n.isMesh||n.isLine||n.isPoints)&&(n.castShadow||n.receiveShadow&&s===3)&&(!n.frustumCulled||n.intersectsFrustum(r))){n.modelViewMatrix.multiplyMatrices(a.matrixWorldInverse,n.matrixWorld);let r=t.update(n),c=n.material;if(Array.isArray(c)){let t=r.groups;for(let l=0,u=t.length;l=2):(M=parseFloat(/^WebGL (\d)/.exec(ne)[1]),j=M>=1);let N=null,P={},F=e.getParameter(e.SCISSOR_BOX),re=e.getParameter(e.VIEWPORT),ie=new ed().fromArray(F),ae=new ed().fromArray(re);function I(t,n,r,i){let a=new Uint8Array(4),o=e.createTexture();e.bindTexture(t,o),e.texParameteri(t,e.TEXTURE_MIN_FILTER,e.NEAREST),e.texParameteri(t,e.TEXTURE_MAG_FILTER,e.NEAREST);for(let o=0;o`u`?!1:/OculusBrowser/g.test(navigator.userAgent),l=new X,u=new WeakMap,d=new Set,f,p=new WeakMap,m=!1;try{m=typeof OffscreenCanvas<`u`&&new OffscreenCanvas(1,1).getContext(`2d`)!==null}catch{}function h(e,t){return m?new OffscreenCanvas(e,t):Zl(`canvas`)}function g(e,t,n){let r=1,i=Se(e);if((i.width>n||i.height>n)&&(r=n/Math.max(i.width,i.height)),r<1)if(typeof HTMLImageElement<`u`&&e instanceof HTMLImageElement||typeof HTMLCanvasElement<`u`&&e instanceof HTMLCanvasElement||typeof ImageBitmap<`u`&&e instanceof ImageBitmap||typeof VideoFrame<`u`&&e instanceof VideoFrame){let n=Math.floor(r*i.width),a=Math.floor(r*i.height);f===void 0&&(f=h(n,a));let o=t?h(n,a):f;return o.width=n,o.height=a,o.getContext(`2d`).drawImage(e,0,0,n,a),nu(`WebGLRenderer: Texture has been resized from (`+i.width+`x`+i.height+`) to (`+n+`x`+a+`).`),o}else return`data`in e&&nu(`WebGLRenderer: Image in DataTexture is too big (`+i.width+`x`+i.height+`).`),e;return e}function _(e){return e.generateMipmaps}function v(t){e.generateMipmap(t)}function y(t){return t.isWebGLCubeRenderTarget?e.TEXTURE_CUBE_MAP:t.isWebGL3DRenderTarget?e.TEXTURE_3D:t.isWebGLArrayRenderTarget||t.isCompressedArrayTexture?e.TEXTURE_2D_ARRAY:e.TEXTURE_2D}function b(n,r,i,a,o,s=!1){if(n!==null){if(e[n]!==void 0)return e[n];nu(`WebGLRenderer: Attempt to use non-existing WebGL internal format '`+n+`'`)}let c;a&&(c=t.get(`EXT_texture_norm16`),c||nu(`WebGLRenderer: Unable to use normalized textures without EXT_texture_norm16 extension`));let l=r;if(r===e.RED&&(i===e.FLOAT&&(l=e.R32F),i===e.HALF_FLOAT&&(l=e.R16F),i===e.UNSIGNED_BYTE&&(l=e.R8),i===e.UNSIGNED_SHORT&&c&&(l=c.R16_EXT),i===e.SHORT&&c&&(l=c.R16_SNORM_EXT)),r===e.RED_INTEGER&&(i===e.UNSIGNED_BYTE&&(l=e.R8UI),i===e.UNSIGNED_SHORT&&(l=e.R16UI),i===e.UNSIGNED_INT&&(l=e.R32UI),i===e.BYTE&&(l=e.R8I),i===e.SHORT&&(l=e.R16I),i===e.INT&&(l=e.R32I)),r===e.RG&&(i===e.FLOAT&&(l=e.RG32F),i===e.HALF_FLOAT&&(l=e.RG16F),i===e.UNSIGNED_BYTE&&(l=e.RG8),i===e.UNSIGNED_SHORT&&c&&(l=c.RG16_EXT),i===e.SHORT&&c&&(l=c.RG16_SNORM_EXT)),r===e.RG_INTEGER&&(i===e.UNSIGNED_BYTE&&(l=e.RG8UI),i===e.UNSIGNED_SHORT&&(l=e.RG16UI),i===e.UNSIGNED_INT&&(l=e.RG32UI),i===e.BYTE&&(l=e.RG8I),i===e.SHORT&&(l=e.RG16I),i===e.INT&&(l=e.RG32I)),r===e.RGB_INTEGER&&(i===e.UNSIGNED_BYTE&&(l=e.RGB8UI),i===e.UNSIGNED_SHORT&&(l=e.RGB16UI),i===e.UNSIGNED_INT&&(l=e.RGB32UI),i===e.BYTE&&(l=e.RGB8I),i===e.SHORT&&(l=e.RGB16I),i===e.INT&&(l=e.RGB32I)),r===e.RGBA_INTEGER&&(i===e.UNSIGNED_BYTE&&(l=e.RGBA8UI),i===e.UNSIGNED_SHORT&&(l=e.RGBA16UI),i===e.UNSIGNED_INT&&(l=e.RGBA32UI),i===e.BYTE&&(l=e.RGBA8I),i===e.SHORT&&(l=e.RGBA16I),i===e.INT&&(l=e.RGBA32I)),r===e.RGB&&(i===e.UNSIGNED_SHORT&&c&&(l=c.RGB16_EXT),i===e.SHORT&&c&&(l=c.RGB16_SNORM_EXT),i===e.UNSIGNED_INT_5_9_9_9_REV&&(l=e.RGB9_E5),i===e.UNSIGNED_INT_10F_11F_11F_REV&&(l=e.R11F_G11F_B10F)),r===e.RGBA){let t=s?Ul:Uu.getTransfer(o);i===e.FLOAT&&(l=e.RGBA32F),i===e.HALF_FLOAT&&(l=e.RGBA16F),i===e.UNSIGNED_BYTE&&(l=t===`srgb`?e.SRGB8_ALPHA8:e.RGBA8),i===e.UNSIGNED_SHORT&&c&&(l=c.RGBA16_EXT),i===e.SHORT&&c&&(l=c.RGBA16_SNORM_EXT),i===e.UNSIGNED_SHORT_4_4_4_4&&(l=e.RGBA4),i===e.UNSIGNED_SHORT_5_5_5_1&&(l=e.RGB5_A1)}return(l===e.R16F||l===e.R32F||l===e.RG16F||l===e.RG32F||l===e.RGBA16F||l===e.RGBA32F)&&t.get(`EXT_color_buffer_float`),l}function x(t,n){let r;return t?n===null||n===1014||n===1020?r=e.DEPTH24_STENCIL8:n===1015?r=e.DEPTH32F_STENCIL8:n===1012&&(r=e.DEPTH24_STENCIL8,nu(`DepthTexture: 16 bit depth attachment is not supported with stencil. Using 24-bit attachment.`)):n===null||n===1014||n===1020?r=e.DEPTH_COMPONENT24:n===1015?r=e.DEPTH_COMPONENT32F:n===1012&&(r=e.DEPTH_COMPONENT16),r}function S(e,t){return _(e)===!0||e.isFramebufferTexture&&e.minFilter!==1003&&e.minFilter!==1006?Math.log2(Math.max(t.width,t.height))+1:e.mipmaps!==void 0&&e.mipmaps.length>0?e.mipmaps.length:e.isCompressedTexture&&Array.isArray(e.image)?t.mipmaps.length:1}function C(e){let t=e.target;t.removeEventListener(`dispose`,C),T(t),t.isVideoTexture&&u.delete(t),t.isHTMLTexture&&d.delete(t)}function w(e){let t=e.target;t.removeEventListener(`dispose`,w),D(t)}function T(e){let t=r.get(e);if(t.__webglInit===void 0)return;let n=e.source,i=p.get(n);if(i){let r=i[t.__cacheKey];r.usedTimes--,r.usedTimes===0&&E(e),Object.keys(i).length===0&&p.delete(n)}r.remove(e)}function E(t){let n=r.get(t);e.deleteTexture(n.__webglTexture);let i=t.source,a=p.get(i);delete a[n.__cacheKey],o.memory.textures--}function D(t){let n=r.get(t);if(t.depthTexture&&(t.depthTexture.dispose(),r.remove(t.depthTexture)),t.isWebGLCubeRenderTarget)for(let t=0;t<6;t++){if(Array.isArray(n.__webglFramebuffer[t]))for(let r=0;r=i.maxTextures&&nu(`WebGLTextures: Trying to use `+(e+1)+` texture units while this GPU supports only `+i.maxTextures),O+=1,e}function j(e){let t=[];return t.push(e.wrapS),t.push(e.wrapT),t.push(e.wrapR||0),t.push(e.magFilter),t.push(e.minFilter),t.push(e.anisotropy),t.push(e.internalFormat),t.push(e.format),t.push(e.type),t.push(e.generateMipmaps),t.push(e.premultiplyAlpha),t.push(e.flipY),t.push(e.unpackAlignment),t.push(e.colorSpace),t.join()}function M(t,i){let a=r.get(t);if(t.isVideoTexture&&be(t),t.isRenderTargetTexture===!1&&t.isExternalTexture!==!0&&t.version>0&&a.__version!==t.version){let e=t.image;if(e===null)nu(`WebGLRenderer: Texture marked for update but no image data found.`);else if(e.complete===!1)nu(`WebGLRenderer: Texture marked for update but image is incomplete`);else{se(a,t,i);return}}else t.isExternalTexture&&(a.__webglTexture=t.sourceTexture?t.sourceTexture:null);n.bindTexture(e.TEXTURE_2D,a.__webglTexture,e.TEXTURE0+i)}function ne(t,i){let a=r.get(t);if(t.isRenderTargetTexture===!1&&t.version>0&&a.__version!==t.version){se(a,t,i);return}else t.isExternalTexture&&(a.__webglTexture=t.sourceTexture?t.sourceTexture:null);n.bindTexture(e.TEXTURE_2D_ARRAY,a.__webglTexture,e.TEXTURE0+i)}function N(t,i){let a=r.get(t);if(t.isRenderTargetTexture===!1&&t.version>0&&a.__version!==t.version){se(a,t,i);return}n.bindTexture(e.TEXTURE_3D,a.__webglTexture,e.TEXTURE0+i)}function P(t,i){let a=r.get(t);if(t.isCubeDepthTexture!==!0&&t.version>0&&a.__version!==t.version){ce(a,t,i);return}n.bindTexture(e.TEXTURE_CUBE_MAP,a.__webglTexture,e.TEXTURE0+i)}let F={[vc]:e.REPEAT,[yc]:e.CLAMP_TO_EDGE,[bc]:e.MIRRORED_REPEAT},re={[xc]:e.NEAREST,[Sc]:e.NEAREST_MIPMAP_NEAREST,[Cc]:e.NEAREST_MIPMAP_LINEAR,[wc]:e.LINEAR,[Tc]:e.LINEAR_MIPMAP_NEAREST,[Ec]:e.LINEAR_MIPMAP_LINEAR},ie={512:e.NEVER,519:e.ALWAYS,513:e.LESS,515:e.LEQUAL,514:e.EQUAL,518:e.GEQUAL,516:e.GREATER,517:e.NOTEQUAL};function ae(n,a){if(a.type===1015&&t.has(`OES_texture_float_linear`)===!1&&(a.magFilter===1006||a.magFilter===1007||a.magFilter===1005||a.magFilter===1008||a.minFilter===1006||a.minFilter===1007||a.minFilter===1005||a.minFilter===1008)&&nu(`WebGLRenderer: Unable to use linear filtering with floating point textures. OES_texture_float_linear not supported on this device.`),e.texParameteri(n,e.TEXTURE_WRAP_S,F[a.wrapS]),e.texParameteri(n,e.TEXTURE_WRAP_T,F[a.wrapT]),(n===e.TEXTURE_3D||n===e.TEXTURE_2D_ARRAY)&&e.texParameteri(n,e.TEXTURE_WRAP_R,F[a.wrapR]),e.texParameteri(n,e.TEXTURE_MAG_FILTER,re[a.magFilter]),e.texParameteri(n,e.TEXTURE_MIN_FILTER,re[a.minFilter]),a.compareFunction&&(e.texParameteri(n,e.TEXTURE_COMPARE_MODE,e.COMPARE_REF_TO_TEXTURE),e.texParameteri(n,e.TEXTURE_COMPARE_FUNC,ie[a.compareFunction])),t.has(`EXT_texture_filter_anisotropic`)===!0){if(a.magFilter===1003||a.minFilter!==1005&&a.minFilter!==1008||a.type===1015&&t.has(`OES_texture_float_linear`)===!1)return;if(a.anisotropy>1||r.get(a).__currentAnisotropy){let o=t.get(`EXT_texture_filter_anisotropic`);e.texParameterf(n,o.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(a.anisotropy,i.getMaxAnisotropy())),r.get(a).__currentAnisotropy=a.anisotropy}}}function I(t,n){let r=!1;t.__webglInit===void 0&&(t.__webglInit=!0,n.addEventListener(`dispose`,C));let i=n.source,a=p.get(i);a===void 0&&(a={},p.set(i,a));let s=j(n);if(s!==t.__cacheKey){a[s]===void 0&&(a[s]={texture:e.createTexture(),usedTimes:0},o.memory.textures++,r=!0),a[s].usedTimes++;let i=a[t.__cacheKey];i!==void 0&&(a[t.__cacheKey].usedTimes--,i.usedTimes===0&&E(n)),t.__cacheKey=s,t.__webglTexture=a[s].texture}return r}function L(e,t,n){return Math.floor(Math.floor(e/n)/t)}function oe(t,r,i,a){let o=t.updateRanges;if(o.length===0)n.texSubImage2D(e.TEXTURE_2D,0,0,0,r.width,r.height,i,a,r.data);else{o.sort((e,t)=>e.start-t.start);let s=0;for(let e=1;e0){C&&w&&n.texStorage2D(e.TEXTURE_2D,E,m,y[0].width,y[0].height);for(let t=0,i=y.length;t0){let t=Bg(h.width,h.height,o.format,o.type);for(let a of o.layerUpdates){let o=h.data.subarray(a*t/h.data.BYTES_PER_ELEMENT,(a+1)*t/h.data.BYTES_PER_ELEMENT);n.compressedTexSubImage3D(e.TEXTURE_2D_ARRAY,i,0,0,a,h.width,h.height,1,r,o)}}else n.compressedTexSubImage3D(e.TEXTURE_2D_ARRAY,i,0,0,0,h.width,h.height,t.depth,r,h.data)}else n.compressedTexImage3D(e.TEXTURE_2D_ARRAY,i,m,h.width,h.height,t.depth,0,h.data,0,0);else nu(`WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()`);else C?T&&n.texSubImage3D(e.TEXTURE_2D_ARRAY,i,0,0,0,h.width,h.height,t.depth,r,p,h.data):n.texImage3D(e.TEXTURE_2D_ARRAY,i,m,h.width,h.height,t.depth,0,r,p,h.data);o.layerUpdates.size>0&&o.clearLayerUpdates()}else{C&&w&&n.texStorage2D(e.TEXTURE_2D,E,m,y[0].width,y[0].height);for(let t=0,i=y.length;t0){let i=Bg(t.width,t.height,o.format,o.type);for(let a of o.layerUpdates){let o=t.data.subarray(a*i/t.data.BYTES_PER_ELEMENT,(a+1)*i/t.data.BYTES_PER_ELEMENT);n.texSubImage3D(e.TEXTURE_2D_ARRAY,0,0,0,a,t.width,t.height,1,r,p,o)}o.clearLayerUpdates()}else n.texSubImage3D(e.TEXTURE_2D_ARRAY,0,0,0,0,t.width,t.height,t.depth,r,p,t.data)}else n.texImage3D(e.TEXTURE_2D_ARRAY,0,m,t.width,t.height,t.depth,0,r,p,t.data);else if(o.isData3DTexture)C?(w&&n.texStorage3D(e.TEXTURE_3D,E,m,t.width,t.height,t.depth),T&&n.texSubImage3D(e.TEXTURE_3D,0,0,0,0,t.width,t.height,t.depth,r,p,t.data)):n.texImage3D(e.TEXTURE_3D,0,m,t.width,t.height,t.depth,0,r,p,t.data);else if(o.isFramebufferTexture){if(w)if(C)n.texStorage2D(e.TEXTURE_2D,E,m,t.width,t.height);else{let i=t.width,a=t.height;for(let t=0;t>=1,a>>=1}}else if(o.isHTMLTexture){if(`texElementImage2D`in e){let n=e.canvas;if(n.hasAttribute(`layoutsubtree`)||n.setAttribute(`layoutsubtree`,`true`),t.parentNode!==n){n.appendChild(t),d.add(o),n.onpaint=e=>{let t=e.changedElements;for(let e of d)t.includes(e.image)&&(e.needsUpdate=!0)},n.requestPaint();return}if(e.texElementImage2D.length===3)e.texElementImage2D(e.TEXTURE_2D,e.RGBA8,t);else{let n=e.RGBA,r=e.RGBA,i=e.UNSIGNED_BYTE;e.texElementImage2D(e.TEXTURE_2D,0,n,r,i,t)}e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE)}}else if(y.length>0){if(C&&w){let t=Se(y[0]);n.texStorage2D(e.TEXTURE_2D,E,m,t.width,t.height)}for(let t=0,i=y.length;t0&&D++;let t=Se(m[0]);n.texStorage2D(e.TEXTURE_CUBE_MAP,D,C,t.width,t.height)}for(let t=0;t<6;t++)if(p){w?E&&n.texSubImage2D(e.TEXTURE_CUBE_MAP_POSITIVE_X+t,0,0,0,m[t].width,m[t].height,y,x,m[t].data):n.texImage2D(e.TEXTURE_CUBE_MAP_POSITIVE_X+t,0,C,m[t].width,m[t].height,0,y,x,m[t].data);for(let r=0;r>u),r=Math.max(1,i.height>>u);l===e.TEXTURE_3D||l===e.TEXTURE_2D_ARRAY?n.texImage3D(l,u,p,t,r,i.depth,0,d,f,null):n.texImage2D(l,u,p,t,r,0,d,f,null)}n.bindFramebuffer(e.FRAMEBUFFER,t),R(i)?s.framebufferTexture2DMultisampleEXT(e.FRAMEBUFFER,c,l,h.__webglTexture,0,ye(i)):(l===e.TEXTURE_2D||l>=e.TEXTURE_CUBE_MAP_POSITIVE_X&&l<=e.TEXTURE_CUBE_MAP_NEGATIVE_Z)&&e.framebufferTexture2D(e.FRAMEBUFFER,c,l,h.__webglTexture,u),n.bindFramebuffer(e.FRAMEBUFFER,null)}function ue(t,n,r){if(e.bindRenderbuffer(e.RENDERBUFFER,t),n.depthBuffer){let i=n.depthTexture,a=i&&i.isDepthTexture?i.type:null,o=x(n.stencilBuffer,a),c=n.stencilBuffer?e.DEPTH_STENCIL_ATTACHMENT:e.DEPTH_ATTACHMENT;R(n)?s.renderbufferStorageMultisampleEXT(e.RENDERBUFFER,ye(n),o,n.width,n.height):r?e.renderbufferStorageMultisample(e.RENDERBUFFER,ye(n),o,n.width,n.height):e.renderbufferStorage(e.RENDERBUFFER,o,n.width,n.height),e.framebufferRenderbuffer(e.FRAMEBUFFER,c,e.RENDERBUFFER,t)}else{let t=n.textures;for(let i=0;i{delete i.__boundDepthTexture,delete i.__depthDisposeCallback,e.removeEventListener(`dispose`,t)};e.addEventListener(`dispose`,t),i.__depthDisposeCallback=t}i.__boundDepthTexture=e}if(t.depthTexture&&!i.__autoAllocateDepthBuffer)if(a)for(let e=0;e<6;e++)de(i.__webglFramebuffer[e],t,e);else{let e=t.texture.mipmaps;e&&e.length>0?de(i.__webglFramebuffer[0],t,0):de(i.__webglFramebuffer,t,0)}else if(a){i.__webglDepthbuffer=[];for(let r=0;r<6;r++)if(n.bindFramebuffer(e.FRAMEBUFFER,i.__webglFramebuffer[r]),i.__webglDepthbuffer[r]===void 0)i.__webglDepthbuffer[r]=e.createRenderbuffer(),ue(i.__webglDepthbuffer[r],t,!1);else{let n=t.stencilBuffer?e.DEPTH_STENCIL_ATTACHMENT:e.DEPTH_ATTACHMENT,a=i.__webglDepthbuffer[r];e.bindRenderbuffer(e.RENDERBUFFER,a),e.framebufferRenderbuffer(e.FRAMEBUFFER,n,e.RENDERBUFFER,a)}}else{let r=t.texture.mipmaps;if(r&&r.length>0?n.bindFramebuffer(e.FRAMEBUFFER,i.__webglFramebuffer[0]):n.bindFramebuffer(e.FRAMEBUFFER,i.__webglFramebuffer),i.__webglDepthbuffer===void 0)i.__webglDepthbuffer=e.createRenderbuffer(),ue(i.__webglDepthbuffer,t,!1);else{let n=t.stencilBuffer?e.DEPTH_STENCIL_ATTACHMENT:e.DEPTH_ATTACHMENT,r=i.__webglDepthbuffer;e.bindRenderbuffer(e.RENDERBUFFER,r),e.framebufferRenderbuffer(e.FRAMEBUFFER,n,e.RENDERBUFFER,r)}}n.bindFramebuffer(e.FRAMEBUFFER,null)}function pe(t,n,i){let a=r.get(t);n!==void 0&&le(a.__webglFramebuffer,t,t.texture,e.COLOR_ATTACHMENT0,e.TEXTURE_2D,0),i!==void 0&&fe(t)}function me(t){let i=t.texture,s=r.get(t),c=r.get(i);t.addEventListener(`dispose`,w);let l=t.textures,u=t.isWebGLCubeRenderTarget===!0,d=l.length>1;if(d||(c.__webglTexture===void 0&&(c.__webglTexture=e.createTexture()),c.__version=i.version,o.memory.textures++),u){s.__webglFramebuffer=[];for(let t=0;t<6;t++)if(i.mipmaps&&i.mipmaps.length>0){s.__webglFramebuffer[t]=[];for(let n=0;n0){s.__webglFramebuffer=[];for(let t=0;t0&&R(t)===!1){s.__webglMultisampledFramebuffer=e.createFramebuffer(),s.__webglColorRenderbuffer=[],n.bindFramebuffer(e.FRAMEBUFFER,s.__webglMultisampledFramebuffer);for(let n=0;n0)for(let r=0;r0)for(let n=0;n0){if(R(t)===!1){let i=t.textures,a=t.width,o=t.height,s=e.COLOR_BUFFER_BIT,l=t.stencilBuffer?e.DEPTH_STENCIL_ATTACHMENT:e.DEPTH_ATTACHMENT,u=r.get(t),d=i.length>1;if(d)for(let t=0;t0?n.bindFramebuffer(e.DRAW_FRAMEBUFFER,u.__webglFramebuffer[0]):n.bindFramebuffer(e.DRAW_FRAMEBUFFER,u.__webglFramebuffer);for(let n=0;n0&&t.has(`WEBGL_multisampled_render_to_texture`)===!0&&n.__useRenderToTexture!==!1}function be(e){let t=o.render.frame;u.get(e)!==t&&(u.set(e,t),e.update())}function xe(e,t){let n=e.colorSpace,r=e.format,i=e.type;return e.isCompressedTexture===!0||e.isVideoTexture===!0||n!==`srgb-linear`&&n!==``&&(Uu.getTransfer(n)===`srgb`?(r!==1023||i!==1009)&&nu(`WebGLTextures: sRGB encoded textures have to use RGBAFormat and UnsignedByteType.`):ru(`WebGLTextures: Unsupported texture color space:`,n)),t}function Se(e){return typeof HTMLImageElement<`u`&&e instanceof HTMLImageElement?(l.width=e.naturalWidth||e.width,l.height=e.naturalHeight||e.height):typeof VideoFrame<`u`&&e instanceof VideoFrame?(l.width=e.displayWidth,l.height=e.displayHeight):(l.width=e.width,l.height=e.height),l}this.allocateTextureUnit=te,this.resetTextureUnits=ee,this.getTextureUnits=k,this.setTextureUnits=A,this.setTexture2D=M,this.setTexture2DArray=ne,this.setTexture3D=N,this.setTextureCube=P,this.rebindTextures=pe,this.setupRenderTarget=me,this.updateRenderTargetMipmap=he,this.updateMultisampleRenderTarget=ve,this.setupDepthRenderbuffer=fe,this.setupFrameBufferTexture=le,this.useMultisampledRTT=R,this.isReversedDepthBuffer=function(){return n.buffers.depth.getReversed()}}function qy(e,t){function n(n,r=``){let i,a=Uu.getTransfer(r);if(n===1009)return e.UNSIGNED_BYTE;if(n===1017)return e.UNSIGNED_SHORT_4_4_4_4;if(n===1018)return e.UNSIGNED_SHORT_5_5_5_1;if(n===35902)return e.UNSIGNED_INT_5_9_9_9_REV;if(n===35899)return e.UNSIGNED_INT_10F_11F_11F_REV;if(n===1010)return e.BYTE;if(n===1011)return e.SHORT;if(n===1012)return e.UNSIGNED_SHORT;if(n===1013)return e.INT;if(n===1014)return e.UNSIGNED_INT;if(n===1015)return e.FLOAT;if(n===1016)return e.HALF_FLOAT;if(n===1021)return e.ALPHA;if(n===1022)return e.RGB;if(n===1023)return e.RGBA;if(n===1026)return e.DEPTH_COMPONENT;if(n===1027)return e.DEPTH_STENCIL;if(n===1028)return e.RED;if(n===1029)return e.RED_INTEGER;if(n===1030)return e.RG;if(n===1031)return e.RG_INTEGER;if(n===1033)return e.RGBA_INTEGER;if(n===33776||n===33777||n===33778||n===33779)if(a===`srgb`)if(i=t.get(`WEBGL_compressed_texture_s3tc_srgb`),i!==null){if(n===33776)return i.COMPRESSED_SRGB_S3TC_DXT1_EXT;if(n===33777)return i.COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;if(n===33778)return i.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;if(n===33779)return i.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT}else return null;else if(i=t.get(`WEBGL_compressed_texture_s3tc`),i!==null){if(n===33776)return i.COMPRESSED_RGB_S3TC_DXT1_EXT;if(n===33777)return i.COMPRESSED_RGBA_S3TC_DXT1_EXT;if(n===33778)return i.COMPRESSED_RGBA_S3TC_DXT3_EXT;if(n===33779)return i.COMPRESSED_RGBA_S3TC_DXT5_EXT}else return null;if(n===35840||n===35841||n===35842||n===35843)if(i=t.get(`WEBGL_compressed_texture_pvrtc`),i!==null){if(n===35840)return i.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;if(n===35841)return i.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;if(n===35842)return i.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;if(n===35843)return i.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG}else return null;if(n===36196||n===37492||n===37496||n===37488||n===37489||n===37490||n===37491)if(i=t.get(`WEBGL_compressed_texture_etc`),i!==null){if(n===36196||n===37492)return a===`srgb`?i.COMPRESSED_SRGB8_ETC2:i.COMPRESSED_RGB8_ETC2;if(n===37496)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ETC2_EAC:i.COMPRESSED_RGBA8_ETC2_EAC;if(n===37488)return i.COMPRESSED_R11_EAC;if(n===37489)return i.COMPRESSED_SIGNED_R11_EAC;if(n===37490)return i.COMPRESSED_RG11_EAC;if(n===37491)return i.COMPRESSED_SIGNED_RG11_EAC}else return null;if(n===37808||n===37809||n===37810||n===37811||n===37812||n===37813||n===37814||n===37815||n===37816||n===37817||n===37818||n===37819||n===37820||n===37821)if(i=t.get(`WEBGL_compressed_texture_astc`),i!==null){if(n===37808)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR:i.COMPRESSED_RGBA_ASTC_4x4_KHR;if(n===37809)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR:i.COMPRESSED_RGBA_ASTC_5x4_KHR;if(n===37810)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR:i.COMPRESSED_RGBA_ASTC_5x5_KHR;if(n===37811)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR:i.COMPRESSED_RGBA_ASTC_6x5_KHR;if(n===37812)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR:i.COMPRESSED_RGBA_ASTC_6x6_KHR;if(n===37813)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR:i.COMPRESSED_RGBA_ASTC_8x5_KHR;if(n===37814)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR:i.COMPRESSED_RGBA_ASTC_8x6_KHR;if(n===37815)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR:i.COMPRESSED_RGBA_ASTC_8x8_KHR;if(n===37816)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR:i.COMPRESSED_RGBA_ASTC_10x5_KHR;if(n===37817)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR:i.COMPRESSED_RGBA_ASTC_10x6_KHR;if(n===37818)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR:i.COMPRESSED_RGBA_ASTC_10x8_KHR;if(n===37819)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR:i.COMPRESSED_RGBA_ASTC_10x10_KHR;if(n===37820)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR:i.COMPRESSED_RGBA_ASTC_12x10_KHR;if(n===37821)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR:i.COMPRESSED_RGBA_ASTC_12x12_KHR}else return null;if(n===36492||n===36494||n===36495)if(i=t.get(`EXT_texture_compression_bptc`),i!==null){if(n===36492)return a===`srgb`?i.COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT:i.COMPRESSED_RGBA_BPTC_UNORM_EXT;if(n===36494)return i.COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT;if(n===36495)return i.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT}else return null;if(n===36283||n===36284||n===36285||n===36286)if(i=t.get(`EXT_texture_compression_rgtc`),i!==null){if(n===36283)return i.COMPRESSED_RED_RGTC1_EXT;if(n===36284)return i.COMPRESSED_SIGNED_RED_RGTC1_EXT;if(n===36285)return i.COMPRESSED_RED_GREEN_RGTC2_EXT;if(n===36286)return i.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT}else return null;return n===1020?e.UNSIGNED_INT_24_8:e[n]===void 0?null:e[n]}return{convert:n}}var Jy=` +}`,mC=[new J(1,0,0),new J(-1,0,0),new J(0,1,0),new J(0,-1,0),new J(0,0,1),new J(0,0,-1)],hC=[new J(0,-1,0),new J(0,-1,0),new J(0,0,1),new J(0,0,-1),new J(0,-1,0),new J(0,-1,0)],gC=new Mm,_C=new J,vC=new J;function yC(e,t,n){let r=new f_,i=new q,a=new q,o=new Dm,s=new Iv,c=new Lv,l={},u=n.maxTextureSize,d={0:1,1:0,2:2},f=new Mv({defines:{VSM_SAMPLES:8},uniforms:{shadow_pass:{value:null},resolution:{value:new q},radius:{value:4}},vertexShader:fC,fragmentShader:pC}),p=f.clone();p.defines.HORIZONTAL_PASS=1;let m=new lg;m.setAttribute(`position`,new Jh(new Float32Array([-1,-1,.5,3,-1,.5,-1,3,.5]),3));let h=new Ig(m,f),g=this;this.enabled=!1,this.autoUpdate=!0,this.needsUpdate=!1,this.type=1;let _=this.type;this.render=function(t,n,s){if(g.enabled===!1||g.autoUpdate===!1&&g.needsUpdate===!1||t.length===0)return;this.type===2&&(Ap(`WebGLShadowMap: PCFSoftShadowMap has been removed. Using PCFShadowMap instead.`),this.type=1);let c=e.getRenderTarget(),l=e.getActiveCubeFace(),d=e.getActiveMipmapLevel(),f=e.state;f.setBlending(0),f.buffers.depth.getReversed()===!0?f.buffers.color.setClear(0,0,0,0):f.buffers.color.setClear(1,1,1,1),f.buffers.depth.setTest(!0),f.setScissorTest(!1);let p=_!==this.type;p&&n.traverse(function(e){e.material&&(Array.isArray(e.material)?e.material.forEach(e=>e.needsUpdate=!0):e.material.needsUpdate=!0)});for(let c=0,l=t.length;cu||i.y>u)&&(i.x>u&&(a.x=Math.floor(u/m.x),i.x=a.x*m.x,d.mapSize.x=a.x),i.y>u&&(a.y=Math.floor(u/m.y),i.y=a.y*m.y,d.mapSize.y=a.y));let h=e.state.buffers.depth.getReversed();if(d.camera._reversedDepth=h,d.map===null||p===!0){if(d.map!==null&&(d.map.depthTexture!==null&&(d.map.depthTexture.dispose(),d.map.depthTexture=null),d.map.dispose()),this.type===3){if(l.isPointLight){Ap(`WebGLShadowMap: VSM shadow maps are not supported for PointLights. Use PCF or BasicShadowMap instead.`);continue}d.map=new km(i.x,i.y,{format:xf,type:cf,minFilter:Zd,magFilter:Zd,generateMipmaps:!1}),d.map.texture.name=l.name+`.shadowMap`,d.map.depthTexture=new I_(i.x,i.y,sf),d.map.depthTexture.name=l.name+`.shadowMapDepth`,d.map.depthTexture.format=_f,d.map.depthTexture.compareFunction=null,d.map.depthTexture.minFilter=Jd,d.map.depthTexture.magFilter=Jd}else l.isPointLight?(d.map=new Xb(i.x),d.map.depthTexture=new L_(i.x,of)):(d.map=new km(i.x,i.y),d.map.depthTexture=new I_(i.x,i.y,of)),d.map.depthTexture.name=l.name+`.shadowMap`,d.map.depthTexture.format=_f,this.type===1?(d.map.depthTexture.compareFunction=h?518:515,d.map.depthTexture.minFilter=Zd,d.map.depthTexture.magFilter=Zd):(d.map.depthTexture.compareFunction=null,d.map.depthTexture.minFilter=Jd,d.map.depthTexture.magFilter=Jd);d.camera.updateProjectionMatrix()}d.map.isWebGLCubeRenderTarget!==!0&&(d.map.width!==i.x||d.map.height!==i.y)&&d.map.setSize(i.x,i.y);let g=d.map.isWebGLCubeRenderTarget?6:d.getViewportCount();l.isPointLight!==!0&&d.updateMatrices(l,s);for(let t=0;t0||n.map&&n.alphaTest>0||n.alphaToCoverage===!0){let e=a.uuid,t=n.uuid,r=l[e];r===void 0&&(r={},l[e]=r);let i=r[t];i===void 0&&(i=a.clone(),r[t]=i,n.addEventListener(`dispose`,x)),a=i}if(a.visible=n.visible,a.wireframe=n.wireframe,i===3?a.side=n.shadowSide===null?n.side:n.shadowSide:a.side=n.shadowSide===null?d[n.side]:n.shadowSide,a.alphaMap=n.alphaMap,a.alphaTest=n.alphaToCoverage===!0?.5:n.alphaTest,a.map=n.map,a.clipShadows=n.clipShadows,a.clippingPlanes=n.clippingPlanes,a.clipIntersection=n.clipIntersection,a.displacementMap=n.displacementMap,a.displacementScale=n.displacementScale,a.displacementBias=n.displacementBias,a.wireframeLinewidth=n.wireframeLinewidth,a.linewidth=n.linewidth,r.isPointLight===!0&&a.isMeshDistanceMaterial===!0){let t=e.properties.get(a);t.light=r}return a}function b(n,i,a,o,s){if(n.visible===!1)return;if(n.layers.test(i.layers)&&(n.isMesh||n.isLine||n.isPoints)&&(n.castShadow||n.receiveShadow&&s===3)&&(!n.frustumCulled||n.intersectsFrustum(r))){n.modelViewMatrix.multiplyMatrices(a.matrixWorldInverse,n.matrixWorld);let r=t.update(n),c=n.material;if(Array.isArray(c)){let t=r.groups;for(let l=0,u=t.length;l=2):(re=parseFloat(/^WebGL (\d)/.exec(ie)[1]),A=re>=1);let ae=null,oe={},j=e.getParameter(e.SCISSOR_BOX),se=e.getParameter(e.VIEWPORT),ce=new Dm().fromArray(j),le=new Dm().fromArray(se);function ue(t,n,r,i){let a=new Uint8Array(4),o=e.createTexture();e.bindTexture(t,o),e.texParameteri(t,e.TEXTURE_MIN_FILTER,e.NEAREST),e.texParameteri(t,e.TEXTURE_MAG_FILTER,e.NEAREST);for(let o=0;o`u`?!1:/OculusBrowser/g.test(navigator.userAgent),l=new q,u=new WeakMap,d=new Set,f,p=new WeakMap,m=!1;try{m=typeof OffscreenCanvas<`u`&&new OffscreenCanvas(1,1).getContext(`2d`)!==null}catch{}function h(e,t){return m?new OffscreenCanvas(e,t):Tp(`canvas`)}function g(e,t,n){let r=1,i=we(e);if((i.width>n||i.height>n)&&(r=n/Math.max(i.width,i.height)),r<1)if(typeof HTMLImageElement<`u`&&e instanceof HTMLImageElement||typeof HTMLCanvasElement<`u`&&e instanceof HTMLCanvasElement||typeof ImageBitmap<`u`&&e instanceof ImageBitmap||typeof VideoFrame<`u`&&e instanceof VideoFrame){let n=Math.floor(r*i.width),a=Math.floor(r*i.height);f===void 0&&(f=h(n,a));let o=t?h(n,a):f;return o.width=n,o.height=a,o.getContext(`2d`).drawImage(e,0,0,n,a),Ap(`WebGLRenderer: Texture has been resized from (`+i.width+`x`+i.height+`) to (`+n+`x`+a+`).`),o}else return`data`in e&&Ap(`WebGLRenderer: Image in DataTexture is too big (`+i.width+`x`+i.height+`).`),e;return e}function _(e){return e.generateMipmaps}function v(t){e.generateMipmap(t)}function y(t){return t.isWebGLCubeRenderTarget?e.TEXTURE_CUBE_MAP:t.isWebGL3DRenderTarget?e.TEXTURE_3D:t.isWebGLArrayRenderTarget||t.isCompressedArrayTexture?e.TEXTURE_2D_ARRAY:e.TEXTURE_2D}function b(n,r,i,a,o,s=!1){if(n!==null){if(e[n]!==void 0)return e[n];Ap(`WebGLRenderer: Attempt to use non-existing WebGL internal format '`+n+`'`)}let c;a&&(c=t.get(`EXT_texture_norm16`),c||Ap(`WebGLRenderer: Unable to use normalized textures without EXT_texture_norm16 extension`));let l=r;if(r===e.RED&&(i===e.FLOAT&&(l=e.R32F),i===e.HALF_FLOAT&&(l=e.R16F),i===e.UNSIGNED_BYTE&&(l=e.R8),i===e.UNSIGNED_SHORT&&c&&(l=c.R16_EXT),i===e.SHORT&&c&&(l=c.R16_SNORM_EXT)),r===e.RED_INTEGER&&(i===e.UNSIGNED_BYTE&&(l=e.R8UI),i===e.UNSIGNED_SHORT&&(l=e.R16UI),i===e.UNSIGNED_INT&&(l=e.R32UI),i===e.BYTE&&(l=e.R8I),i===e.SHORT&&(l=e.R16I),i===e.INT&&(l=e.R32I)),r===e.RG&&(i===e.FLOAT&&(l=e.RG32F),i===e.HALF_FLOAT&&(l=e.RG16F),i===e.UNSIGNED_BYTE&&(l=e.RG8),i===e.UNSIGNED_SHORT&&c&&(l=c.RG16_EXT),i===e.SHORT&&c&&(l=c.RG16_SNORM_EXT)),r===e.RG_INTEGER&&(i===e.UNSIGNED_BYTE&&(l=e.RG8UI),i===e.UNSIGNED_SHORT&&(l=e.RG16UI),i===e.UNSIGNED_INT&&(l=e.RG32UI),i===e.BYTE&&(l=e.RG8I),i===e.SHORT&&(l=e.RG16I),i===e.INT&&(l=e.RG32I)),r===e.RGB_INTEGER&&(i===e.UNSIGNED_BYTE&&(l=e.RGB8UI),i===e.UNSIGNED_SHORT&&(l=e.RGB16UI),i===e.UNSIGNED_INT&&(l=e.RGB32UI),i===e.BYTE&&(l=e.RGB8I),i===e.SHORT&&(l=e.RGB16I),i===e.INT&&(l=e.RGB32I)),r===e.RGBA_INTEGER&&(i===e.UNSIGNED_BYTE&&(l=e.RGBA8UI),i===e.UNSIGNED_SHORT&&(l=e.RGBA16UI),i===e.UNSIGNED_INT&&(l=e.RGBA32UI),i===e.BYTE&&(l=e.RGBA8I),i===e.SHORT&&(l=e.RGBA16I),i===e.INT&&(l=e.RGBA32I)),r===e.RGB&&(i===e.UNSIGNED_SHORT&&c&&(l=c.RGB16_EXT),i===e.SHORT&&c&&(l=c.RGB16_SNORM_EXT),i===e.UNSIGNED_INT_5_9_9_9_REV&&(l=e.RGB9_E5),i===e.UNSIGNED_INT_10F_11F_11F_REV&&(l=e.R11F_G11F_B10F)),r===e.RGBA){let t=s?_p:gm.getTransfer(o);i===e.FLOAT&&(l=e.RGBA32F),i===e.HALF_FLOAT&&(l=e.RGBA16F),i===e.UNSIGNED_BYTE&&(l=t===`srgb`?e.SRGB8_ALPHA8:e.RGBA8),i===e.UNSIGNED_SHORT&&c&&(l=c.RGBA16_EXT),i===e.SHORT&&c&&(l=c.RGBA16_SNORM_EXT),i===e.UNSIGNED_SHORT_4_4_4_4&&(l=e.RGBA4),i===e.UNSIGNED_SHORT_5_5_5_1&&(l=e.RGB5_A1)}return(l===e.R16F||l===e.R32F||l===e.RG16F||l===e.RG32F||l===e.RGBA16F||l===e.RGBA32F)&&t.get(`EXT_color_buffer_float`),l}function x(t,n){let r;return t?n===null||n===1014||n===1020?r=e.DEPTH24_STENCIL8:n===1015?r=e.DEPTH32F_STENCIL8:n===1012&&(r=e.DEPTH24_STENCIL8,Ap(`DepthTexture: 16 bit depth attachment is not supported with stencil. Using 24-bit attachment.`)):n===null||n===1014||n===1020?r=e.DEPTH_COMPONENT24:n===1015?r=e.DEPTH_COMPONENT32F:n===1012&&(r=e.DEPTH_COMPONENT16),r}function S(e,t){return _(e)===!0||e.isFramebufferTexture&&e.minFilter!==1003&&e.minFilter!==1006?Math.log2(Math.max(t.width,t.height))+1:e.mipmaps!==void 0&&e.mipmaps.length>0?e.mipmaps.length:e.isCompressedTexture&&Array.isArray(e.image)?t.mipmaps.length:1}function C(e){let t=e.target;t.removeEventListener(`dispose`,C),T(t),t.isVideoTexture&&u.delete(t),t.isHTMLTexture&&d.delete(t)}function w(e){let t=e.target;t.removeEventListener(`dispose`,w),D(t)}function T(e){let t=r.get(e);if(t.__webglInit===void 0)return;let n=e.source,i=p.get(n);if(i){let r=i[t.__cacheKey];r.usedTimes--,r.usedTimes===0&&E(e),Object.keys(i).length===0&&p.delete(n)}r.remove(e)}function E(t){let n=r.get(t);e.deleteTexture(n.__webglTexture);let i=t.source,a=p.get(i);delete a[n.__cacheKey],o.memory.textures--}function D(t){let n=r.get(t);if(t.depthTexture&&(t.depthTexture.dispose(),r.remove(t.depthTexture)),t.isWebGLCubeRenderTarget)for(let t=0;t<6;t++){if(Array.isArray(n.__webglFramebuffer[t]))for(let r=0;r=i.maxTextures&&Ap(`WebGLTextures: Trying to use `+(e+1)+` texture units while this GPU supports only `+i.maxTextures),O+=1,e}function A(e){let t=[];return t.push(e.wrapS),t.push(e.wrapT),t.push(e.wrapR||0),t.push(e.magFilter),t.push(e.minFilter),t.push(e.anisotropy),t.push(e.internalFormat),t.push(e.format),t.push(e.type),t.push(e.generateMipmaps),t.push(e.premultiplyAlpha),t.push(e.flipY),t.push(e.unpackAlignment),t.push(e.colorSpace),t.join()}function re(t,i){let a=r.get(t);if(t.isVideoTexture&&Se(t),t.isRenderTargetTexture===!1&&t.isExternalTexture!==!0&&t.version>0&&a.__version!==t.version){let e=t.image;if(e===null)Ap(`WebGLRenderer: Texture marked for update but no image data found.`);else if(e.complete===!1)Ap(`WebGLRenderer: Texture marked for update but image is incomplete`);else{de(a,t,i);return}}else t.isExternalTexture&&(a.__webglTexture=t.sourceTexture?t.sourceTexture:null);n.bindTexture(e.TEXTURE_2D,a.__webglTexture,e.TEXTURE0+i)}function ie(t,i){let a=r.get(t);if(t.isRenderTargetTexture===!1&&t.version>0&&a.__version!==t.version){de(a,t,i);return}else t.isExternalTexture&&(a.__webglTexture=t.sourceTexture?t.sourceTexture:null);n.bindTexture(e.TEXTURE_2D_ARRAY,a.__webglTexture,e.TEXTURE0+i)}function ae(t,i){let a=r.get(t);if(t.isRenderTargetTexture===!1&&t.version>0&&a.__version!==t.version){de(a,t,i);return}n.bindTexture(e.TEXTURE_3D,a.__webglTexture,e.TEXTURE0+i)}function oe(t,i){let a=r.get(t);if(t.isCubeDepthTexture!==!0&&t.version>0&&a.__version!==t.version){fe(a,t,i);return}n.bindTexture(e.TEXTURE_CUBE_MAP,a.__webglTexture,e.TEXTURE0+i)}let j={[Gd]:e.REPEAT,[Kd]:e.CLAMP_TO_EDGE,[qd]:e.MIRRORED_REPEAT},se={[Jd]:e.NEAREST,[Yd]:e.NEAREST_MIPMAP_NEAREST,[Xd]:e.NEAREST_MIPMAP_LINEAR,[Zd]:e.LINEAR,[Qd]:e.LINEAR_MIPMAP_NEAREST,[$d]:e.LINEAR_MIPMAP_LINEAR},ce={512:e.NEVER,519:e.ALWAYS,513:e.LESS,515:e.LEQUAL,514:e.EQUAL,518:e.GEQUAL,516:e.GREATER,517:e.NOTEQUAL};function le(n,a){if(a.type===1015&&t.has(`OES_texture_float_linear`)===!1&&(a.magFilter===1006||a.magFilter===1007||a.magFilter===1005||a.magFilter===1008||a.minFilter===1006||a.minFilter===1007||a.minFilter===1005||a.minFilter===1008)&&Ap(`WebGLRenderer: Unable to use linear filtering with floating point textures. OES_texture_float_linear not supported on this device.`),e.texParameteri(n,e.TEXTURE_WRAP_S,j[a.wrapS]),e.texParameteri(n,e.TEXTURE_WRAP_T,j[a.wrapT]),(n===e.TEXTURE_3D||n===e.TEXTURE_2D_ARRAY)&&e.texParameteri(n,e.TEXTURE_WRAP_R,j[a.wrapR]),e.texParameteri(n,e.TEXTURE_MAG_FILTER,se[a.magFilter]),e.texParameteri(n,e.TEXTURE_MIN_FILTER,se[a.minFilter]),a.compareFunction&&(e.texParameteri(n,e.TEXTURE_COMPARE_MODE,e.COMPARE_REF_TO_TEXTURE),e.texParameteri(n,e.TEXTURE_COMPARE_FUNC,ce[a.compareFunction])),t.has(`EXT_texture_filter_anisotropic`)===!0){if(a.magFilter===1003||a.minFilter!==1005&&a.minFilter!==1008||a.type===1015&&t.has(`OES_texture_float_linear`)===!1)return;if(a.anisotropy>1||r.get(a).__currentAnisotropy){let o=t.get(`EXT_texture_filter_anisotropic`);e.texParameterf(n,o.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(a.anisotropy,i.getMaxAnisotropy())),r.get(a).__currentAnisotropy=a.anisotropy}}}function ue(t,n){let r=!1;t.__webglInit===void 0&&(t.__webglInit=!0,n.addEventListener(`dispose`,C));let i=n.source,a=p.get(i);a===void 0&&(a={},p.set(i,a));let s=A(n);if(s!==t.__cacheKey){a[s]===void 0&&(a[s]={texture:e.createTexture(),usedTimes:0},o.memory.textures++,r=!0),a[s].usedTimes++;let i=a[t.__cacheKey];i!==void 0&&(a[t.__cacheKey].usedTimes--,i.usedTimes===0&&E(n)),t.__cacheKey=s,t.__webglTexture=a[s].texture}return r}function M(e,t,n){return Math.floor(Math.floor(e/n)/t)}function N(t,r,i,a){let o=t.updateRanges;if(o.length===0)n.texSubImage2D(e.TEXTURE_2D,0,0,0,r.width,r.height,i,a,r.data);else{o.sort((e,t)=>e.start-t.start);let s=0;for(let e=1;e0){C&&w&&n.texStorage2D(e.TEXTURE_2D,E,m,y[0].width,y[0].height);for(let t=0,i=y.length;t0){let t=hb(h.width,h.height,o.format,o.type);for(let a of o.layerUpdates){let o=h.data.subarray(a*t/h.data.BYTES_PER_ELEMENT,(a+1)*t/h.data.BYTES_PER_ELEMENT);n.compressedTexSubImage3D(e.TEXTURE_2D_ARRAY,i,0,0,a,h.width,h.height,1,r,o)}}else n.compressedTexSubImage3D(e.TEXTURE_2D_ARRAY,i,0,0,0,h.width,h.height,t.depth,r,h.data)}else n.compressedTexImage3D(e.TEXTURE_2D_ARRAY,i,m,h.width,h.height,t.depth,0,h.data,0,0);else Ap(`WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()`);else C?T&&n.texSubImage3D(e.TEXTURE_2D_ARRAY,i,0,0,0,h.width,h.height,t.depth,r,p,h.data):n.texImage3D(e.TEXTURE_2D_ARRAY,i,m,h.width,h.height,t.depth,0,r,p,h.data);o.layerUpdates.size>0&&o.clearLayerUpdates()}else{C&&w&&n.texStorage2D(e.TEXTURE_2D,E,m,y[0].width,y[0].height);for(let t=0,i=y.length;t0){let i=hb(t.width,t.height,o.format,o.type);for(let a of o.layerUpdates){let o=t.data.subarray(a*i/t.data.BYTES_PER_ELEMENT,(a+1)*i/t.data.BYTES_PER_ELEMENT);n.texSubImage3D(e.TEXTURE_2D_ARRAY,0,0,0,a,t.width,t.height,1,r,p,o)}o.clearLayerUpdates()}else n.texSubImage3D(e.TEXTURE_2D_ARRAY,0,0,0,0,t.width,t.height,t.depth,r,p,t.data)}else n.texImage3D(e.TEXTURE_2D_ARRAY,0,m,t.width,t.height,t.depth,0,r,p,t.data);else if(o.isData3DTexture)C?(w&&n.texStorage3D(e.TEXTURE_3D,E,m,t.width,t.height,t.depth),T&&n.texSubImage3D(e.TEXTURE_3D,0,0,0,0,t.width,t.height,t.depth,r,p,t.data)):n.texImage3D(e.TEXTURE_3D,0,m,t.width,t.height,t.depth,0,r,p,t.data);else if(o.isFramebufferTexture){if(w)if(C)n.texStorage2D(e.TEXTURE_2D,E,m,t.width,t.height);else{let i=t.width,a=t.height;for(let t=0;t>=1,a>>=1}}else if(o.isHTMLTexture){if(`texElementImage2D`in e){let n=e.canvas;if(n.hasAttribute(`layoutsubtree`)||n.setAttribute(`layoutsubtree`,`true`),t.parentNode!==n){n.appendChild(t),d.add(o),n.onpaint=e=>{let t=e.changedElements;for(let e of d)t.includes(e.image)&&(e.needsUpdate=!0)},n.requestPaint();return}if(e.texElementImage2D.length===3)e.texElementImage2D(e.TEXTURE_2D,e.RGBA8,t);else{let n=e.RGBA,r=e.RGBA,i=e.UNSIGNED_BYTE;e.texElementImage2D(e.TEXTURE_2D,0,n,r,i,t)}e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE)}}else if(y.length>0){if(C&&w){let t=we(y[0]);n.texStorage2D(e.TEXTURE_2D,E,m,t.width,t.height)}for(let t=0,i=y.length;t0&&D++;let t=we(m[0]);n.texStorage2D(e.TEXTURE_CUBE_MAP,D,C,t.width,t.height)}for(let t=0;t<6;t++)if(p){w?E&&n.texSubImage2D(e.TEXTURE_CUBE_MAP_POSITIVE_X+t,0,0,0,m[t].width,m[t].height,y,x,m[t].data):n.texImage2D(e.TEXTURE_CUBE_MAP_POSITIVE_X+t,0,C,m[t].width,m[t].height,0,y,x,m[t].data);for(let r=0;r>u),r=Math.max(1,i.height>>u);l===e.TEXTURE_3D||l===e.TEXTURE_2D_ARRAY?n.texImage3D(l,u,p,t,r,i.depth,0,d,f,null):n.texImage2D(l,u,p,t,r,0,d,f,null)}n.bindFramebuffer(e.FRAMEBUFFER,t),I(i)?s.framebufferTexture2DMultisampleEXT(e.FRAMEBUFFER,c,l,h.__webglTexture,0,xe(i)):(l===e.TEXTURE_2D||l>=e.TEXTURE_CUBE_MAP_POSITIVE_X&&l<=e.TEXTURE_CUBE_MAP_NEGATIVE_Z)&&e.framebufferTexture2D(e.FRAMEBUFFER,c,l,h.__webglTexture,u),n.bindFramebuffer(e.FRAMEBUFFER,null)}function me(t,n,r){if(e.bindRenderbuffer(e.RENDERBUFFER,t),n.depthBuffer){let i=n.depthTexture,a=i&&i.isDepthTexture?i.type:null,o=x(n.stencilBuffer,a),c=n.stencilBuffer?e.DEPTH_STENCIL_ATTACHMENT:e.DEPTH_ATTACHMENT;I(n)?s.renderbufferStorageMultisampleEXT(e.RENDERBUFFER,xe(n),o,n.width,n.height):r?e.renderbufferStorageMultisample(e.RENDERBUFFER,xe(n),o,n.width,n.height):e.renderbufferStorage(e.RENDERBUFFER,o,n.width,n.height),e.framebufferRenderbuffer(e.FRAMEBUFFER,c,e.RENDERBUFFER,t)}else{let t=n.textures;for(let i=0;i{delete i.__boundDepthTexture,delete i.__depthDisposeCallback,e.removeEventListener(`dispose`,t)};e.addEventListener(`dispose`,t),i.__depthDisposeCallback=t}i.__boundDepthTexture=e}if(t.depthTexture&&!i.__autoAllocateDepthBuffer)if(a)for(let e=0;e<6;e++)P(i.__webglFramebuffer[e],t,e);else{let e=t.texture.mipmaps;e&&e.length>0?P(i.__webglFramebuffer[0],t,0):P(i.__webglFramebuffer,t,0)}else if(a){i.__webglDepthbuffer=[];for(let r=0;r<6;r++)if(n.bindFramebuffer(e.FRAMEBUFFER,i.__webglFramebuffer[r]),i.__webglDepthbuffer[r]===void 0)i.__webglDepthbuffer[r]=e.createRenderbuffer(),me(i.__webglDepthbuffer[r],t,!1);else{let n=t.stencilBuffer?e.DEPTH_STENCIL_ATTACHMENT:e.DEPTH_ATTACHMENT,a=i.__webglDepthbuffer[r];e.bindRenderbuffer(e.RENDERBUFFER,a),e.framebufferRenderbuffer(e.FRAMEBUFFER,n,e.RENDERBUFFER,a)}}else{let r=t.texture.mipmaps;if(r&&r.length>0?n.bindFramebuffer(e.FRAMEBUFFER,i.__webglFramebuffer[0]):n.bindFramebuffer(e.FRAMEBUFFER,i.__webglFramebuffer),i.__webglDepthbuffer===void 0)i.__webglDepthbuffer=e.createRenderbuffer(),me(i.__webglDepthbuffer,t,!1);else{let n=t.stencilBuffer?e.DEPTH_STENCIL_ATTACHMENT:e.DEPTH_ATTACHMENT,r=i.__webglDepthbuffer;e.bindRenderbuffer(e.RENDERBUFFER,r),e.framebufferRenderbuffer(e.FRAMEBUFFER,n,e.RENDERBUFFER,r)}}n.bindFramebuffer(e.FRAMEBUFFER,null)}function he(t,n,i){let a=r.get(t);n!==void 0&&pe(a.__webglFramebuffer,t,t.texture,e.COLOR_ATTACHMENT0,e.TEXTURE_2D,0),i!==void 0&&F(t)}function ge(t){let i=t.texture,s=r.get(t),c=r.get(i);t.addEventListener(`dispose`,w);let l=t.textures,u=t.isWebGLCubeRenderTarget===!0,d=l.length>1;if(d||(c.__webglTexture===void 0&&(c.__webglTexture=e.createTexture()),c.__version=i.version,o.memory.textures++),u){s.__webglFramebuffer=[];for(let t=0;t<6;t++)if(i.mipmaps&&i.mipmaps.length>0){s.__webglFramebuffer[t]=[];for(let n=0;n0){s.__webglFramebuffer=[];for(let t=0;t0&&I(t)===!1){s.__webglMultisampledFramebuffer=e.createFramebuffer(),s.__webglColorRenderbuffer=[],n.bindFramebuffer(e.FRAMEBUFFER,s.__webglMultisampledFramebuffer);for(let n=0;n0)for(let r=0;r0)for(let n=0;n0){if(I(t)===!1){let i=t.textures,a=t.width,o=t.height,s=e.COLOR_BUFFER_BIT,l=t.stencilBuffer?e.DEPTH_STENCIL_ATTACHMENT:e.DEPTH_ATTACHMENT,u=r.get(t),d=i.length>1;if(d)for(let t=0;t0?n.bindFramebuffer(e.DRAW_FRAMEBUFFER,u.__webglFramebuffer[0]):n.bindFramebuffer(e.DRAW_FRAMEBUFFER,u.__webglFramebuffer);for(let n=0;n0&&t.has(`WEBGL_multisampled_render_to_texture`)===!0&&n.__useRenderToTexture!==!1}function Se(e){let t=o.render.frame;u.get(e)!==t&&(u.set(e,t),e.update())}function Ce(e,t){let n=e.colorSpace,r=e.format,i=e.type;return e.isCompressedTexture===!0||e.isVideoTexture===!0||n!==`srgb-linear`&&n!==``&&(gm.getTransfer(n)===`srgb`?(r!==1023||i!==1009)&&Ap(`WebGLTextures: sRGB encoded textures have to use RGBAFormat and UnsignedByteType.`):jp(`WebGLTextures: Unsupported texture color space:`,n)),t}function we(e){return typeof HTMLImageElement<`u`&&e instanceof HTMLImageElement?(l.width=e.naturalWidth||e.width,l.height=e.naturalHeight||e.height):typeof VideoFrame<`u`&&e instanceof VideoFrame?(l.width=e.displayWidth,l.height=e.displayHeight):(l.width=e.width,l.height=e.height),l}this.allocateTextureUnit=ne,this.resetTextureUnits=ee,this.getTextureUnits=te,this.setTextureUnits=k,this.setTexture2D=re,this.setTexture2DArray=ie,this.setTexture3D=ae,this.setTextureCube=oe,this.rebindTextures=he,this.setupRenderTarget=ge,this.updateRenderTargetMipmap=_e,this.updateMultisampleRenderTarget=be,this.setupDepthRenderbuffer=F,this.setupFrameBufferTexture=pe,this.useMultisampledRTT=I,this.isReversedDepthBuffer=function(){return n.buffers.depth.getReversed()}}function SC(e,t){function n(n,r=``){let i,a=gm.getTransfer(r);if(n===1009)return e.UNSIGNED_BYTE;if(n===1017)return e.UNSIGNED_SHORT_4_4_4_4;if(n===1018)return e.UNSIGNED_SHORT_5_5_5_1;if(n===35902)return e.UNSIGNED_INT_5_9_9_9_REV;if(n===35899)return e.UNSIGNED_INT_10F_11F_11F_REV;if(n===1010)return e.BYTE;if(n===1011)return e.SHORT;if(n===1012)return e.UNSIGNED_SHORT;if(n===1013)return e.INT;if(n===1014)return e.UNSIGNED_INT;if(n===1015)return e.FLOAT;if(n===1016)return e.HALF_FLOAT;if(n===1021)return e.ALPHA;if(n===1022)return e.RGB;if(n===1023)return e.RGBA;if(n===1026)return e.DEPTH_COMPONENT;if(n===1027)return e.DEPTH_STENCIL;if(n===1028)return e.RED;if(n===1029)return e.RED_INTEGER;if(n===1030)return e.RG;if(n===1031)return e.RG_INTEGER;if(n===1033)return e.RGBA_INTEGER;if(n===33776||n===33777||n===33778||n===33779)if(a===`srgb`)if(i=t.get(`WEBGL_compressed_texture_s3tc_srgb`),i!==null){if(n===33776)return i.COMPRESSED_SRGB_S3TC_DXT1_EXT;if(n===33777)return i.COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;if(n===33778)return i.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;if(n===33779)return i.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT}else return null;else if(i=t.get(`WEBGL_compressed_texture_s3tc`),i!==null){if(n===33776)return i.COMPRESSED_RGB_S3TC_DXT1_EXT;if(n===33777)return i.COMPRESSED_RGBA_S3TC_DXT1_EXT;if(n===33778)return i.COMPRESSED_RGBA_S3TC_DXT3_EXT;if(n===33779)return i.COMPRESSED_RGBA_S3TC_DXT5_EXT}else return null;if(n===35840||n===35841||n===35842||n===35843)if(i=t.get(`WEBGL_compressed_texture_pvrtc`),i!==null){if(n===35840)return i.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;if(n===35841)return i.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;if(n===35842)return i.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;if(n===35843)return i.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG}else return null;if(n===36196||n===37492||n===37496||n===37488||n===37489||n===37490||n===37491)if(i=t.get(`WEBGL_compressed_texture_etc`),i!==null){if(n===36196||n===37492)return a===`srgb`?i.COMPRESSED_SRGB8_ETC2:i.COMPRESSED_RGB8_ETC2;if(n===37496)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ETC2_EAC:i.COMPRESSED_RGBA8_ETC2_EAC;if(n===37488)return i.COMPRESSED_R11_EAC;if(n===37489)return i.COMPRESSED_SIGNED_R11_EAC;if(n===37490)return i.COMPRESSED_RG11_EAC;if(n===37491)return i.COMPRESSED_SIGNED_RG11_EAC}else return null;if(n===37808||n===37809||n===37810||n===37811||n===37812||n===37813||n===37814||n===37815||n===37816||n===37817||n===37818||n===37819||n===37820||n===37821)if(i=t.get(`WEBGL_compressed_texture_astc`),i!==null){if(n===37808)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR:i.COMPRESSED_RGBA_ASTC_4x4_KHR;if(n===37809)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR:i.COMPRESSED_RGBA_ASTC_5x4_KHR;if(n===37810)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR:i.COMPRESSED_RGBA_ASTC_5x5_KHR;if(n===37811)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR:i.COMPRESSED_RGBA_ASTC_6x5_KHR;if(n===37812)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR:i.COMPRESSED_RGBA_ASTC_6x6_KHR;if(n===37813)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR:i.COMPRESSED_RGBA_ASTC_8x5_KHR;if(n===37814)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR:i.COMPRESSED_RGBA_ASTC_8x6_KHR;if(n===37815)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR:i.COMPRESSED_RGBA_ASTC_8x8_KHR;if(n===37816)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR:i.COMPRESSED_RGBA_ASTC_10x5_KHR;if(n===37817)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR:i.COMPRESSED_RGBA_ASTC_10x6_KHR;if(n===37818)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR:i.COMPRESSED_RGBA_ASTC_10x8_KHR;if(n===37819)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR:i.COMPRESSED_RGBA_ASTC_10x10_KHR;if(n===37820)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR:i.COMPRESSED_RGBA_ASTC_12x10_KHR;if(n===37821)return a===`srgb`?i.COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR:i.COMPRESSED_RGBA_ASTC_12x12_KHR}else return null;if(n===36492||n===36494||n===36495)if(i=t.get(`EXT_texture_compression_bptc`),i!==null){if(n===36492)return a===`srgb`?i.COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT:i.COMPRESSED_RGBA_BPTC_UNORM_EXT;if(n===36494)return i.COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT;if(n===36495)return i.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT}else return null;if(n===36283||n===36284||n===36285||n===36286)if(i=t.get(`EXT_texture_compression_rgtc`),i!==null){if(n===36283)return i.COMPRESSED_RED_RGTC1_EXT;if(n===36284)return i.COMPRESSED_SIGNED_RED_RGTC1_EXT;if(n===36285)return i.COMPRESSED_RED_GREEN_RGTC2_EXT;if(n===36286)return i.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT}else return null;return n===1020?e.UNSIGNED_INT_24_8:e[n]===void 0?null:e[n]}return{convert:n}}var CC=` void main() { gl_Position = vec4( position, 1.0 ); -}`,Yy=` +}`,wC=` uniform sampler2DArray depthColor; uniform float depthWidth; uniform float depthHeight; @@ -4227,7 +4227,7 @@ void main() { } -}`,Xy=class{constructor(){this.texture=null,this.mesh=null,this.depthNear=0,this.depthFar=0}init(e,t){if(this.texture===null){let n=new fm(e.texture);(e.depthNear!==t.depthNear||e.depthFar!==t.depthFar)&&(this.depthNear=e.depthNear,this.depthFar=e.depthFar),this.texture=n}}getMesh(e){if(this.texture!==null&&this.mesh===null){let t=e.cameras[0].viewport,n=new ah({vertexShader:Jy,fragmentShader:Yy,uniforms:{depthColor:{value:this.texture},depthWidth:{value:t.z},depthHeight:{value:t.w}}});this.mesh=new up(new qm(20,20),n)}return this.mesh}reset(){this.texture=null,this.mesh=null}getDepthTexture(){return this.texture}},Zy=class extends su{constructor(e,t){super();let n=this,r=null,i=1,a=null,o=`local-floor`,s=1,c=null,l=null,u=null,d=null,f=null,p=null,m=typeof XRWebGLBinding<`u`,h=new Xy,g={},_=t.getContextAttributes(),v=null,y=null,b=[],x=[],S=new X,C=null,w=null,T=new ag;T.viewport=new ed;let E=new ag;E.viewport=new ed;let D=[T,E],O=new yg,ee=null,k=null;this.cameraAutoUpdate=!0,this.enabled=!1,this.isPresenting=!1,this.getController=function(e){let t=b[e];return t===void 0&&(t=new Fd,b[e]=t),t.getTargetRaySpace()},this.getControllerGrip=function(e){let t=b[e];return t===void 0&&(t=new Fd,b[e]=t),t.getGripSpace()},this.getHand=function(e){let t=b[e];return t===void 0&&(t=new Fd,b[e]=t),t.getHandSpace()};function A(e){let t=x.indexOf(e.inputSource);if(t===-1)return;let n=b[t];n!==void 0&&(n.update(e.inputSource,e.frame,c||a),n.dispatchEvent({type:e.type,data:e.inputSource}))}function te(){r.removeEventListener(`select`,A),r.removeEventListener(`selectstart`,A),r.removeEventListener(`selectend`,A),r.removeEventListener(`squeeze`,A),r.removeEventListener(`squeezestart`,A),r.removeEventListener(`squeezeend`,A),r.removeEventListener(`end`,te),r.removeEventListener(`inputsourceschange`,j);for(let e=0;e=0&&(x[r]=null,b[r].disconnect(n))}for(let t=0;t=x.length){x.push(n),r=e;break}else if(x[e]===null){x[e]=n,r=e;break}if(r===-1)break}let i=b[r];i&&i.connect(n)}}let M=new Z,ne=new Z;function N(e,t,n){M.setFromMatrixPosition(t.matrixWorld),ne.setFromMatrixPosition(n.matrixWorld);let r=M.distanceTo(ne),i=t.projectionMatrix.elements,a=n.projectionMatrix.elements,o=i[14]/(i[10]-1),s=i[14]/(i[10]+1),c=(i[9]+1)/i[5],l=(i[9]-1)/i[5],u=(i[8]-1)/i[0],d=(a[8]+1)/a[0],f=o*u,p=o*d,m=r/(-u+d),h=m*-u;if(t.matrixWorld.decompose(e.position,e.quaternion,e.scale),e.translateX(h),e.translateZ(m),e.matrixWorld.compose(e.position,e.quaternion,e.scale),e.matrixWorldInverse.copy(e.matrixWorld).invert(),i[10]===-1)e.projectionMatrix.copy(t.projectionMatrix),e.projectionMatrixInverse.copy(t.projectionMatrixInverse);else{let t=o+m,n=s+m,i=f-h,a=p+(r-h),u=c*s/n*t,d=l*s/n*t;e.projectionMatrix.makePerspective(i,a,u,d,t,n),e.projectionMatrixInverse.copy(e.projectionMatrix).invert()}}function P(e,t){t===null?e.matrixWorld.copy(e.matrix):e.matrixWorld.multiplyMatrices(t.matrixWorld,e.matrix),e.matrixWorldInverse.copy(e.matrixWorld).invert()}this.updateCamera=function(e){if(r===null)return;let t=e.near,n=e.far;h.texture!==null&&(h.depthNear>0&&(t=h.depthNear),h.depthFar>0&&(n=h.depthFar)),O.near=E.near=T.near=t,O.far=E.far=T.far=n,(ee!==O.near||k!==O.far)&&(r.updateRenderState({depthNear:O.near,depthFar:O.far}),ee=O.near,k=O.far),O.layers.mask=e.layers.mask|6,T.layers.mask=O.layers.mask&-5,E.layers.mask=O.layers.mask&-3;let i=e.parent,a=O.cameras;P(O,i);for(let e=0;e0&&(e.alphaTest.value=r.alphaTest);let i=t.get(r),a=i.envMap,o=i.envMapRotation;a&&(e.envMap.value=a,e.envMapRotation.value.setFromMatrix4(Qy.makeRotationFromEuler(o)).transpose(),a.isCubeTexture&&a.isRenderTargetTexture===!1&&e.envMapRotation.value.premultiply($y),e.reflectivity.value=r.reflectivity,e.ior.value=r.ior,e.refractionRatio.value=r.refractionRatio),r.lightMap&&(e.lightMap.value=r.lightMap,e.lightMapIntensity.value=r.lightMapIntensity,n(r.lightMap,e.lightMapTransform)),r.aoMap&&(e.aoMap.value=r.aoMap,e.aoMapIntensity.value=r.aoMapIntensity,n(r.aoMap,e.aoMapTransform))}function o(e,t){e.diffuse.value.copy(t.color),e.opacity.value=t.opacity,t.map&&(e.map.value=t.map,n(t.map,e.mapTransform))}function s(e,t){e.dashSize.value=t.dashSize,e.totalSize.value=t.dashSize+t.gapSize,e.scale.value=t.scale}function c(e,t,r,i){e.diffuse.value.copy(t.color),e.opacity.value=t.opacity,e.size.value=t.size*r,e.scale.value=i*.5,t.map&&(e.map.value=t.map,n(t.map,e.uvTransform)),t.alphaMap&&(e.alphaMap.value=t.alphaMap,n(t.alphaMap,e.alphaMapTransform)),t.alphaTest>0&&(e.alphaTest.value=t.alphaTest)}function l(e,t){e.diffuse.value.copy(t.color),e.opacity.value=t.opacity,e.rotation.value=t.rotation,t.map&&(e.map.value=t.map,n(t.map,e.mapTransform)),t.alphaMap&&(e.alphaMap.value=t.alphaMap,n(t.alphaMap,e.alphaMapTransform)),t.alphaTest>0&&(e.alphaTest.value=t.alphaTest)}function u(e,t){e.specular.value.copy(t.specular),e.shininess.value=Math.max(t.shininess,1e-4)}function d(e,t){t.gradientMap&&(e.gradientMap.value=t.gradientMap)}function f(e,t){e.metalness.value=t.metalness,t.metalnessMap&&(e.metalnessMap.value=t.metalnessMap,n(t.metalnessMap,e.metalnessMapTransform)),e.roughness.value=t.roughness,t.roughnessMap&&(e.roughnessMap.value=t.roughnessMap,n(t.roughnessMap,e.roughnessMapTransform)),t.envMap&&(e.envMapIntensity.value=t.envMapIntensity)}function p(e,t,r){e.ior.value=t.ior,t.sheen>0&&(e.sheenColor.value.copy(t.sheenColor).multiplyScalar(t.sheen),e.sheenRoughness.value=t.sheenRoughness,t.sheenColorMap&&(e.sheenColorMap.value=t.sheenColorMap,n(t.sheenColorMap,e.sheenColorMapTransform)),t.sheenRoughnessMap&&(e.sheenRoughnessMap.value=t.sheenRoughnessMap,n(t.sheenRoughnessMap,e.sheenRoughnessMapTransform))),t.clearcoat>0&&(e.clearcoat.value=t.clearcoat,e.clearcoatRoughness.value=t.clearcoatRoughness,t.clearcoatMap&&(e.clearcoatMap.value=t.clearcoatMap,n(t.clearcoatMap,e.clearcoatMapTransform)),t.clearcoatRoughnessMap&&(e.clearcoatRoughnessMap.value=t.clearcoatRoughnessMap,n(t.clearcoatRoughnessMap,e.clearcoatRoughnessMapTransform)),t.clearcoatNormalMap&&(e.clearcoatNormalMap.value=t.clearcoatNormalMap,n(t.clearcoatNormalMap,e.clearcoatNormalMapTransform),e.clearcoatNormalScale.value.copy(t.clearcoatNormalScale),t.side===1&&e.clearcoatNormalScale.value.negate())),t.dispersion>0&&(e.dispersion.value=t.dispersion),t.retroreflectivity>0&&(e.retroreflectivity.value=t.retroreflectivity),t.iridescence>0&&(e.iridescence.value=t.iridescence,e.iridescenceIOR.value=t.iridescenceIOR,e.iridescenceThicknessMinimum.value=t.iridescenceThicknessRange[0],e.iridescenceThicknessMaximum.value=t.iridescenceThicknessRange[1],t.iridescenceMap&&(e.iridescenceMap.value=t.iridescenceMap,n(t.iridescenceMap,e.iridescenceMapTransform)),t.iridescenceThicknessMap&&(e.iridescenceThicknessMap.value=t.iridescenceThicknessMap,n(t.iridescenceThicknessMap,e.iridescenceThicknessMapTransform))),t.transmission>0&&(e.transmission.value=t.transmission,e.transmissionSamplerMap.value=r.texture,e.transmissionSamplerSize.value.set(r.width,r.height),t.transmissionMap&&(e.transmissionMap.value=t.transmissionMap,n(t.transmissionMap,e.transmissionMapTransform)),e.thickness.value=t.thickness,t.thicknessMap&&(e.thicknessMap.value=t.thicknessMap,n(t.thicknessMap,e.thicknessMapTransform)),e.attenuationDistance.value=t.attenuationDistance,e.attenuationColor.value.copy(t.attenuationColor)),t.anisotropy>0&&(e.anisotropyVector.value.set(t.anisotropy*Math.cos(t.anisotropyRotation),t.anisotropy*Math.sin(t.anisotropyRotation)),t.anisotropyMap&&(e.anisotropyMap.value=t.anisotropyMap,n(t.anisotropyMap,e.anisotropyMapTransform))),e.specularIntensity.value=t.specularIntensity,e.specularColor.value.copy(t.specularColor),t.specularColorMap&&(e.specularColorMap.value=t.specularColorMap,n(t.specularColorMap,e.specularColorMapTransform)),t.specularIntensityMap&&(e.specularIntensityMap.value=t.specularIntensityMap,n(t.specularIntensityMap,e.specularIntensityMapTransform))}function m(e,t){t.matcap&&(e.matcap.value=t.matcap)}function h(e,n){let r=t.get(n).light;e.referencePosition.value.setFromMatrixPosition(r.matrixWorld),e.nearDistance.value=r.shadow.camera.near,e.farDistance.value=r.shadow.camera.far}return{refreshFogUniforms:r,refreshMaterialUniforms:i}}function tb(e,t,n,r){let i={},a={},o=[],s=e.getParameter(e.MAX_UNIFORM_BUFFER_BINDINGS);function c(e,t){let n=t.program;r.uniformBlockBinding(e,n)}function l(e,n){let o=i[e.id];o===void 0&&(g(e),o=u(e),i[e.id]=o,e.addEventListener(`dispose`,v));let s=n.program;r.updateUBOMapping(e,s);let c=t.render.frame;a[e.id]!==c&&(f(e),a[e.id]=c)}function u(t){let n=d();t.__bindingPointIndex=n;let r=e.createBuffer(),i=t.__size,a=t.usage;return e.bindBuffer(e.UNIFORM_BUFFER,r),e.bufferData(e.UNIFORM_BUFFER,i,a),e.bindBuffer(e.UNIFORM_BUFFER,null),e.bindBufferBase(e.UNIFORM_BUFFER,n,r),r}function d(){for(let e=0;e0&&(n+=16-r),e.__size=n,e.__cache={},this}function _(e){let t={boundary:0,storage:0};return typeof e==`number`||typeof e==`boolean`?(t.boundary=4,t.storage=4):e.isVector2?(t.boundary=8,t.storage=8):e.isVector3||e.isColor?(t.boundary=16,t.storage=12):e.isVector4?(t.boundary=16,t.storage=16):e.isMatrix3?(t.boundary=48,t.storage=48):e.isMatrix4?(t.boundary=64,t.storage=64):e.isTexture?nu(`WebGLRenderer: Texture samplers can not be part of an uniforms group.`):ArrayBuffer.isView(e)?(t.boundary=16,t.storage=e.byteLength):nu(`WebGLRenderer: Unsupported uniform value type.`,e),t}function v(t){let n=t.target;n.removeEventListener(`dispose`,v);let r=o.indexOf(n.__bindingPointIndex);o.splice(r,1),e.deleteBuffer(i[n.id]),delete i[n.id],delete a[n.id]}function y(){for(let t in i)e.deleteBuffer(i[t]);o=[],i={},a={}}return{bind:c,update:l,dispose:y}}var nb=new Uint16Array([12469,15057,12620,14925,13266,14620,13807,14376,14323,13990,14545,13625,14713,13328,14840,12882,14931,12528,14996,12233,15039,11829,15066,11525,15080,11295,15085,10976,15082,10705,15073,10495,13880,14564,13898,14542,13977,14430,14158,14124,14393,13732,14556,13410,14702,12996,14814,12596,14891,12291,14937,11834,14957,11489,14958,11194,14943,10803,14921,10506,14893,10278,14858,9960,14484,14039,14487,14025,14499,13941,14524,13740,14574,13468,14654,13106,14743,12678,14818,12344,14867,11893,14889,11509,14893,11180,14881,10751,14852,10428,14812,10128,14765,9754,14712,9466,14764,13480,14764,13475,14766,13440,14766,13347,14769,13070,14786,12713,14816,12387,14844,11957,14860,11549,14868,11215,14855,10751,14825,10403,14782,10044,14729,9651,14666,9352,14599,9029,14967,12835,14966,12831,14963,12804,14954,12723,14936,12564,14917,12347,14900,11958,14886,11569,14878,11247,14859,10765,14828,10401,14784,10011,14727,9600,14660,9289,14586,8893,14508,8533,15111,12234,15110,12234,15104,12216,15092,12156,15067,12010,15028,11776,14981,11500,14942,11205,14902,10752,14861,10393,14812,9991,14752,9570,14682,9252,14603,8808,14519,8445,14431,8145,15209,11449,15208,11451,15202,11451,15190,11438,15163,11384,15117,11274,15055,10979,14994,10648,14932,10343,14871,9936,14803,9532,14729,9218,14645,8742,14556,8381,14461,8020,14365,7603,15273,10603,15272,10607,15267,10619,15256,10631,15231,10614,15182,10535,15118,10389,15042,10167,14963,9787,14883,9447,14800,9115,14710,8665,14615,8318,14514,7911,14411,7507,14279,7198,15314,9675,15313,9683,15309,9712,15298,9759,15277,9797,15229,9773,15166,9668,15084,9487,14995,9274,14898,8910,14800,8539,14697,8234,14590,7790,14479,7409,14367,7067,14178,6621,15337,8619,15337,8631,15333,8677,15325,8769,15305,8871,15264,8940,15202,8909,15119,8775,15022,8565,14916,8328,14804,8009,14688,7614,14569,7287,14448,6888,14321,6483,14088,6171,15350,7402,15350,7419,15347,7480,15340,7613,15322,7804,15287,7973,15229,8057,15148,8012,15046,7846,14933,7611,14810,7357,14682,7069,14552,6656,14421,6316,14251,5948,14007,5528,15356,5942,15356,5977,15353,6119,15348,6294,15332,6551,15302,6824,15249,7044,15171,7122,15070,7050,14949,6861,14818,6611,14679,6349,14538,6067,14398,5651,14189,5311,13935,4958,15359,4123,15359,4153,15356,4296,15353,4646,15338,5160,15311,5508,15263,5829,15188,6042,15088,6094,14966,6001,14826,5796,14678,5543,14527,5287,14377,4985,14133,4586,13869,4257,15360,1563,15360,1642,15358,2076,15354,2636,15341,3350,15317,4019,15273,4429,15203,4732,15105,4911,14981,4932,14836,4818,14679,4621,14517,4386,14359,4156,14083,3795,13808,3437,15360,122,15360,137,15358,285,15355,636,15344,1274,15322,2177,15281,2765,15215,3223,15120,3451,14995,3569,14846,3567,14681,3466,14511,3305,14344,3121,14037,2800,13753,2467,15360,0,15360,1,15359,21,15355,89,15346,253,15325,479,15287,796,15225,1148,15133,1492,15008,1749,14856,1882,14685,1886,14506,1783,14324,1608,13996,1398,13702,1183]),rb=null;function ib(){return rb===null&&(rb=new wp(nb,16,16,qc,Pc),rb.name=`DFG_LUT`,rb.minFilter=wc,rb.magFilter=wc,rb.wrapS=yc,rb.wrapT=yc,rb.generateMipmaps=!1,rb.needsUpdate=!0),rb}var ab=class{constructor(e={}){let{canvas:t=Ql(),context:n=null,depth:r=!0,stencil:i=!1,alpha:a=!1,antialias:o=!1,premultipliedAlpha:s=!0,preserveDrawingBuffer:c=!1,powerPreference:l=`default`,failIfMajorPerformanceCaveat:u=!1,reversedDepthBuffer:d=!1,outputBufferType:f=Dc}=e;this.isWebGLRenderer=!0;let p;if(n!==null){if(typeof WebGLRenderingContext<`u`&&n instanceof WebGLRenderingContext)throw Error(`THREE.WebGLRenderer: WebGL 1 is not supported since r163.`);p=n.getContextAttributes().alpha}else p=a;let m=f,h=new Set([Yc,Jc,Kc]),g=new Set([Dc,Mc,Ac,Lc,Fc,Ic]),_=new Uint32Array(4),v=new Int32Array(4),y=new Z,b=null,x=null,S=[],C=[],w=null;this.domElement=t,this.debug={checkShaderErrors:!0,diagnostics:{keywords:!1},onShaderError:null},this.autoClear=!0,this.autoClearColor=!0,this.autoClearDepth=!0,this.autoClearStencil=!0,this.sortObjects=!0,this.clippingPlanes=[],this.localClippingEnabled=!1,this.toneMapping=0,this.toneMappingExposure=1,this.transmissionResolutionScale=1;let T=this,E=!1,D=null,O=null,ee=null,k=null;this._outputColorSpace=Vl;let A=0,te=0,j=null,M=-1,ne=null,N=new ed,P=new ed,F=null,re=new Q(0),ie=0,ae=t.width,I=t.height,L=1,oe=null,se=null,ce=new ed(0,0,ae,I),le=new ed(0,0,ae,I),ue=!1,de=new Bp,fe=!1,pe=!1,me=new ad,he=new Z,ge=new ed,_e={background:null,fog:null,environment:null,overrideMaterial:null,isScene:!0},ve=!1;function ye(){return j===null?L:1}let R=n;function be(e,n){return t.getContext(e,n)}let xe,Se,z,Ce,we,Te,Ee,De,Oe,ke,Ae,je,Me,Ne,Pe,Fe,Ie,Le,Re,ze,Be,B,Ve;try{let e={alpha:!0,depth:r,stencil:i,antialias:o,premultipliedAlpha:s,preserveDrawingBuffer:c,powerPreference:l,failIfMajorPerformanceCaveat:u};if(`setAttribute`in t&&t.setAttribute(`data-engine`,`three.js r186`),t.addEventListener(`webglcontextlost`,Ue,!1),t.addEventListener(`webglcontextrestored`,We,!1),t.addEventListener(`webglcontextcreationerror`,Ge,!1),R===null){let t=`webgl2`;if(R=be(t,e),R===null)throw be(t)?Error(`THREE.WebGLRenderer: Error creating WebGL context with your selected attributes.`):Error(`THREE.WebGLRenderer: Error creating WebGL context.`)}V()}catch(e){throw t.removeEventListener(`webglcontextlost`,Ue,!1),t.removeEventListener(`webglcontextrestored`,We,!1),t.removeEventListener(`webglcontextcreationerror`,Ge,!1),ru(`WebGLRenderer: `+e.message),e}function V(){xe=new C_(R),xe.init(),Be=new qy(R,xe),Se=new Qg(R,xe,e,Be),z=new Gy(R,xe),Se.reversedDepthBuffer&&d&&z.buffers.depth.setReversed(!0),O=R.createFramebuffer(),ee=R.createFramebuffer(),k=R.createFramebuffer(),Ce=new E_(R),we=new Ty,Te=new Ky(R,xe,z,we,Se,Be,Ce),Ee=new S_(T),De=new Ug(R),B=new Xg(R,De),Oe=new w_(R,De,Ce,B),ke=new O_(R,Oe,De,B,Ce),Le=new D_(R,Se,Te),Pe=new $g(we),Ae=new wy(T,Ee,xe,Se,B,Pe),je=new eb(T,we),Me=new ky,Ne=new Iy(xe),Ie=new Yg(T,Ee,z,ke,p,s),Fe=new Wy(T,ke,Se),Ve=new tb(R,Ce,Se,z),Re=new Zg(R,xe,Ce),ze=new T_(R,xe,Ce),Ce.programs=Ae.programs,T.capabilities=Se,T.extensions=xe,T.properties=we,T.renderLists=Me,T.shadowMap=Fe,T.state=z,T.info=Ce}m!==1009&&(w=new A_(m,t.width,t.height,o,r,i));let He=new Zy(T,R);this.xr=He,this.getContext=function(){return R},this.getContextAttributes=function(){return R.getContextAttributes()},this.forceContextLoss=function(){let e=xe.get(`WEBGL_lose_context`);e&&e.loseContext()},this.forceContextRestore=function(){let e=xe.get(`WEBGL_lose_context`);e&&e.restoreContext()},this.getPixelRatio=function(){return L},this.setPixelRatio=function(e){e!==void 0&&(L=e,this.setSize(ae,I,!1))},this.getSize=function(e){return e.set(ae,I)},this.setSize=function(e,n,r=!0){if(He.isPresenting){nu(`WebGLRenderer: Can't change size while VR device is presenting.`);return}ae=e,I=n,t.width=Math.floor(e*L),t.height=Math.floor(n*L),r===!0&&(t.style.width=e+`px`,t.style.height=n+`px`),w!==null&&w.setSize(t.width,t.height),this.setViewport(0,0,e,n)},this.getDrawingBufferSize=function(e){return e.set(ae*L,I*L).floor()},this.setDrawingBufferSize=function(e,n,r){ae=e,I=n,L=r,t.width=Math.floor(e*r),t.height=Math.floor(n*r),this.setViewport(0,0,e,n)},this.setEffects=function(e){if(m===1009){ru(`WebGLRenderer: setEffects() requires outputBufferType set to HalfFloatType or FloatType.`);return}if(e){for(let t=0;t{function n(){if(r.forEach(function(e){let t=we.get(e).currentProgram;(t===void 0||t.isReady())&&r.delete(e)}),r.size===0){t(e);return}setTimeout(n,10)}xe.get(`KHR_parallel_shader_compile`)===null?setTimeout(n,10):n()})};let H=null;function Xe(e){H&&H(e)}function Ze(){$e.stop()}function Qe(){$e.start()}let $e=new Hg;$e.setAnimationLoop(Xe),typeof self<`u`&&$e.setContext(self),this.setAnimationLoop=function(e){H=e,He.setAnimationLoop(e),e===null?$e.stop():$e.start()},He.addEventListener(`sessionstart`,Ze),He.addEventListener(`sessionend`,Qe),this.render=function(e,t){if(t!==void 0&&t.isCamera!==!0){ru(`WebGLRenderer.render: camera is not an instance of THREE.Camera.`);return}if(E===!0)return;D!==null&&D.renderStart(e,t);let n=He.enabled===!0&&He.isPresenting===!0,r=w!==null&&(j===null||n)&&w.begin(T,j);if(e.matrixWorldAutoUpdate===!0&&e.updateMatrixWorld(),t.parent===null&&t.matrixWorldAutoUpdate===!0&&t.updateMatrixWorld(),He.enabled===!0&&He.isPresenting===!0&&(w===null||w.isCompositing()===!1)&&(He.cameraAutoUpdate===!0&&He.updateCamera(t),t=He.getCamera()),e.isScene===!0&&e.onBeforeRender(T,e,t,j),x=Ne.get(e,C.length),x.init(t),x.state.textureUnits=Te.getTextureUnits(),C.push(x),me.multiplyMatrices(t.projectionMatrix,t.matrixWorldInverse),de.setFromProjectionMatrix(me,Jl,t.reversedDepth),pe=this.localClippingEnabled,fe=Pe.init(this.clippingPlanes,pe),b=Me.get(e,S.length),b.init(),S.push(b),He.enabled===!0&&He.isPresenting===!0){let e=T.xr.getDepthSensingMesh();e!==null&&et(e,t,-1/0,T.sortObjects)}et(e,t,0,T.sortObjects),b.finish(),D!==null&&D.updateLights(x.state.lightsArray),T.sortObjects===!0&&b.sort(oe,se),ve=He.enabled===!1||He.isPresenting===!1||He.hasDepthSensing()===!1,ve&&Ie.addToRenderList(b,e),this.info.render.frame++,this.info.autoReset===!0&&this.info.reset(),fe===!0&&Pe.beginShadows();let i=x.state.shadowsArray;if(Fe.render(i,e,t),fe===!0&&Pe.endShadows(),(r&&w.hasRenderPass())===!1){let n=b.opaque,r=b.transmissive;if(x.setupLights(),t.isArrayCamera){let i=t.cameras;if(r.length>0)for(let t=0,a=i.length;t0&&nt(n,r,e,t),ve&&Ie.render(e),tt(b,e,t)}j!==null&&te===0&&(Te.updateMultisampleRenderTarget(j),Te.updateRenderTargetMipmap(j)),r&&w.end(T),e.isScene===!0&&e.onAfterRender(T,e,t),B.resetDefaultState(),M=-1,ne=null,C.pop(),C.length>0?(x=C[C.length-1],Te.setTextureUnits(x.state.textureUnits),fe===!0&&Pe.setGlobalState(T.clippingPlanes,x.state.camera)):x=null,S.pop(),b=S.length>0?S[S.length-1]:null,D!==null&&D.renderEnd()};function et(e,t,n,r){if(e.visible===!1)return;if(e.layers.test(t.layers)){if(e.isGroup)n=e.renderOrder;else if(e.isLOD)e.autoUpdate===!0&&e.update(t);else if(e.isLightProbeGrid)x.pushLightProbeGrid(e);else if(e.isLight)x.pushLight(e),e.castShadow&&x.pushShadow(e);else if(e.isSprite){if(!e.frustumCulled||e.intersectsFrustum(de)){r&&ge.setFromMatrixPosition(e.matrixWorld).applyMatrix4(me);let i=ke.update(e),a=e.material;a.visible&&b.push(e,i,a,n,ge.z,null,t)}}else if((e.isMesh||e.isLine||e.isPoints)&&(!e.frustumCulled||e.intersectsFrustum(de))){let i=ke.update(e),a=e.material;if(r&&(e.boundingSphere===void 0?(i.boundingSphere===null&&i.computeBoundingSphere(),ge.copy(i.boundingSphere.center)):(e.boundingSphere===null&&e.computeBoundingSphere(),ge.copy(e.boundingSphere.center)),ge.applyMatrix4(e.matrixWorld).applyMatrix4(me)),Array.isArray(a)){let r=i.groups;for(let o=0,s=r.length;o0&&rt(i,t,n),a.length>0&&rt(a,t,n),o.length>0&&rt(o,t,n),z.buffers.depth.setTest(!0),z.buffers.depth.setMask(!0),z.buffers.color.setMask(!0),z.setPolygonOffset(!1)}function nt(e,t,n,r){if((n.isScene===!0?n.overrideMaterial:null)!==null)return;if(x.state.transmissionRenderTarget[r.id]===void 0){let e=xe.has(`EXT_color_buffer_half_float`)||xe.has(`EXT_color_buffer_float`);x.state.transmissionRenderTarget[r.id]=new nd(1,1,{generateMipmaps:!0,type:e?Pc:Dc,minFilter:Ec,samples:Math.max(4,Se.samples),stencilBuffer:i,resolveDepthBuffer:!1,resolveStencilBuffer:!1,storeMultisampledDepthBuffer:!1,storeMultisampledStencilBuffer:!1,colorSpace:Uu.workingColorSpace})}let a=x.state.transmissionRenderTarget[r.id],o=r.viewport||N;a.setSize(o.z*T.transmissionResolutionScale,o.w*T.transmissionResolutionScale);let s=T.getRenderTarget(),c=T.getActiveCubeFace(),l=T.getActiveMipmapLevel();T.setRenderTarget(a),T.getClearColor(re),ie=T.getClearAlpha(),ie<1&&T.setClearColor(16777215,.5),T.clear(),ve&&Ie.render(n);let u=T.toneMapping;T.toneMapping=0;let d=r.viewport;if(r.viewport!==void 0&&(r.viewport=void 0),x.setupLightsView(r),fe===!0&&Pe.setGlobalState(T.clippingPlanes,r),rt(e,n,r),Te.updateMultisampleRenderTarget(a),Te.updateRenderTargetMipmap(a),xe.has(`WEBGL_multisampled_render_to_texture`)===!1){let e=!1;for(let i=0,a=t.length;i0,r.currentProgram=d,r.uniformsList=null,d}function U(e){if(e.uniformsList===null){let t=e.currentProgram.getUniforms();e.uniformsList=Lv.seqWithValue(t.seq,e.uniforms)}return e.uniformsList}function ot(e,t){let n=we.get(e);n.outputColorSpace=t.outputColorSpace,n.batching=t.batching,n.batchingColor=t.batchingColor,n.instancing=t.instancing,n.instancingColor=t.instancingColor,n.instancingMorph=t.instancingMorph,n.skinning=t.skinning,n.morphTargets=t.morphTargets,n.morphNormals=t.morphNormals,n.morphColors=t.morphColors,n.morphTargetsCount=t.morphTargetsCount,n.numClippingPlanes=t.numClippingPlanes,n.numIntersection=t.numClipIntersection,n.vertexAlphas=t.vertexAlphas,n.vertexTangents=t.vertexTangents,n.toneMapping=t.toneMapping}function st(e,t){if(e.length===0)return null;if(e.length===1)return e[0].texture===null?null:e[0];y.setFromMatrixPosition(t.matrixWorld);for(let t=0,n=e.length;t0),f=!!n.morphAttributes.position,p=!!n.morphAttributes.normal,m=!!n.morphAttributes.color,h=0;r.toneMapped&&(j===null||j.isXRRenderTarget===!0)&&(h=T.toneMapping);let g=n.morphAttributes.position||n.morphAttributes.normal||n.morphAttributes.color,_=g===void 0?0:g.length,v=we.get(r),y=x.state.lights;if(fe===!0&&(pe===!0||e!==ne)){let t=e===ne&&r.id===M;Pe.setState(r,e,t)}let b=!1;r.version===v.__version?v.needsLights&&v.lightsStateVersion!==y.state.version?b=!0:v.outputColorSpace===s?i.isBatchedMesh&&v.batching===!1||!i.isBatchedMesh&&v.batching===!0||i.isBatchedMesh&&v.batchingColor===!0&&i._colorsTexture===null||i.isBatchedMesh&&v.batchingColor===!1&&i._colorsTexture!==null||i.isInstancedMesh&&v.instancing===!1||!i.isInstancedMesh&&v.instancing===!0||i.isSkinnedMesh&&v.skinning===!1||!i.isSkinnedMesh&&v.skinning===!0||i.isInstancedMesh&&v.instancingColor===!0&&i.instanceColor===null||i.isInstancedMesh&&v.instancingColor===!1&&i.instanceColor!==null||i.isInstancedMesh&&v.instancingMorph===!0&&i.morphTexture===null||i.isInstancedMesh&&v.instancingMorph===!1&&i.morphTexture!==null?b=!0:v.envMap===l?r.fog===!0&&v.fog!==a||v.numClippingPlanes!==void 0&&(v.numClippingPlanes!==Pe.numPlanes||v.numIntersection!==Pe.numIntersection)?b=!0:v.vertexAlphas===u&&v.vertexTangents===d&&v.morphTargets===f&&v.morphNormals===p&&v.morphColors===m&&v.toneMapping===h&&v.morphTargetsCount===_?!!v.lightProbeGrid!=x.state.lightProbeGridArray.length>0&&(b=!0):b=!0:b=!0:b=!0:(b=!0,v.__version=r.version);let S=v.currentProgram;b===!0&&(S=at(r,t,i),D&&r.isNodeMaterial&&D.onUpdateProgram(r,S,v));let C=!1,w=!1,E=!1,O=S.getUniforms(),ee=v.uniforms;if(z.useProgram(S.program)&&(C=!0,w=!0,E=!0),r.id!==M&&(M=r.id,w=!0),v.needsLights){let e=st(x.state.lightProbeGridArray,i);v.lightProbeGrid!==e&&(v.lightProbeGrid=e,w=!0)}if(C||ne!==e){z.buffers.depth.getReversed()&&e.reversedDepth!==!0&&(e._reversedDepth=!0,e.updateProjectionMatrix()),O.setValue(R,`projectionMatrix`,e.projectionMatrix),O.setValue(R,`viewMatrix`,e.matrixWorldInverse);let t=O.map.cameraPosition;t!==void 0&&t.setValue(R,he.setFromMatrixPosition(e.matrixWorld)),Se.logarithmicDepthBuffer&&O.setValue(R,`logDepthBufFC`,2/(Math.log(e.far+1)/Math.LN2)),(r.isMeshPhongMaterial||r.isMeshToonMaterial||r.isMeshLambertMaterial||r.isMeshBasicMaterial||r.isMeshStandardMaterial||r.isShaderMaterial)&&O.setValue(R,`isOrthographic`,e.isOrthographicCamera===!0),ne!==e&&(ne=e,w=!0,E=!0)}if(v.needsLights&&(y.state.sunShadowMap.length>0&&O.setValue(R,`sunShadowMap`,y.state.sunShadowMap,Te),y.state.directionalShadowMap.length>0&&O.setValue(R,`directionalShadowMap`,y.state.directionalShadowMap,Te),y.state.spotShadowMap.length>0&&O.setValue(R,`spotShadowMap`,y.state.spotShadowMap,Te),y.state.pointShadowMap.length>0&&O.setValue(R,`pointShadowMap`,y.state.pointShadowMap,Te)),i.isSkinnedMesh){O.setOptional(R,i,`bindMatrix`),O.setOptional(R,i,`bindMatrixInverse`);let e=i.skeleton;e&&(e.boneTexture===null&&e.computeBoneTexture(),O.setValue(R,`boneTexture`,e.boneTexture,Te))}i.isBatchedMesh&&(O.setOptional(R,i,`batchingTexture`),O.setValue(R,`batchingTexture`,i._matricesTexture,Te),O.setOptional(R,i,`batchingIdTexture`),O.setValue(R,`batchingIdTexture`,i._indirectTexture,Te),O.setOptional(R,i,`batchingColorTexture`),i._colorsTexture!==null&&O.setValue(R,`batchingColorTexture`,i._colorsTexture,Te));let k=n.morphAttributes;if((k.position!==void 0||k.normal!==void 0||k.color!==void 0)&&Le.update(i,n,S),(w||v.receiveShadow!==i.receiveShadow)&&(v.receiveShadow=i.receiveShadow,O.setValue(R,`receiveShadow`,i.receiveShadow)),(r.isMeshStandardMaterial||r.isMeshLambertMaterial||r.isMeshPhongMaterial)&&r.envMap===null&&t.environment!==null&&(ee.envMapIntensity.value=t.environmentIntensity),ee.dfgLUT!==void 0&&(ee.dfgLUT.value=ib()),w){if(O.setValue(R,`toneMappingExposure`,T.toneMappingExposure),v.needsLights&<(ee,E),a&&r.fog===!0&&je.refreshFogUniforms(ee,a),je.refreshMaterialUniforms(ee,r,L,I,x.state.transmissionRenderTarget[e.id]),v.needsLights&&v.lightProbeGrid){let e=v.lightProbeGrid;ee.probesSH.value=e.texture,ee.probesMin.value.copy(e.boundingBox.min),ee.probesMax.value.copy(e.boundingBox.max),ee.probesResolution.value.copy(e.resolution)}Lv.upload(R,U(v),ee,Te)}if(r.isShaderMaterial&&r.uniformsNeedUpdate===!0&&(Lv.upload(R,U(v),ee,Te),r.uniformsNeedUpdate=!1),r.isSpriteMaterial&&O.setValue(R,`center`,i.center),O.setValue(R,`modelViewMatrix`,i.modelViewMatrix),O.setValue(R,`normalMatrix`,i.normalMatrix),O.setValue(R,`modelMatrix`,i.matrixWorld),r.uniformsGroups!==void 0){let e=r.uniformsGroups;for(let t=0,n=e.length;t0&&Te.useMultisampledRTT(e)===!1?we.get(e).__webglMultisampledFramebuffer:Array.isArray(c)?c[n]:c,N.copy(e.viewport),P.copy(e.scissor),F=e.scissorTest}else N.copy(ce).multiplyScalar(L).floor(),P.copy(le).multiplyScalar(L).floor(),F=ue;if(n!==0&&(r=O),z.bindFramebuffer(R.FRAMEBUFFER,r)&&z.drawBuffers(e,r),z.viewport(N),z.scissor(P),z.setScissorTest(F),i){let r=we.get(e.texture);R.framebufferTexture2D(R.FRAMEBUFFER,R.COLOR_ATTACHMENT0,R.TEXTURE_CUBE_MAP_POSITIVE_X+t,r.__webglTexture,n)}else if(a){let r=t;for(let t=0;t1&&R.readBuffer(R.COLOR_ATTACHMENT0+s);let u=dt(o);if(u.__formatReadable===!1){ru(`WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.`);return}if(u.__typeReadable===!1){ru(`WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.`);return}t>=0&&t<=e.width-r&&n>=0&&n<=e.height-i&&R.readPixels(t,n,r,i,Be.convert(c),Be.convert(l),a)}finally{let e=j===null?null:we.get(j).__webglFramebuffer;z.bindFramebuffer(R.FRAMEBUFFER,e)}}},this.readRenderTargetPixelsAsync=async function(e,t,n,r,i,a,o,s=0){if(!(e&&e.isWebGLRenderTarget))throw Error(`THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.`);let c=we.get(e).__webglFramebuffer;if(e.isWebGLCubeRenderTarget&&o!==void 0&&(c=c[o]),c)if(t>=0&&t<=e.width-r&&n>=0&&n<=e.height-i){z.bindFramebuffer(R.FRAMEBUFFER,c);let o=e.textures[s],l=o.format,u=o.type;e.textures.length>1&&R.readBuffer(R.COLOR_ATTACHMENT0+s);let d=dt(o);if(d.__formatReadable===!1)throw Error(`THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in RGBA or implementation defined format.`);if(d.__typeReadable===!1)throw Error(`THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in UnsignedByteType or implementation defined type.`);let f=R.createBuffer();R.bindBuffer(R.PIXEL_PACK_BUFFER,f),R.bufferData(R.PIXEL_PACK_BUFFER,a.byteLength,R.STREAM_READ),R.readPixels(t,n,r,i,Be.convert(l),Be.convert(u),0),R.bindBuffer(R.PIXEL_PACK_BUFFER,null);let p=j===null?null:we.get(j).__webglFramebuffer;z.bindFramebuffer(R.FRAMEBUFFER,p);let m=R.fenceSync(R.SYNC_GPU_COMMANDS_COMPLETE,0);return R.flush(),await au(R,m,4),R.bindBuffer(R.PIXEL_PACK_BUFFER,f),R.getBufferSubData(R.PIXEL_PACK_BUFFER,0,a),R.bindBuffer(R.PIXEL_PACK_BUFFER,null),R.deleteBuffer(f),R.deleteSync(m),a}else throw Error(`THREE.WebGLRenderer.readRenderTargetPixelsAsync: requested read bounds are out of range.`)},this.copyFramebufferToTexture=function(e,t=null,n=0){let r=2**-n,i=Math.floor(e.image.width*r),a=Math.floor(e.image.height*r),o=t===null?0:t.x,s=t===null?0:t.y;Te.setTexture2D(e,0),R.copyTexSubImage2D(R.TEXTURE_2D,n,0,0,o,s,i,a),z.unbindTexture()},this.copyTextureToTexture=function(e,t,n=null,r=null,i=0,a=0){let o,s,c,l,u,d,f,p,m,h=e.isCompressedTexture?e.mipmaps[a]:e.image;if(n!==null)o=n.max.x-n.min.x,s=n.max.y-n.min.y,c=n.isBox3?n.max.z-n.min.z:1,l=n.min.x,u=n.min.y,d=n.isBox3?n.min.z:0;else{let t=2**-i;o=Math.floor(h.width*t),s=Math.floor(h.height*t),c=e.isDataArrayTexture?h.depth:e.isData3DTexture?Math.floor(h.depth*t):1,l=0,u=0,d=0}r===null?(f=0,p=0,m=0):(f=r.x,p=r.y,m=r.z);let g=Be.convert(t.format),_=Be.convert(t.type),v;t.isData3DTexture?(Te.setTexture3D(t,0),v=R.TEXTURE_3D):t.isDataArrayTexture||t.isCompressedArrayTexture?(Te.setTexture2DArray(t,0),v=R.TEXTURE_2D_ARRAY):(Te.setTexture2D(t,0),v=R.TEXTURE_2D),z.activeTexture(R.TEXTURE0),z.pixelStorei(R.UNPACK_FLIP_Y_WEBGL,t.flipY),z.pixelStorei(R.UNPACK_PREMULTIPLY_ALPHA_WEBGL,t.premultiplyAlpha),z.pixelStorei(R.UNPACK_ALIGNMENT,t.unpackAlignment);let y=z.getParameter(R.UNPACK_ROW_LENGTH),b=z.getParameter(R.UNPACK_IMAGE_HEIGHT),x=z.getParameter(R.UNPACK_SKIP_PIXELS),S=z.getParameter(R.UNPACK_SKIP_ROWS),C=z.getParameter(R.UNPACK_SKIP_IMAGES);z.pixelStorei(R.UNPACK_ROW_LENGTH,h.width),z.pixelStorei(R.UNPACK_IMAGE_HEIGHT,h.height),z.pixelStorei(R.UNPACK_SKIP_PIXELS,l),z.pixelStorei(R.UNPACK_SKIP_ROWS,u),z.pixelStorei(R.UNPACK_SKIP_IMAGES,d);let w=e.isDataArrayTexture||e.isData3DTexture,T=t.isDataArrayTexture||t.isData3DTexture;if(e.isDepthTexture){let n=we.get(e),r=we.get(t),h=we.get(n.__renderTarget),g=we.get(r.__renderTarget);z.bindFramebuffer(R.READ_FRAMEBUFFER,h.__webglFramebuffer),z.bindFramebuffer(R.DRAW_FRAMEBUFFER,g.__webglFramebuffer);for(let n=0;n=0&&(x[r]=null,b[r].disconnect(n))}for(let t=0;t=x.length){x.push(n),r=e;break}else if(x[e]===null){x[e]=n,r=e;break}if(r===-1)break}let i=b[r];i&&i.connect(n)}}let re=new J,ie=new J;function ae(e,t,n){re.setFromMatrixPosition(t.matrixWorld),ie.setFromMatrixPosition(n.matrixWorld);let r=re.distanceTo(ie),i=t.projectionMatrix.elements,a=n.projectionMatrix.elements,o=i[14]/(i[10]-1),s=i[14]/(i[10]+1),c=(i[9]+1)/i[5],l=(i[9]-1)/i[5],u=(i[8]-1)/i[0],d=(a[8]+1)/a[0],f=o*u,p=o*d,m=r/(-u+d),h=m*-u;if(t.matrixWorld.decompose(e.position,e.quaternion,e.scale),e.translateX(h),e.translateZ(m),e.matrixWorld.compose(e.position,e.quaternion,e.scale),e.matrixWorldInverse.copy(e.matrixWorld).invert(),i[10]===-1)e.projectionMatrix.copy(t.projectionMatrix),e.projectionMatrixInverse.copy(t.projectionMatrixInverse);else{let t=o+m,n=s+m,i=f-h,a=p+(r-h),u=c*s/n*t,d=l*s/n*t;e.projectionMatrix.makePerspective(i,a,u,d,t,n),e.projectionMatrixInverse.copy(e.projectionMatrix).invert()}}function oe(e,t){t===null?e.matrixWorld.copy(e.matrix):e.matrixWorld.multiplyMatrices(t.matrixWorld,e.matrix),e.matrixWorldInverse.copy(e.matrixWorld).invert()}this.updateCamera=function(e){if(r===null)return;let t=e.near,n=e.far;h.texture!==null&&(h.depthNear>0&&(t=h.depthNear),h.depthFar>0&&(n=h.depthFar)),O.near=E.near=T.near=t,O.far=E.far=T.far=n,(ee!==O.near||te!==O.far)&&(r.updateRenderState({depthNear:O.near,depthFar:O.far}),ee=O.near,te=O.far),O.layers.mask=e.layers.mask|6,T.layers.mask=O.layers.mask&-5,E.layers.mask=O.layers.mask&-3;let i=e.parent,a=O.cameras;oe(O,i);for(let e=0;e0&&(e.alphaTest.value=r.alphaTest);let i=t.get(r),a=i.envMap,o=i.envMapRotation;a&&(e.envMap.value=a,e.envMapRotation.value.setFromMatrix4(DC.makeRotationFromEuler(o)).transpose(),a.isCubeTexture&&a.isRenderTargetTexture===!1&&e.envMapRotation.value.premultiply(OC),e.reflectivity.value=r.reflectivity,e.ior.value=r.ior,e.refractionRatio.value=r.refractionRatio),r.lightMap&&(e.lightMap.value=r.lightMap,e.lightMapIntensity.value=r.lightMapIntensity,n(r.lightMap,e.lightMapTransform)),r.aoMap&&(e.aoMap.value=r.aoMap,e.aoMapIntensity.value=r.aoMapIntensity,n(r.aoMap,e.aoMapTransform))}function o(e,t){e.diffuse.value.copy(t.color),e.opacity.value=t.opacity,t.map&&(e.map.value=t.map,n(t.map,e.mapTransform))}function s(e,t){e.dashSize.value=t.dashSize,e.totalSize.value=t.dashSize+t.gapSize,e.scale.value=t.scale}function c(e,t,r,i){e.diffuse.value.copy(t.color),e.opacity.value=t.opacity,e.size.value=t.size*r,e.scale.value=i*.5,t.map&&(e.map.value=t.map,n(t.map,e.uvTransform)),t.alphaMap&&(e.alphaMap.value=t.alphaMap,n(t.alphaMap,e.alphaMapTransform)),t.alphaTest>0&&(e.alphaTest.value=t.alphaTest)}function l(e,t){e.diffuse.value.copy(t.color),e.opacity.value=t.opacity,e.rotation.value=t.rotation,t.map&&(e.map.value=t.map,n(t.map,e.mapTransform)),t.alphaMap&&(e.alphaMap.value=t.alphaMap,n(t.alphaMap,e.alphaMapTransform)),t.alphaTest>0&&(e.alphaTest.value=t.alphaTest)}function u(e,t){e.specular.value.copy(t.specular),e.shininess.value=Math.max(t.shininess,1e-4)}function d(e,t){t.gradientMap&&(e.gradientMap.value=t.gradientMap)}function f(e,t){e.metalness.value=t.metalness,t.metalnessMap&&(e.metalnessMap.value=t.metalnessMap,n(t.metalnessMap,e.metalnessMapTransform)),e.roughness.value=t.roughness,t.roughnessMap&&(e.roughnessMap.value=t.roughnessMap,n(t.roughnessMap,e.roughnessMapTransform)),t.envMap&&(e.envMapIntensity.value=t.envMapIntensity)}function p(e,t,r){e.ior.value=t.ior,t.sheen>0&&(e.sheenColor.value.copy(t.sheenColor).multiplyScalar(t.sheen),e.sheenRoughness.value=t.sheenRoughness,t.sheenColorMap&&(e.sheenColorMap.value=t.sheenColorMap,n(t.sheenColorMap,e.sheenColorMapTransform)),t.sheenRoughnessMap&&(e.sheenRoughnessMap.value=t.sheenRoughnessMap,n(t.sheenRoughnessMap,e.sheenRoughnessMapTransform))),t.clearcoat>0&&(e.clearcoat.value=t.clearcoat,e.clearcoatRoughness.value=t.clearcoatRoughness,t.clearcoatMap&&(e.clearcoatMap.value=t.clearcoatMap,n(t.clearcoatMap,e.clearcoatMapTransform)),t.clearcoatRoughnessMap&&(e.clearcoatRoughnessMap.value=t.clearcoatRoughnessMap,n(t.clearcoatRoughnessMap,e.clearcoatRoughnessMapTransform)),t.clearcoatNormalMap&&(e.clearcoatNormalMap.value=t.clearcoatNormalMap,n(t.clearcoatNormalMap,e.clearcoatNormalMapTransform),e.clearcoatNormalScale.value.copy(t.clearcoatNormalScale),t.side===1&&e.clearcoatNormalScale.value.negate())),t.dispersion>0&&(e.dispersion.value=t.dispersion),t.retroreflectivity>0&&(e.retroreflectivity.value=t.retroreflectivity),t.iridescence>0&&(e.iridescence.value=t.iridescence,e.iridescenceIOR.value=t.iridescenceIOR,e.iridescenceThicknessMinimum.value=t.iridescenceThicknessRange[0],e.iridescenceThicknessMaximum.value=t.iridescenceThicknessRange[1],t.iridescenceMap&&(e.iridescenceMap.value=t.iridescenceMap,n(t.iridescenceMap,e.iridescenceMapTransform)),t.iridescenceThicknessMap&&(e.iridescenceThicknessMap.value=t.iridescenceThicknessMap,n(t.iridescenceThicknessMap,e.iridescenceThicknessMapTransform))),t.transmission>0&&(e.transmission.value=t.transmission,e.transmissionSamplerMap.value=r.texture,e.transmissionSamplerSize.value.set(r.width,r.height),t.transmissionMap&&(e.transmissionMap.value=t.transmissionMap,n(t.transmissionMap,e.transmissionMapTransform)),e.thickness.value=t.thickness,t.thicknessMap&&(e.thicknessMap.value=t.thicknessMap,n(t.thicknessMap,e.thicknessMapTransform)),e.attenuationDistance.value=t.attenuationDistance,e.attenuationColor.value.copy(t.attenuationColor)),t.anisotropy>0&&(e.anisotropyVector.value.set(t.anisotropy*Math.cos(t.anisotropyRotation),t.anisotropy*Math.sin(t.anisotropyRotation)),t.anisotropyMap&&(e.anisotropyMap.value=t.anisotropyMap,n(t.anisotropyMap,e.anisotropyMapTransform))),e.specularIntensity.value=t.specularIntensity,e.specularColor.value.copy(t.specularColor),t.specularColorMap&&(e.specularColorMap.value=t.specularColorMap,n(t.specularColorMap,e.specularColorMapTransform)),t.specularIntensityMap&&(e.specularIntensityMap.value=t.specularIntensityMap,n(t.specularIntensityMap,e.specularIntensityMapTransform))}function m(e,t){t.matcap&&(e.matcap.value=t.matcap)}function h(e,n){let r=t.get(n).light;e.referencePosition.value.setFromMatrixPosition(r.matrixWorld),e.nearDistance.value=r.shadow.camera.near,e.farDistance.value=r.shadow.camera.far}return{refreshFogUniforms:r,refreshMaterialUniforms:i}}function AC(e,t,n,r){let i={},a={},o=[],s=e.getParameter(e.MAX_UNIFORM_BUFFER_BINDINGS);function c(e,t){let n=t.program;r.uniformBlockBinding(e,n)}function l(e,n){let o=i[e.id];o===void 0&&(g(e),o=u(e),i[e.id]=o,e.addEventListener(`dispose`,v));let s=n.program;r.updateUBOMapping(e,s);let c=t.render.frame;a[e.id]!==c&&(f(e),a[e.id]=c)}function u(t){let n=d();t.__bindingPointIndex=n;let r=e.createBuffer(),i=t.__size,a=t.usage;return e.bindBuffer(e.UNIFORM_BUFFER,r),e.bufferData(e.UNIFORM_BUFFER,i,a),e.bindBuffer(e.UNIFORM_BUFFER,null),e.bindBufferBase(e.UNIFORM_BUFFER,n,r),r}function d(){for(let e=0;e0&&(n+=16-r),e.__size=n,e.__cache={},this}function _(e){let t={boundary:0,storage:0};return typeof e==`number`||typeof e==`boolean`?(t.boundary=4,t.storage=4):e.isVector2?(t.boundary=8,t.storage=8):e.isVector3||e.isColor?(t.boundary=16,t.storage=12):e.isVector4?(t.boundary=16,t.storage=16):e.isMatrix3?(t.boundary=48,t.storage=48):e.isMatrix4?(t.boundary=64,t.storage=64):e.isTexture?Ap(`WebGLRenderer: Texture samplers can not be part of an uniforms group.`):ArrayBuffer.isView(e)?(t.boundary=16,t.storage=e.byteLength):Ap(`WebGLRenderer: Unsupported uniform value type.`,e),t}function v(t){let n=t.target;n.removeEventListener(`dispose`,v);let r=o.indexOf(n.__bindingPointIndex);o.splice(r,1),e.deleteBuffer(i[n.id]),delete i[n.id],delete a[n.id]}function y(){for(let t in i)e.deleteBuffer(i[t]);o=[],i={},a={}}return{bind:c,update:l,dispose:y}}var jC=new Uint16Array([12469,15057,12620,14925,13266,14620,13807,14376,14323,13990,14545,13625,14713,13328,14840,12882,14931,12528,14996,12233,15039,11829,15066,11525,15080,11295,15085,10976,15082,10705,15073,10495,13880,14564,13898,14542,13977,14430,14158,14124,14393,13732,14556,13410,14702,12996,14814,12596,14891,12291,14937,11834,14957,11489,14958,11194,14943,10803,14921,10506,14893,10278,14858,9960,14484,14039,14487,14025,14499,13941,14524,13740,14574,13468,14654,13106,14743,12678,14818,12344,14867,11893,14889,11509,14893,11180,14881,10751,14852,10428,14812,10128,14765,9754,14712,9466,14764,13480,14764,13475,14766,13440,14766,13347,14769,13070,14786,12713,14816,12387,14844,11957,14860,11549,14868,11215,14855,10751,14825,10403,14782,10044,14729,9651,14666,9352,14599,9029,14967,12835,14966,12831,14963,12804,14954,12723,14936,12564,14917,12347,14900,11958,14886,11569,14878,11247,14859,10765,14828,10401,14784,10011,14727,9600,14660,9289,14586,8893,14508,8533,15111,12234,15110,12234,15104,12216,15092,12156,15067,12010,15028,11776,14981,11500,14942,11205,14902,10752,14861,10393,14812,9991,14752,9570,14682,9252,14603,8808,14519,8445,14431,8145,15209,11449,15208,11451,15202,11451,15190,11438,15163,11384,15117,11274,15055,10979,14994,10648,14932,10343,14871,9936,14803,9532,14729,9218,14645,8742,14556,8381,14461,8020,14365,7603,15273,10603,15272,10607,15267,10619,15256,10631,15231,10614,15182,10535,15118,10389,15042,10167,14963,9787,14883,9447,14800,9115,14710,8665,14615,8318,14514,7911,14411,7507,14279,7198,15314,9675,15313,9683,15309,9712,15298,9759,15277,9797,15229,9773,15166,9668,15084,9487,14995,9274,14898,8910,14800,8539,14697,8234,14590,7790,14479,7409,14367,7067,14178,6621,15337,8619,15337,8631,15333,8677,15325,8769,15305,8871,15264,8940,15202,8909,15119,8775,15022,8565,14916,8328,14804,8009,14688,7614,14569,7287,14448,6888,14321,6483,14088,6171,15350,7402,15350,7419,15347,7480,15340,7613,15322,7804,15287,7973,15229,8057,15148,8012,15046,7846,14933,7611,14810,7357,14682,7069,14552,6656,14421,6316,14251,5948,14007,5528,15356,5942,15356,5977,15353,6119,15348,6294,15332,6551,15302,6824,15249,7044,15171,7122,15070,7050,14949,6861,14818,6611,14679,6349,14538,6067,14398,5651,14189,5311,13935,4958,15359,4123,15359,4153,15356,4296,15353,4646,15338,5160,15311,5508,15263,5829,15188,6042,15088,6094,14966,6001,14826,5796,14678,5543,14527,5287,14377,4985,14133,4586,13869,4257,15360,1563,15360,1642,15358,2076,15354,2636,15341,3350,15317,4019,15273,4429,15203,4732,15105,4911,14981,4932,14836,4818,14679,4621,14517,4386,14359,4156,14083,3795,13808,3437,15360,122,15360,137,15358,285,15355,636,15344,1274,15322,2177,15281,2765,15215,3223,15120,3451,14995,3569,14846,3567,14681,3466,14511,3305,14344,3121,14037,2800,13753,2467,15360,0,15360,1,15359,21,15355,89,15346,253,15325,479,15287,796,15225,1148,15133,1492,15008,1749,14856,1882,14685,1886,14506,1783,14324,1608,13996,1398,13702,1183]),MC=null;function NC(){return MC===null&&(MC=new Xg(jC,16,16,xf,cf),MC.name=`DFG_LUT`,MC.minFilter=Zd,MC.magFilter=Zd,MC.wrapS=Kd,MC.wrapT=Kd,MC.generateMipmaps=!1,MC.needsUpdate=!0),MC}var PC=class{constructor(e={}){let{canvas:t=Ep(),context:n=null,depth:r=!0,stencil:i=!1,alpha:a=!1,antialias:o=!1,premultipliedAlpha:s=!0,preserveDrawingBuffer:c=!1,powerPreference:l=`default`,failIfMajorPerformanceCaveat:u=!1,reversedDepthBuffer:d=!1,outputBufferType:f=ef}=e;this.isWebGLRenderer=!0;let p;if(n!==null){if(typeof WebGLRenderingContext<`u`&&n instanceof WebGLRenderingContext)throw Error(`THREE.WebGLRenderer: WebGL 1 is not supported since r163.`);p=n.getContextAttributes().alpha}else p=a;let m=f,h=new Set([Cf,Sf,bf]),g=new Set([ef,of,rf,df,lf,uf]),_=new Uint32Array(4),v=new Int32Array(4),y=new J,b=null,x=null,S=[],C=[],w=null;this.domElement=t,this.debug={checkShaderErrors:!0,diagnostics:{keywords:!1},onShaderError:null},this.autoClear=!0,this.autoClearColor=!0,this.autoClearDepth=!0,this.autoClearStencil=!0,this.sortObjects=!0,this.clippingPlanes=[],this.localClippingEnabled=!1,this.toneMapping=0,this.toneMappingExposure=1,this.transmissionResolutionScale=1;let T=this,E=!1,D=null,O=null,ee=null,te=null;this._outputColorSpace=hp;let k=0,ne=0,A=null,re=-1,ie=null,ae=new Dm,oe=new Dm,j=null,se=new Y(0),ce=0,le=t.width,ue=t.height,M=1,N=null,de=null,fe=new Dm(0,0,le,ue),pe=new Dm(0,0,le,ue),me=!1,P=new f_,F=!1,he=!1,ge=new Mm,_e=new J,ve=new Dm,ye={background:null,fog:null,environment:null,overrideMaterial:null,isScene:!0},be=!1;function xe(){return A===null?M:1}let I=n;function Se(e,n){return t.getContext(e,n)}let Ce,we,L,Te,Ee,De,Oe,ke,Ae,je,Me,Ne,Pe,Fe,Ie,Le,Re,ze,Be,Ve,He,Ue,We;try{let e={alpha:!0,depth:r,stencil:i,antialias:o,premultipliedAlpha:s,preserveDrawingBuffer:c,powerPreference:l,failIfMajorPerformanceCaveat:u};if(`setAttribute`in t&&t.setAttribute(`data-engine`,`three.js r186`),t.addEventListener(`webglcontextlost`,qe,!1),t.addEventListener(`webglcontextrestored`,Je,!1),t.addEventListener(`webglcontextcreationerror`,Ye,!1),I===null){let t=`webgl2`;if(I=Se(t,e),I===null)throw Se(t)?Error(`THREE.WebGLRenderer: Error creating WebGL context with your selected attributes.`):Error(`THREE.WebGLRenderer: Error creating WebGL context.`)}Ge()}catch(e){throw t.removeEventListener(`webglcontextlost`,qe,!1),t.removeEventListener(`webglcontextrestored`,Je,!1),t.removeEventListener(`webglcontextcreationerror`,Ye,!1),jp(`WebGLRenderer: `+e.message),e}function Ge(){Ce=new Qb(I),Ce.init(),He=new SC(I,Ce),we=new Db(I,Ce,e,He),L=new bC(I,Ce),we.reversedDepthBuffer&&d&&L.buffers.depth.setReversed(!0),O=I.createFramebuffer(),ee=I.createFramebuffer(),te=I.createFramebuffer(),Te=new tx(I),Ee=new eC,De=new xC(I,Ce,L,Ee,we,He,Te),Oe=new Zb(T),ke=new vb(I),Ue=new Tb(I,ke),Ae=new $b(I,ke,Te,Ue),je=new rx(I,Ae,ke,Ue,Te),ze=new nx(I,we,De),Ie=new Ob(Ee),Me=new $S(T,Oe,Ce,we,Ue,Ie),Ne=new kC(T,Ee),Pe=new iC,Fe=new dC(Ce),Re=new wb(T,Oe,L,je,p,s),Le=new yC(T,je,we),We=new AC(I,Te,we,L),Be=new Eb(I,Ce,Te),Ve=new ex(I,Ce,Te),Te.programs=Me.programs,T.capabilities=we,T.extensions=Ce,T.properties=Ee,T.renderLists=Pe,T.shadowMap=Le,T.state=L,T.info=Te}m!==1009&&(w=new ax(m,t.width,t.height,o,r,i));let Ke=new EC(T,I);this.xr=Ke,this.getContext=function(){return I},this.getContextAttributes=function(){return I.getContextAttributes()},this.forceContextLoss=function(){let e=Ce.get(`WEBGL_lose_context`);e&&e.loseContext()},this.forceContextRestore=function(){let e=Ce.get(`WEBGL_lose_context`);e&&e.restoreContext()},this.getPixelRatio=function(){return M},this.setPixelRatio=function(e){e!==void 0&&(M=e,this.setSize(le,ue,!1))},this.getSize=function(e){return e.set(le,ue)},this.setSize=function(e,n,r=!0){if(Ke.isPresenting){Ap(`WebGLRenderer: Can't change size while VR device is presenting.`);return}le=e,ue=n,t.width=Math.floor(e*M),t.height=Math.floor(n*M),r===!0&&(t.style.width=e+`px`,t.style.height=n+`px`),w!==null&&w.setSize(t.width,t.height),this.setViewport(0,0,e,n)},this.getDrawingBufferSize=function(e){return e.set(le*M,ue*M).floor()},this.setDrawingBufferSize=function(e,n,r){le=e,ue=n,M=r,t.width=Math.floor(e*r),t.height=Math.floor(n*r),this.setViewport(0,0,e,n)},this.setEffects=function(e){if(m===1009){jp(`WebGLRenderer: setEffects() requires outputBufferType set to HalfFloatType or FloatType.`);return}if(e){for(let t=0;t{function n(){if(r.forEach(function(e){let t=Ee.get(e).currentProgram;(t===void 0||t.isReady())&&r.delete(e)}),r.size===0){t(e);return}setTimeout(n,10)}Ce.get(`KHR_parallel_shader_compile`)===null?setTimeout(n,10):n()})};let et=null;function tt(e){et&&et(e)}function nt(){it.stop()}function rt(){it.start()}let it=new _b;it.setAnimationLoop(tt),typeof self<`u`&&it.setContext(self),this.setAnimationLoop=function(e){et=e,Ke.setAnimationLoop(e),e===null?it.stop():it.start()},Ke.addEventListener(`sessionstart`,nt),Ke.addEventListener(`sessionend`,rt),this.render=function(e,t){if(t!==void 0&&t.isCamera!==!0){jp(`WebGLRenderer.render: camera is not an instance of THREE.Camera.`);return}if(E===!0)return;D!==null&&D.renderStart(e,t);let n=Ke.enabled===!0&&Ke.isPresenting===!0,r=w!==null&&(A===null||n)&&w.begin(T,A);if(e.matrixWorldAutoUpdate===!0&&e.updateMatrixWorld(),t.parent===null&&t.matrixWorldAutoUpdate===!0&&t.updateMatrixWorld(),Ke.enabled===!0&&Ke.isPresenting===!0&&(w===null||w.isCompositing()===!1)&&(Ke.cameraAutoUpdate===!0&&Ke.updateCamera(t),t=Ke.getCamera()),e.isScene===!0&&e.onBeforeRender(T,e,t,A),x=Fe.get(e,C.length),x.init(t),x.state.textureUnits=De.getTextureUnits(),C.push(x),ge.multiplyMatrices(t.projectionMatrix,t.matrixWorldInverse),P.setFromProjectionMatrix(ge,Sp,t.reversedDepth),he=this.localClippingEnabled,F=Ie.init(this.clippingPlanes,he),b=Pe.get(e,S.length),b.init(),S.push(b),Ke.enabled===!0&&Ke.isPresenting===!0){let e=T.xr.getDepthSensingMesh();e!==null&&at(e,t,-1/0,T.sortObjects)}at(e,t,0,T.sortObjects),b.finish(),D!==null&&D.updateLights(x.state.lightsArray),T.sortObjects===!0&&b.sort(N,de),be=Ke.enabled===!1||Ke.isPresenting===!1||Ke.hasDepthSensing()===!1,be&&Re.addToRenderList(b,e),this.info.render.frame++,this.info.autoReset===!0&&this.info.reset(),F===!0&&Ie.beginShadows();let i=x.state.shadowsArray;if(Le.render(i,e,t),F===!0&&Ie.endShadows(),(r&&w.hasRenderPass())===!1){let n=b.opaque,r=b.transmissive;if(x.setupLights(),t.isArrayCamera){let i=t.cameras;if(r.length>0)for(let t=0,a=i.length;t0&&st(n,r,e,t),be&&Re.render(e),ot(b,e,t)}A!==null&&ne===0&&(De.updateMultisampleRenderTarget(A),De.updateRenderTargetMipmap(A)),r&&w.end(T),e.isScene===!0&&e.onAfterRender(T,e,t),Ue.resetDefaultState(),re=-1,ie=null,C.pop(),C.length>0?(x=C[C.length-1],De.setTextureUnits(x.state.textureUnits),F===!0&&Ie.setGlobalState(T.clippingPlanes,x.state.camera)):x=null,S.pop(),b=S.length>0?S[S.length-1]:null,D!==null&&D.renderEnd()};function at(e,t,n,r){if(e.visible===!1)return;if(e.layers.test(t.layers)){if(e.isGroup)n=e.renderOrder;else if(e.isLOD)e.autoUpdate===!0&&e.update(t);else if(e.isLightProbeGrid)x.pushLightProbeGrid(e);else if(e.isLight)x.pushLight(e),e.castShadow&&x.pushShadow(e);else if(e.isSprite){if(!e.frustumCulled||e.intersectsFrustum(P)){r&&ve.setFromMatrixPosition(e.matrixWorld).applyMatrix4(ge);let i=je.update(e),a=e.material;a.visible&&b.push(e,i,a,n,ve.z,null,t)}}else if((e.isMesh||e.isLine||e.isPoints)&&(!e.frustumCulled||e.intersectsFrustum(P))){let i=je.update(e),a=e.material;if(r&&(e.boundingSphere===void 0?(i.boundingSphere===null&&i.computeBoundingSphere(),ve.copy(i.boundingSphere.center)):(e.boundingSphere===null&&e.computeBoundingSphere(),ve.copy(e.boundingSphere.center)),ve.applyMatrix4(e.matrixWorld).applyMatrix4(ge)),Array.isArray(a)){let r=i.groups;for(let o=0,s=r.length;o0&&ct(i,t,n),a.length>0&&ct(a,t,n),o.length>0&&ct(o,t,n),L.buffers.depth.setTest(!0),L.buffers.depth.setMask(!0),L.buffers.color.setMask(!0),L.setPolygonOffset(!1)}function st(e,t,n,r){if((n.isScene===!0?n.overrideMaterial:null)!==null)return;if(x.state.transmissionRenderTarget[r.id]===void 0){let e=Ce.has(`EXT_color_buffer_half_float`)||Ce.has(`EXT_color_buffer_float`);x.state.transmissionRenderTarget[r.id]=new km(1,1,{generateMipmaps:!0,type:e?cf:ef,minFilter:$d,samples:Math.max(4,we.samples),stencilBuffer:i,resolveDepthBuffer:!1,resolveStencilBuffer:!1,storeMultisampledDepthBuffer:!1,storeMultisampledStencilBuffer:!1,colorSpace:gm.workingColorSpace})}let a=x.state.transmissionRenderTarget[r.id],o=r.viewport||ae;a.setSize(o.z*T.transmissionResolutionScale,o.w*T.transmissionResolutionScale);let s=T.getRenderTarget(),c=T.getActiveCubeFace(),l=T.getActiveMipmapLevel();T.setRenderTarget(a),T.getClearColor(se),ce=T.getClearAlpha(),ce<1&&T.setClearColor(16777215,.5),T.clear(),be&&Re.render(n);let u=T.toneMapping;T.toneMapping=0;let d=r.viewport;if(r.viewport!==void 0&&(r.viewport=void 0),x.setupLightsView(r),F===!0&&Ie.setGlobalState(T.clippingPlanes,r),ct(e,n,r),De.updateMultisampleRenderTarget(a),De.updateRenderTargetMipmap(a),Ce.has(`WEBGL_multisampled_render_to_texture`)===!1){let e=!1;for(let i=0,a=t.length;i0,r.currentProgram=d,r.uniformsList=null,d}function dt(e){if(e.uniformsList===null){let t=e.currentProgram.getUniforms();e.uniformsList=fS.seqWithValue(t.seq,e.uniforms)}return e.uniformsList}function ft(e,t){let n=Ee.get(e);n.outputColorSpace=t.outputColorSpace,n.batching=t.batching,n.batchingColor=t.batchingColor,n.instancing=t.instancing,n.instancingColor=t.instancingColor,n.instancingMorph=t.instancingMorph,n.skinning=t.skinning,n.morphTargets=t.morphTargets,n.morphNormals=t.morphNormals,n.morphColors=t.morphColors,n.morphTargetsCount=t.morphTargetsCount,n.numClippingPlanes=t.numClippingPlanes,n.numIntersection=t.numClipIntersection,n.vertexAlphas=t.vertexAlphas,n.vertexTangents=t.vertexTangents,n.toneMapping=t.toneMapping}function pt(e,t){if(e.length===0)return null;if(e.length===1)return e[0].texture===null?null:e[0];y.setFromMatrixPosition(t.matrixWorld);for(let t=0,n=e.length;t0),f=!!n.morphAttributes.position,p=!!n.morphAttributes.normal,m=!!n.morphAttributes.color,h=0;r.toneMapped&&(A===null||A.isXRRenderTarget===!0)&&(h=T.toneMapping);let g=n.morphAttributes.position||n.morphAttributes.normal||n.morphAttributes.color,_=g===void 0?0:g.length,v=Ee.get(r),y=x.state.lights;if(F===!0&&(he===!0||e!==ie)){let t=e===ie&&r.id===re;Ie.setState(r,e,t)}let b=!1;r.version===v.__version?v.needsLights&&v.lightsStateVersion!==y.state.version?b=!0:v.outputColorSpace===s?i.isBatchedMesh&&v.batching===!1||!i.isBatchedMesh&&v.batching===!0||i.isBatchedMesh&&v.batchingColor===!0&&i._colorsTexture===null||i.isBatchedMesh&&v.batchingColor===!1&&i._colorsTexture!==null||i.isInstancedMesh&&v.instancing===!1||!i.isInstancedMesh&&v.instancing===!0||i.isSkinnedMesh&&v.skinning===!1||!i.isSkinnedMesh&&v.skinning===!0||i.isInstancedMesh&&v.instancingColor===!0&&i.instanceColor===null||i.isInstancedMesh&&v.instancingColor===!1&&i.instanceColor!==null||i.isInstancedMesh&&v.instancingMorph===!0&&i.morphTexture===null||i.isInstancedMesh&&v.instancingMorph===!1&&i.morphTexture!==null?b=!0:v.envMap===l?r.fog===!0&&v.fog!==a||v.numClippingPlanes!==void 0&&(v.numClippingPlanes!==Ie.numPlanes||v.numIntersection!==Ie.numIntersection)?b=!0:v.vertexAlphas===u&&v.vertexTangents===d&&v.morphTargets===f&&v.morphNormals===p&&v.morphColors===m&&v.toneMapping===h&&v.morphTargetsCount===_?!!v.lightProbeGrid!=x.state.lightProbeGridArray.length>0&&(b=!0):b=!0:b=!0:b=!0:(b=!0,v.__version=r.version);let S=v.currentProgram;b===!0&&(S=ut(r,t,i),D&&r.isNodeMaterial&&D.onUpdateProgram(r,S,v));let C=!1,w=!1,E=!1,O=S.getUniforms(),ee=v.uniforms;if(L.useProgram(S.program)&&(C=!0,w=!0,E=!0),r.id!==re&&(re=r.id,w=!0),v.needsLights){let e=pt(x.state.lightProbeGridArray,i);v.lightProbeGrid!==e&&(v.lightProbeGrid=e,w=!0)}if(C||ie!==e){L.buffers.depth.getReversed()&&e.reversedDepth!==!0&&(e._reversedDepth=!0,e.updateProjectionMatrix()),O.setValue(I,`projectionMatrix`,e.projectionMatrix),O.setValue(I,`viewMatrix`,e.matrixWorldInverse);let t=O.map.cameraPosition;t!==void 0&&t.setValue(I,_e.setFromMatrixPosition(e.matrixWorld)),we.logarithmicDepthBuffer&&O.setValue(I,`logDepthBufFC`,2/(Math.log(e.far+1)/Math.LN2)),(r.isMeshPhongMaterial||r.isMeshToonMaterial||r.isMeshLambertMaterial||r.isMeshBasicMaterial||r.isMeshStandardMaterial||r.isShaderMaterial)&&O.setValue(I,`isOrthographic`,e.isOrthographicCamera===!0),ie!==e&&(ie=e,w=!0,E=!0)}if(v.needsLights&&(y.state.sunShadowMap.length>0&&O.setValue(I,`sunShadowMap`,y.state.sunShadowMap,De),y.state.directionalShadowMap.length>0&&O.setValue(I,`directionalShadowMap`,y.state.directionalShadowMap,De),y.state.spotShadowMap.length>0&&O.setValue(I,`spotShadowMap`,y.state.spotShadowMap,De),y.state.pointShadowMap.length>0&&O.setValue(I,`pointShadowMap`,y.state.pointShadowMap,De)),i.isSkinnedMesh){O.setOptional(I,i,`bindMatrix`),O.setOptional(I,i,`bindMatrixInverse`);let e=i.skeleton;e&&(e.boneTexture===null&&e.computeBoneTexture(),O.setValue(I,`boneTexture`,e.boneTexture,De))}i.isBatchedMesh&&(O.setOptional(I,i,`batchingTexture`),O.setValue(I,`batchingTexture`,i._matricesTexture,De),O.setOptional(I,i,`batchingIdTexture`),O.setValue(I,`batchingIdTexture`,i._indirectTexture,De),O.setOptional(I,i,`batchingColorTexture`),i._colorsTexture!==null&&O.setValue(I,`batchingColorTexture`,i._colorsTexture,De));let te=n.morphAttributes;if((te.position!==void 0||te.normal!==void 0||te.color!==void 0)&&ze.update(i,n,S),(w||v.receiveShadow!==i.receiveShadow)&&(v.receiveShadow=i.receiveShadow,O.setValue(I,`receiveShadow`,i.receiveShadow)),(r.isMeshStandardMaterial||r.isMeshLambertMaterial||r.isMeshPhongMaterial)&&r.envMap===null&&t.environment!==null&&(ee.envMapIntensity.value=t.environmentIntensity),ee.dfgLUT!==void 0&&(ee.dfgLUT.value=NC()),w){if(O.setValue(I,`toneMappingExposure`,T.toneMappingExposure),v.needsLights&&ht(ee,E),a&&r.fog===!0&&Ne.refreshFogUniforms(ee,a),Ne.refreshMaterialUniforms(ee,r,M,ue,x.state.transmissionRenderTarget[e.id]),v.needsLights&&v.lightProbeGrid){let e=v.lightProbeGrid;ee.probesSH.value=e.texture,ee.probesMin.value.copy(e.boundingBox.min),ee.probesMax.value.copy(e.boundingBox.max),ee.probesResolution.value.copy(e.resolution)}fS.upload(I,dt(v),ee,De)}if(r.isShaderMaterial&&r.uniformsNeedUpdate===!0&&(fS.upload(I,dt(v),ee,De),r.uniformsNeedUpdate=!1),r.isSpriteMaterial&&O.setValue(I,`center`,i.center),O.setValue(I,`modelViewMatrix`,i.modelViewMatrix),O.setValue(I,`normalMatrix`,i.normalMatrix),O.setValue(I,`modelMatrix`,i.matrixWorld),r.uniformsGroups!==void 0){let e=r.uniformsGroups;for(let t=0,n=e.length;t0&&De.useMultisampledRTT(e)===!1?Ee.get(e).__webglMultisampledFramebuffer:Array.isArray(c)?c[n]:c,ae.copy(e.viewport),oe.copy(e.scissor),j=e.scissorTest}else ae.copy(fe).multiplyScalar(M).floor(),oe.copy(pe).multiplyScalar(M).floor(),j=me;if(n!==0&&(r=O),L.bindFramebuffer(I.FRAMEBUFFER,r)&&L.drawBuffers(e,r),L.viewport(ae),L.scissor(oe),L.setScissorTest(j),i){let r=Ee.get(e.texture);I.framebufferTexture2D(I.FRAMEBUFFER,I.COLOR_ATTACHMENT0,I.TEXTURE_CUBE_MAP_POSITIVE_X+t,r.__webglTexture,n)}else if(a){let r=t;for(let t=0;t1&&I.readBuffer(I.COLOR_ATTACHMENT0+s);let u=_t(o);if(u.__formatReadable===!1){jp(`WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.`);return}if(u.__typeReadable===!1){jp(`WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.`);return}t>=0&&t<=e.width-r&&n>=0&&n<=e.height-i&&I.readPixels(t,n,r,i,He.convert(c),He.convert(l),a)}finally{let e=A===null?null:Ee.get(A).__webglFramebuffer;L.bindFramebuffer(I.FRAMEBUFFER,e)}}},this.readRenderTargetPixelsAsync=async function(e,t,n,r,i,a,o,s=0){if(!(e&&e.isWebGLRenderTarget))throw Error(`THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.`);let c=Ee.get(e).__webglFramebuffer;if(e.isWebGLCubeRenderTarget&&o!==void 0&&(c=c[o]),c)if(t>=0&&t<=e.width-r&&n>=0&&n<=e.height-i){L.bindFramebuffer(I.FRAMEBUFFER,c);let o=e.textures[s],l=o.format,u=o.type;e.textures.length>1&&I.readBuffer(I.COLOR_ATTACHMENT0+s);let d=_t(o);if(d.__formatReadable===!1)throw Error(`THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in RGBA or implementation defined format.`);if(d.__typeReadable===!1)throw Error(`THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in UnsignedByteType or implementation defined type.`);let f=I.createBuffer();I.bindBuffer(I.PIXEL_PACK_BUFFER,f),I.bufferData(I.PIXEL_PACK_BUFFER,a.byteLength,I.STREAM_READ),I.readPixels(t,n,r,i,He.convert(l),He.convert(u),0),I.bindBuffer(I.PIXEL_PACK_BUFFER,null);let p=A===null?null:Ee.get(A).__webglFramebuffer;L.bindFramebuffer(I.FRAMEBUFFER,p);let m=I.fenceSync(I.SYNC_GPU_COMMANDS_COMPLETE,0);return I.flush(),await Np(I,m,4),I.bindBuffer(I.PIXEL_PACK_BUFFER,f),I.getBufferSubData(I.PIXEL_PACK_BUFFER,0,a),I.bindBuffer(I.PIXEL_PACK_BUFFER,null),I.deleteBuffer(f),I.deleteSync(m),a}else throw Error(`THREE.WebGLRenderer.readRenderTargetPixelsAsync: requested read bounds are out of range.`)},this.copyFramebufferToTexture=function(e,t=null,n=0){let r=2**-n,i=Math.floor(e.image.width*r),a=Math.floor(e.image.height*r),o=t===null?0:t.x,s=t===null?0:t.y;De.setTexture2D(e,0),I.copyTexSubImage2D(I.TEXTURE_2D,n,0,0,o,s,i,a),L.unbindTexture()},this.copyTextureToTexture=function(e,t,n=null,r=null,i=0,a=0){let o,s,c,l,u,d,f,p,m,h=e.isCompressedTexture?e.mipmaps[a]:e.image;if(n!==null)o=n.max.x-n.min.x,s=n.max.y-n.min.y,c=n.isBox3?n.max.z-n.min.z:1,l=n.min.x,u=n.min.y,d=n.isBox3?n.min.z:0;else{let t=2**-i;o=Math.floor(h.width*t),s=Math.floor(h.height*t),c=e.isDataArrayTexture?h.depth:e.isData3DTexture?Math.floor(h.depth*t):1,l=0,u=0,d=0}r===null?(f=0,p=0,m=0):(f=r.x,p=r.y,m=r.z);let g=He.convert(t.format),_=He.convert(t.type),v;t.isData3DTexture?(De.setTexture3D(t,0),v=I.TEXTURE_3D):t.isDataArrayTexture||t.isCompressedArrayTexture?(De.setTexture2DArray(t,0),v=I.TEXTURE_2D_ARRAY):(De.setTexture2D(t,0),v=I.TEXTURE_2D),L.activeTexture(I.TEXTURE0),L.pixelStorei(I.UNPACK_FLIP_Y_WEBGL,t.flipY),L.pixelStorei(I.UNPACK_PREMULTIPLY_ALPHA_WEBGL,t.premultiplyAlpha),L.pixelStorei(I.UNPACK_ALIGNMENT,t.unpackAlignment);let y=L.getParameter(I.UNPACK_ROW_LENGTH),b=L.getParameter(I.UNPACK_IMAGE_HEIGHT),x=L.getParameter(I.UNPACK_SKIP_PIXELS),S=L.getParameter(I.UNPACK_SKIP_ROWS),C=L.getParameter(I.UNPACK_SKIP_IMAGES);L.pixelStorei(I.UNPACK_ROW_LENGTH,h.width),L.pixelStorei(I.UNPACK_IMAGE_HEIGHT,h.height),L.pixelStorei(I.UNPACK_SKIP_PIXELS,l),L.pixelStorei(I.UNPACK_SKIP_ROWS,u),L.pixelStorei(I.UNPACK_SKIP_IMAGES,d);let w=e.isDataArrayTexture||e.isData3DTexture,T=t.isDataArrayTexture||t.isData3DTexture;if(e.isDepthTexture){let n=Ee.get(e),r=Ee.get(t),h=Ee.get(n.__renderTarget),g=Ee.get(r.__renderTarget);L.bindFramebuffer(I.READ_FRAMEBUFFER,h.__webglFramebuffer),L.bindFramebuffer(I.DRAW_FRAMEBUFFER,g.__webglFramebuffer);for(let n=0;n`${e}:${t}`));function Mb(e,t){return jb.has(`${e}:${t}`)}function Nb(e){return e.startup+e.active+Math.floor(e.recovery*Ab/100)}function Pb(e,t,n,r,i){return t&&!i&&Mb(e,n.class)&&r>=Lb(n.class,n.target,n.power)}function Fb(e){return xb[e].combo_window}function Ib(e,t,n){let r=xb[e],i=r.startup,a=r.active,o=r.recovery,s=r.reach,c=r.lateral_arc;if(t===`body`){let e=Sb.body;i+=e.startup_add??0,o+=e.recovery_add??0,s=Math.max(e.reach_min??0,s-(e.reach_sub??0)),c+=e.lateral_arc_add??0}if(n===`power`){let e=Sb.power;i+=e.startup_add??0,a+=e.active_add??0,o+=e.recovery_add??0,s+=e.reach_add??0}return{startup:i,active:a,recovery:o,reach:s,lateralArc:c}}function Lb(e,t,n){let r=xb[e].stamina_cost;return t===`body`&&(r+=Sb.body.stamina_cost_add??0),n===`power`&&(r=Math.floor(r*(Sb.power.stamina_cost_mul_num??1)/(Sb.power.stamina_cost_mul_den??1))),r}function Rb(e){return e.startup+e.active+e.recovery}var zb=7,Bb=70,Vb=1e3,Hb=Ob*2;function Ub(e,t){return Math.max(48,100-Math.floor((Vb-e)/18)-Math.floor(t/35))}function Wb(e,t,n){if(e.action===null||!Mb(e.action,t.class))return!1;let r=e.action_startup_ticks+e.action_active_ticks+e.action_recovery_ticks;return n-e.action_start_tick=Lb(t.class,t.target,t.power)}function qb(e,t){if(e.action===null)return 0;let n=e.action_start_tick+e.action_startup_ticks+e.action_active_ticks+e.action_recovery_ticks;return Math.max(0,n-t)}function Jb(e){return e.queued_actions>0||e.is_downed||e.stunned_ticks>0||e.clinch_ticks>0||e.clinch_startup_ticks>0||e.taunt_ticks>0||e.is_foul_recovery_target||[`slip_left`,`slip_right`,`weave`,`pull`].includes(e.defense)}function Yb(e,t,n={x:0,y:0,speed:0}){let r=Math.max(2,Math.floor(zb*Ub(e.conditioning,e.trauma.body)/100));(t.defense===`guard_high`||t.defense===`guard_low`)&&(r=Math.max(2,Math.floor(r*Bb/100)));let i=Math.hypot(t.moveX,t.moveY),a=i>1e3?1e3/i:1;return n.x=t.moveX*a*r/1e3,n.y=t.moveY*a*r/1e3,n.speed=r,n}function Xb(e,t,n,r){if(n<=0||Jb(e))return{dx:0,dy:0};let i=r===void 0?e.action===null?0:1/0:qb(e,r);if(i>=n)return{dx:0,dy:0};let a=e.velocity_x,o=e.velocity_y,s=0,c=0,l=Math.floor(n),u=n-l;for(let e=0;e=i,r=t(e);a=(a+(n?r.x:0))/2,o=(o+(n?r.y:0))/2;let l=Math.hypot(a,o);l>r.speed&&(a=a/l*r.speed,o=o/l*r.speed),s+=a,c+=o}if(u>0){let e=l>=i,n=t(l);s+=(a+(e?n.x:0))/2*u,c+=(o+(e?n.y:0))/2*u}return{dx:s,dy:c}}var Zb=1e3,Qb=2.5,$b=class{history=[];offset={dx:0,dy:0};mean={x:0,y:0,speed:0};sample={x:0,y:0,speed:0};horizon=null;update(e,t,n,r,i,a,o,s){for(this.history.push({at:r,held:n?{...t,moveX:0,moveY:0}:t});this.history.length>2&&this.history[1].atthis.meanIntent(e,r-(n-1-i)*t,t),n,a)}let l=Qb*zb*s*o,u=c.dx-this.offset.dx,d=c.dy-this.offset.dy,f=Math.hypot(u,d),p=f>l?l/f:1;return this.offset.dx+=u*p,this.offset.dy+=d*p,{dx:this.offset.dx,dy:this.offset.dy}}meanIntent(e,t,n){let r=t-n/2,i=t+n/2,a=this.mean;a.x=0,a.y=0,a.speed=0;for(let t=0;tr?{x:i*(kb-r),y:a*r}:d>n?{x:i*n,y:a*(kb-n)}:{x:i*d,y:a*f}}function tx(e,t,n){let r=e.x+t.dx,i=e.y+t.dy;if(n!==null){let t=Math.min(Hb,Math.hypot(e.x-n.x,e.y-n.y)),a=r-n.x,o=i-n.y,s=Math.hypot(a,o);s1e-9?(r=n.x+a/s*t,i=n.y+o/s*t):(r=e.x,i=e.y))}let a=ex(r,i);return{dx:a.x-e.x,dy:a.y-e.y}}function nx(e){return e===`slip_left`||e===`slip_right`||e===`weave`||e===`pull`}function rx(e,t,n){return e.stunned_ticks>0||e.taunt_ticks>0||e.clinch_ticks>0?e.defense:n===null?nx(e.defense)?e.defense:t.defense:n}var ix=class{kind=null;id=``;pressedAt=0;until=0;sequence=null;started=!1;press(e,t,n,r,i){this.kind=e,this.id=t,this.pressedAt=n,this.until=n+(Math.max(0,r)+10)*1e3/i,this.sequence=null,this.started=!1}acknowledge(e,t,n){if(this.kind===null)return;this.sequence??=n?.(this.id)??null,e.defense===this.kind&&(this.started=!0);let r=!t||e.stunned_ticks>0||e.clinch_ticks>0,i=!this.started&&this.sequence!==null&&e.last_input_sequence>=this.sequence&&e.queued_actions===0;(r||i)&&(this.kind=null)}pose(e){return this.kind!==null&&e>=this.until&&(this.kind=null),this.kind}holdsFeet(e,t){return this.kind!==null&&e()=>(e=Math.imul(e^e>>>15,1|e),e^=e+Math.imul(e^e>>>7,61|e),((e^e>>>14)>>>0)/4294967296);function lx(){let e=new Nd;e.name=`arena`;let t=[],n=[],r=[],i=new sh({color:`#05070c`,roughness:.95});n.push(i);let a=new hm(30,40);t.push(a);let o=new up(a,i);o.rotation.x=-Math.PI/2,o.position.y=-1,o.receiveShadow=!0,e.add(o);let s=cx(20260727),c=[{radius:8.2,y:-.55,count:120,scale:1},{radius:10.6,y:.35,count:150,scale:1.04},{radius:13.2,y:1.35,count:180,scale:1.08},{radius:16,y:2.45,count:210,scale:1.12}],l=c.reduce((e,t)=>e+t.count,0),u=new mm(.19,.5,4,8),d=new Ym(.115,8,7);t.push(u,d);let f=new sh({roughness:.9,metalness:0,emissive:1317158,emissiveIntensity:.7}),p=new sh({roughness:.75,metalness:0,emissive:2759958,emissiveIntensity:.7});n.push(f,p);let m=new Ip(u,f,l),h=new Ip(d,p,l);m.instanceMatrix.setUsage(ql),h.instanceMatrix.setUsage(ql);let g=new ad,_=new Q,v=new Float32Array(l),y=[],b=0;for(let e of c)for(let t=0;t{if(L=Math.max(0,L-t*ax),!n){let n=.05+L*.06,r=.05+L*.2,i=2.3+L*3.2;for(let t=0;t0?.85:0}},excite:e=>{L=Math.min(1,L+Math.max(0,e))},excitement:()=>L,dispose:()=>{for(let e of t)e.dispose();for(let e of n)e.dispose();for(let e of r)e.dispose();m.dispose(),h.dispose()}}}var ux=2.05,dx=5.5,fx=1.05,px=2.5,mx=5.5;function hx(e,t,n){return e<2.5||t<2?null:(n+Math.floor((e-px)/mx))%2}function gx(e){return Math.max(0,e-px)%mx}var _x=.95,vx=2.6,yx=-25;function bx(e,t,n,r,i){let a=e===0?-1:1,o=r*Math.PI/180,s=-a*Math.SQRT1_2,c=a*Math.SQRT1_2,l=s*Math.cos(o)+c*Math.sin(o),u=-s*Math.sin(o)+c*Math.cos(o);return i.set(a*t+l*n,0,-a*t+u*n)}function xx(e,t,n,r,i){bx(e,t,vx,yx+Math.min(1,n/mx)*5,r).setY(1.45),bx(e,t,.62,27,i).setY(1.05)}var Sx=class{current=new Z(0,ux,dx);look=new Z(0,fx,0);swayPhase=0;update(e,t,n,r,i,a,o,s){this.swayPhase+=e;let c=(n.x+r.x)/2,l=(n.z+r.z)/2,u=Pu.clamp(c,-1.4,1.4),d=Pu.clamp(l,-1.1,1.1),f=Pu.clamp(dx+i*.6-(a?.9:0),4.6,8.4),p=ux+i*.08-(a?.45:0),m=s?0:Math.sin(this.swayPhase*.21)*.35,h=s?0:Math.sin(this.swayPhase*.13)*.3,g=u*.32+m,_=f+d*.2,v=p+h*.2;if(this.current.x+=(g-this.current.x)*(1-Math.exp(-2.6*e)),this.current.y+=(v-this.current.y)*(1-Math.exp(-2.6*e)),this.current.z+=(_-this.current.z)*(1-Math.exp(-2.6*e)),this.look.x+=(u-this.look.x)*(1-Math.exp(-3.2*e)),this.look.y+=((a?.7:fx)-this.look.y)*(1-Math.exp(-3.2*e)),this.look.z+=(d*.6-this.look.z)*(1-Math.exp(-3.2*e)),!s&&o>5e-4){let e=t*61;this.current.x+=Math.sin(e*1.31)*o,this.current.y+=Math.cos(e*1.97)*o*.6,this.look.x+=Math.sin(e*1.53)*o*.5,this.look.y+=Math.cos(e*2.11)*o*.35}return{position:this.current,lookAt:this.look}}},Cx=3.05,wx=3.85,Tx=4.05,Ex=3.42,Dx=[.5,.88,1.26];function Ox(e){let t=Cx/Math.max(1,e.ring_half_width),n=Cx/Math.max(1,e.ring_half_height);return{x:e=>e*t,z:e=>-e*n}}var kx={blue:1920728,red:12131356,neutral:15067115},Ax=900,jx=90,Mx=48,Nx=48,Px=2,Fx=4,Ix=24,Lx=16,Rx=.12,zx=.085,Bx=.05,Vx=1/60,Hx=e=>()=>(e=Math.imul(e^e>>>15,1|e),e^=e+Math.imul(e^e>>>7,61|e),((e^e>>>14)>>>0)/4294967296),Ux=(e,t=0)=>Number.isFinite(e)?e:t;function Wx(e){if(typeof e==`number`)return{x:e<0?-1:1,z:0};let t=Math.hypot(e.x,e.z);return Number.isFinite(t)&&t>1e-6?{x:e.x/t,z:e.z/t}:{x:1,z:0}}var Gx=new Z(0,1,0),Kx=e=>Number.isFinite(e)?Pu.clamp(e,0,Bx):0,qx=new Set([`hit`,`counter_hit`,`block`,`perfect_block`,`guard_break`,`knockdown`,`bleed`]);function Jx(e){if(e.kind===`bleed`)return`ooze`;if(e.kind===`knockdown`)return`impact`;let t=e.detail.split(`:`);if(t.at(-1)===`body`)return`body_burst`;let n=t.find(e=>[`jab`,`straight`,`hook`,`uppercut`].includes(e));return n===`hook`?`fan`:n===`uppercut`?`plume`:`jet`}function Yx(e,t){e.set(Ux(t.x),Ux(t.y),Ux(t.z),Ux(t.w,1)),e.lengthSq()<1e-6?e.identity():e.normalize()}function Xx(e){let t=document.createElement(`canvas`);t.width=128,t.height=128;let n=t.getContext(`2d`);if(n!==null){let t=Hx(e);n.clearRect(0,0,128,128),n.fillStyle=`rgba(255,255,255,1)`;let r=7+Math.floor(t()*6);for(let e=0;enew up(e.geometry,e.material))}setBloodLevel(e){if(e===this.bloodLevel)return;let t=this.bloodLevel;this.bloodLevel=e,(e===`off`||t===`full`&&e===`reduced`)&&this.clearArcadeGore()}get shakeAmount(){return this.shake}get liveParticles(){return this.droplets.filter(e=>e.alive).length}get liveBloodParticles(){return this.droplets.filter(e=>e.alive&&e.blood).length}get liveMist(){return this.mists.filter(e=>e.alive).length}get visibleDecals(){return this.decals.filter(e=>e.visible).length}get liveGibs(){return this.gibs.filter(e=>e.alive).length}get activeHeads(){return this.heads.filter(e=>e.active).length}severedHeadPosition(e,t){let n=this.heads[Math.trunc(e)];return n===void 0||!n.active?!1:(t.copy(n.mesh.position),!0)}get activeHands(){return this.hands.filter(e=>e.active).length}get activeStumps(){return[...this.stumps,...this.handStumps].filter(e=>e.active).length}spawnDroplet(e,t,n,r,i,a,o,s,c,l=c?.006+this.ambientRandom()*.009:.004+this.ambientRandom()*.004){let u=this.liveDroplets1e-4?(this.dropletVelocity.set(t.vx/n,t.vy/n,t.vz/n),this.dropletQuaternion.setFromUnitVectors(Gx,this.dropletVelocity)):this.dropletQuaternion.identity();let i=Math.min(1,t.life/(t.maxLife*.3));this.dropletScale.set(t.radius*i,t.radius*r*i,t.radius*i),this.dropletMatrix.compose(this.dropletVelocity.set(t.x,t.y,t.z),this.dropletQuaternion,this.dropletScale),this.dropletMesh.setMatrixAt(e,this.dropletMatrix)}spawnMist(e,t,n,r,i){let a=this.mistIndex%jx;this.mistIndex+=1;let o=this.mists[a];o.alive=!0,o.x=Ux(e),o.y=Ux(t,1),o.z=Ux(n),o.maxLife=Math.max(.05,Ux(i,.5)),o.life=o.maxLife,o.scale=Math.max(.1,Ux(r,1)),this.mistPositions[a*3]=o.x,this.mistPositions[a*3+1]=o.y,this.mistPositions[a*3+2]=o.z,this.mistColors[a*3]=.6*o.scale,this.mistColors[a*3+1]=.07*o.scale,this.mistColors[a*3+2]=.08*o.scale,this.mistGeometry.attributes.position.needsUpdate=!0,this.mistGeometry.attributes.color.needsUpdate=!0}placeDecal(e,t,n,r,i,a,o){let s=this.decals[this.decalIndex%Mx];this.decalIndex+=1,s.visible=!0,s.position.x=Ux(e),s.position.z=Ux(t),s.rotation.z=Ux(i),s.scale.set(Math.max(.01,Ux(n,.2))*1.4,Math.max(.01,Ux(r,.15))*1.4,1);let c=s.material;c.opacity=Pu.clamp(Ux(a,.4)*1.6,0,1),c.color.setHex(o)}ambientRandom(){return this.ambientSeed=Math.imul(this.ambientSeed^this.ambientSeed>>>15,1|this.ambientSeed),this.ambientSeed^=this.ambientSeed+Math.imul(this.ambientSeed^this.ambientSeed>>>7,61|this.ambientSeed),((this.ambientSeed^this.ambientSeed>>>14)>>>0)/4294967296}splatter(e,t,n,r=()=>this.ambientRandom()){if(this.bloodLevel===`off`)return;let i=this.bloodLevel===`reduced`,a=i?3:12,o=i?.35:1;for(let s=0;sthis.ambientRandom()){if(this.bloodLevel===`off`)return;let i=this.bloodLevel===`reduced`,a=i?.35:1;if(this.placeDecal(e,t,2.8*n*a,2*n*a,r()*Math.PI,.56*a,4523529),!i)for(let n=0;n<3;n+=1){let n=r()*Math.PI*2,i=.18+r()*.32;this.placeDecal(e+Math.sin(n)*i,t+Math.cos(n)*i,.45+r()*.55,.22+r()*.38,r()*Math.PI,.38+r()*.18,6425104)}}addEvent(e,t,n,r){if(!qx.has(e.kind))return;let i=Hx(e.event_id*7919+17),a=e.kind===`block`||e.kind===`perfect_block`,o=Jx(e),s=Wx(r??e.direction),c=r===void 0?Math.abs(Ux(e.direction)):1,l={x:Ux(t.x),y:e.detail.endsWith(`:body`)?1.05:1.58,z:Ux(t.z)},u=n?0:Math.round((a?6:16)+Math.min(20,Math.max(0,e.amount)/20)),d=n||e.blood<=0||this.bloodLevel===`off`?0:this.bloodLevel===`reduced`?Math.min(24,Math.round(e.blood*.24)):Math.min(120,Math.round(e.blood*1.1));this.shake=Math.min(.09,this.shake+(a?.008:Math.max(.012,e.amount/2600))),e.kind===`knockdown`&&(this.shake=Math.min(.14,this.shake+.06));for(let e=0;e0&&this.bloodLevel!==`off`&&(e.amount>190||e.kind===`knockdown`||e.kind===`counter_hit`)){let t=this.bloodLevel===`reduced`?e.kind===`knockdown`?3:2:e.kind===`knockdown`?14:10,n=this.bloodLevel===`reduced`?.35:1;for(let e=0;e6&&this.bloodLevel!==`off`&&this.splatter(l.x+(i()-.5)*.6,l.z+(i()-.5)*.6,1+Math.min(2,e.blood/55),i),this.bloodLevel===`full`&&!n&&e.blood>=40&&[`hit`,`counter_hit`,`knockdown`].includes(e.kind)){let t=Math.min(12,Math.max(4,Math.round(e.blood/9)));for(let e=0;e=Px)return;let s=Math.trunc(e),c=Number.isSafeInteger(i)?i:0,l=this.heads[s];if(l.active||this.lastDecapitationEvent[s]===c)return;this.applyBakedPart(l,o,a);let u=Hx(c*104729+s*8191+23),d=Wx(r);this.lastDecapitationEvent[s]=c,l.active=!0,l.moving=!0,l.eventId=c;let f=1.8+u()*1.1;l.vy=2.3+u()*1.1;let p=(u()-.5)*2.2;l.vx=f*d.x-p*d.z,l.vz=f*d.z+p*d.x,l.vrx=(u()-.5)*12,l.vry=(u()-.5)*12,l.vrz=(u()-.5)*12,l.bounces=0,l.stained=!1,l.mesh.position.set(Ux(t.x),Ux(t.y,1.55),Ux(t.z)),Yx(l.mesh.quaternion,n),l.mesh.visible=!0;let m=this.stumps[s];m.active=!0,m.fountainLife=1.25,m.accumulator=0,m.seed=c|1,m.direction=d,m.mesh.position.copy(l.mesh.position),m.mesh.quaternion.copy(l.mesh.quaternion),m.mesh.visible=!0;for(let e=0;e=Px)return;let c=Math.trunc(e)*2+(t===`left`?0:1),l=Number.isSafeInteger(a)?a:0,u=this.hands[c];if(u.active||this.lastDismembermentEvent[c]===l)return;this.applyBakedPart(u,s,o);let d=Hx(l*130363+c*12289+37),f=Wx(i);this.lastDismembermentEvent[c]=l,u.active=!0,u.moving=!0,u.eventId=l;let p=1.3+d()*1.1;u.vy=1.5+d()*1.2;let m=(d()-.5)*1.8;u.vx=p*f.x-m*f.z,u.vz=p*f.z+m*f.x,u.vrx=(d()-.5)*16,u.vry=(d()-.5)*16,u.vrz=(d()-.5)*16,u.bounces=0,u.stained=!1,u.mesh.position.set(Ux(n.x),Ux(n.y,1.25),Ux(n.z)),Yx(u.mesh.quaternion,r),u.mesh.visible=!0;let h=this.handStumps[c];h.active=!0,h.fountainLife=.9,h.accumulator=0,h.seed=l|1,h.direction=f,h.mesh.position.copy(u.mesh.position),h.mesh.quaternion.copy(u.mesh.quaternion),h.mesh.visible=!0;for(let e=0;e=Px)return;let i=Math.trunc(e)*2+(t===`left`?0:1),a=this.handStumps[i];a.active&&(a.mesh.position.set(Ux(n.x),Ux(n.y,1.2),Ux(n.z)),Yx(a.mesh.quaternion,r))}anchorStump(e,t,n){if(this.bloodLevel!==`full`||e<0||e>=Px)return;let r=this.stumps[Math.trunc(e)];r.active&&(r.mesh.position.set(Ux(t.x),Ux(t.y,1.5),Ux(t.z)),Yx(r.mesh.quaternion,n))}restoreFighter(e){if(e<0||e>=Px)return;let t=Math.trunc(e),n=this.heads[t];n.active=!1,n.moving=!1,n.mesh.visible=!1,n.mesh.position.y=-50,this.applyBakedPart(n,void 0,9063730);let r=this.stumps[t];r.active=!1,r.fountainLife=0,r.accumulator=0,r.mesh.visible=!1,r.mesh.position.y=-50;for(let e of[t*2,t*2+1]){let t=this.hands[e];t.active=!1,t.moving=!1,t.mesh.visible=!1,t.mesh.position.y=-50,this.applyBakedPart(t,void 0,1920728);let n=this.handStumps[e];n.active=!1,n.fountainLife=0,n.accumulator=0,n.mesh.visible=!1,n.mesh.position.y=-50}}drip(e,t,n,r=0){let i=+(r===1);if(this.bloodLevel===`off`||n||t<=.05){this.stopDrip(i);return}let a=this.dripEmitters[i];a.position.set(Ux(e.x),Ux(e.y,1.5),Ux(e.z)),a.rate=this.bloodLevel===`reduced`?Math.min(3,t*3):Math.min(18,t*14),a.active=!0}stopDrip(e){let t=this.dripEmitters[+(e===1)];t.active=!1,t.rate=0,t.accumulator=0}updateDripEmitters(e){for(let t of this.dripEmitters)if(t.active)for(t.accumulator+=e*t.rate;t.accumulator>=1;)--t.accumulator,this.spawnDroplet(t.position.x+(this.ambientRandom()-.5)*.14,t.position.y-.04,t.position.z+(this.ambientRandom()-.5)*.1,(this.ambientRandom()-.5)*.08,-.25-this.ambientRandom()*.4,(this.ambientRandom()-.5)*.08,{r:.5,g:.04,b:.06},1.1,!0)}spawnTeeth(e,t,n,r){if(this.bloodLevel===`off`)return;let i=Hx((Number.isSafeInteger(r)?r:0)*7331+91),a=Wx(t),o=Math.min(4,Math.max(1,Math.round(n*(this.bloodLevel===`reduced`?.5:1))));for(let t=0;t.35?(r.bounces+=1,r.vy=Math.abs(r.vy)*.3,r.vx*=.68,r.vz*=.68,r.vrx*=.7,r.vry*=.7,r.vrz*=.7):r.tooth?(r.vx=0,r.vy=0,r.vz=0,r.vrx=r.vry=r.vrz=0):r.alive=!1),this.writeGibMatrix(n,r)}t&&(this.gibMesh.instanceMatrix.needsUpdate=!0)}confineGib(e){Math.abs(e.x)>3.05&&(e.x=Math.sign(e.x)*Cx,e.vx*=-.45),Math.abs(e.z)>3.05&&(e.z=Math.sign(e.z)*Cx,e.vz*=-.45)}updateDetachedParts(e,t){for(let n of e)if(!(!n.active||!n.moving)){if(n.vy-=5.8*t,n.mesh.position.x+=n.vx*t,n.mesh.position.y+=n.vy*t,n.mesh.position.z+=n.vz*t,n.mesh.rotation.x+=n.vrx*t,n.mesh.rotation.y+=n.vry*t,n.mesh.rotation.z+=n.vrz*t,Math.abs(n.mesh.position.x)>3.05&&(n.mesh.position.x=Math.sign(n.mesh.position.x)*Cx,n.vx*=-.42),Math.abs(n.mesh.position.z)>3.05&&(n.mesh.position.z=Math.sign(n.mesh.position.z)*Cx,n.vz*=-.42),n.mesh.position.y<=0+n.radius){if(n.mesh.position.y=0+n.radius,!n.stained&&this.bloodLevel===`full`){n.stained=!0;let e=n.radius===Rx?1:.55;this.placeDecal(n.mesh.position.x,n.mesh.position.z,1.3*e,.9*e,n.mesh.rotation.z,.58,4392199)}n.bounces<2&&Math.abs(n.vy)>.42?(n.bounces+=1,n.vy=Math.abs(n.vy)*.32,n.vx*=.62,n.vz*=.62,n.vrx*=.65,n.vry*=.65,n.vrz*=.65):(n.moving=!1,n.vx=0,n.vy=0,n.vz=0,n.vrx=0,n.vry=0,n.vrz=0)}(!Number.isFinite(n.mesh.position.x)||!Number.isFinite(n.mesh.position.y)||!Number.isFinite(n.mesh.position.z)||!Number.isFinite(n.mesh.quaternion.x)||!Number.isFinite(n.mesh.quaternion.y)||!Number.isFinite(n.mesh.quaternion.z)||!Number.isFinite(n.mesh.quaternion.w))&&(n.mesh.position.set(0,0+n.radius,0),n.mesh.quaternion.identity(),n.moving=!1,n.vx=0,n.vy=0,n.vz=0)}}stumpRandom(e){let t=e.seed|0;return t=Math.imul(t^t>>>15,1|t),t^=t+Math.imul(t^t>>>7,61|t),e.seed=t|0,((t^t>>>14)>>>0)/4294967296}updateStumpFountains(e){if(this.bloodLevel===`full`){for(let t of[...this.stumps,...this.handStumps])if(!(!t.active||t.fountainLife<=0))for(t.fountainLife=Math.max(0,t.fountainLife-e),t.accumulator+=e*36;t.accumulator>=1;){--t.accumulator;let e=this.stumpRandom(t),n=this.stumpRandom(t),r=this.stumpRandom(t);this.sprayDroplet(t.direction,t.mesh.position.x+(e-.5)*.08,t.mesh.position.y+.015,t.mesh.position.z+(n-.5)*.08,.35+e*.55,1.3+n*1.45,(r-.5)*.85,{r:.58,g:.025,b:.045},.65+r*.5,!0)}}}update(e){for(this.simulationRemainder+=Kx(e);this.simulationRemainder+1e-9>=Vx;)this.simulationRemainder=Math.max(0,this.simulationRemainder-Vx),this.simulate(Vx)}simulate(e){this.shake*=.02**e,this.updateDripEmitters(e),this.updateStumpFountains(e),this.updateDetachedParts(this.heads,e),this.updateDetachedParts(this.hands,e),this.updateGibs(e);let t=this.liveDroplets,n=!1;for(let t=0;t0&&(this.dropletBuffers.position.needsUpdate=!0,this.dropletBuffers.color.needsUpdate=!0,this.uploadDroplets(0,this.liveDroplets,n));let r=!1;for(let[t,n]of this.mists.entries()){if(!n.alive)continue;if(r=!0,n.life-=e,n.life<=0){n.alive=!1,this.mistPositions[t*3+1]=-50;continue}let i=1-n.life/n.maxLife,a=Math.max(0,1-i)*.7*n.scale;this.mistPositions[t*3]=n.x,this.mistPositions[t*3+1]=n.y+i*.14,this.mistPositions[t*3+2]=n.z,this.mistColors[t*3]=a,this.mistColors[t*3+1]=a*.1,this.mistColors[t*3+2]=a*.12}r&&(this.mistGeometry.attributes.position.needsUpdate=!0,this.mistGeometry.attributes.color.needsUpdate=!0)}clearDroplets(e){let t=this.liveDroplets;for(let t=0;t=2.0 are supported.`));return}let c=new $S(i,{path:t||this.resourcePath||``,crossOrigin:this.crossOrigin,requestHeader:this.requestHeader,manager:this.manager,ktx2Loader:this.ktx2Loader,meshoptDecoder:this.meshoptDecoder});c.fileLoader.setRequestHeader(this.requestHeader);for(let e=0;e=0&&o[t]===void 0&&console.warn(`THREE.GLTFLoader: Unknown extension "`+t+`".`)}}c.setExtensions(a),c.setPlugins(o),c.parse(n,r)}parseAsync(e,t){let n=this;return new Promise(function(r,i){n.parse(e,t,r,i)})}};function rS(){let e={};return{get:function(t){return e[t]},add:function(t,n){e[t]=n},remove:function(t){delete e[t]},removeAll:function(){e={}}}}function iS(e,t,n){let r=e.json.materials[t];return r.extensions&&r.extensions[n]?r.extensions[n]:null}var aS={KHR_BINARY_GLTF:`KHR_binary_glTF`,KHR_DRACO_MESH_COMPRESSION:`KHR_draco_mesh_compression`,KHR_LIGHTS_PUNCTUAL:`KHR_lights_punctual`,KHR_MATERIALS_CLEARCOAT:`KHR_materials_clearcoat`,KHR_MATERIALS_DISPERSION:`KHR_materials_dispersion`,KHR_MATERIALS_IOR:`KHR_materials_ior`,KHR_MATERIALS_SHEEN:`KHR_materials_sheen`,KHR_MATERIALS_SPECULAR:`KHR_materials_specular`,KHR_MATERIALS_TRANSMISSION:`KHR_materials_transmission`,KHR_MATERIALS_IRIDESCENCE:`KHR_materials_iridescence`,KHR_MATERIALS_ANISOTROPY:`KHR_materials_anisotropy`,KHR_MATERIALS_UNLIT:`KHR_materials_unlit`,KHR_MATERIALS_VOLUME:`KHR_materials_volume`,KHR_TEXTURE_BASISU:`KHR_texture_basisu`,KHR_TEXTURE_TRANSFORM:`KHR_texture_transform`,KHR_MESH_QUANTIZATION:`KHR_mesh_quantization`,KHR_MATERIALS_EMISSIVE_STRENGTH:`KHR_materials_emissive_strength`,EXT_MATERIALS_BUMP:`EXT_materials_bump`,EXT_TEXTURE_WEBP:`EXT_texture_webp`,EXT_TEXTURE_AVIF:`EXT_texture_avif`,EXT_MESHOPT_COMPRESSION:`EXT_meshopt_compression`,KHR_MESHOPT_COMPRESSION:`KHR_meshopt_compression`,EXT_MESH_GPU_INSTANCING:`EXT_mesh_gpu_instancing`},oS=class{constructor(e){this.parser=e,this.name=aS.KHR_LIGHTS_PUNCTUAL,this.cache={refs:{},uses:{}}}_markDefs(){let e=this.parser,t=this.parser.json.nodes||[];for(let n=0,r=t.length;n=0)throw Error(`THREE.GLTFLoader: setKTX2Loader must be called before loading KTX2 textures`);return null}return t.loadTextureImage(e,i.source,a)}},bS=class{constructor(e){this.parser=e,this.name=aS.EXT_TEXTURE_WEBP}loadTexture(e){let t=this.name,n=this.parser,r=n.json,i=r.textures[e];if(!i.extensions||!i.extensions[t])return null;let a=i.extensions[t],o=r.images[a.source],s=n.textureLoader;if(o.uri){let e=n.options.manager.getHandler(o.uri);e!==null&&(s=e)}return n.loadTextureImage(e,a.source,s)}},xS=class{constructor(e){this.parser=e,this.name=aS.EXT_TEXTURE_AVIF}loadTexture(e){let t=this.name,n=this.parser,r=n.json,i=r.textures[e];if(!i.extensions||!i.extensions[t])return null;let a=i.extensions[t],o=r.images[a.source],s=n.textureLoader;if(o.uri){let e=n.options.manager.getHandler(o.uri);e!==null&&(s=e)}return n.loadTextureImage(e,a.source,s)}},SS=class{constructor(e,t){this.name=t,this.parser=e}loadBufferView(e){let t=this.parser.json,n=t.bufferViews[e];if(n.extensions&&n.extensions[this.name]){let e=n.extensions[this.name],r=this.parser.getDependency(`buffer`,e.buffer),i=this.parser.options.meshoptDecoder;if(!i||!i.supported){if(t.extensionsRequired&&t.extensionsRequired.indexOf(this.name)>=0)throw Error(`THREE.GLTFLoader: setMeshoptDecoder must be called before loading compressed files`);return null}return r.then(function(t){let n=e.byteOffset||0,r=e.byteLength||0,a=e.count,o=e.byteStride,s=new Uint8Array(t,n,r);return i.decodeGltfBufferAsync?i.decodeGltfBufferAsync(a,o,s,e.mode,e.filter).then(function(e){return e.buffer}):i.ready.then(function(){let t=new ArrayBuffer(a*o);return i.decodeGltfBuffer(new Uint8Array(t),a,o,s,e.mode,e.filter),t})})}else return null}},CS=class{constructor(e){this.name=aS.EXT_MESH_GPU_INSTANCING,this.parser=e}createNodeMesh(e){let t=this.parser.json,n=t.nodes[e];if(!n.extensions||!n.extensions[this.name]||n.mesh===void 0)return null;let r=t.meshes[n.mesh];for(let e of r.primitives)if(e.mode!==PS.TRIANGLES&&e.mode!==PS.TRIANGLE_STRIP&&e.mode!==PS.TRIANGLE_FAN&&e.mode!==void 0)return null;let i=n.extensions[this.name].attributes,a=[],o={};for(let e in i)a.push(this.parser.getDependency(`accessor`,i[e]).then(t=>(o[e]=t,o[e])));return a.length<1?null:(a.push(this.parser.createNodeMesh(e)),Promise.all(a).then(e=>{let t=e.pop(),n=t.isGroup?t.children:[t],r=e[0].count,i=[];for(let e of n){let t=new ad,n=new Z,a=new Fu,s=new Z(1,1,1),c=new Ip(e.geometry,e.material,r);for(let e=0;e0||e.search(/^data\:image\/jpeg/)===0?`image/jpeg`:e.search(/\.webp($|\?)/i)>0||e.search(/^data\:image\/webp/)===0?`image/webp`:e.search(/\.ktx2($|\?)/i)>0||e.search(/^data\:image\/ktx2/)===0?`image/ktx2`:`image/png`}var QS=new ad,$S=class{constructor(e={},t={}){this.json=e,this.extensions={},this.plugins={},this.options=t,this.cache=new rS,this.associations=new Map,this.primitiveCache={},this.nodeCache={},this.meshCache={refs:{},uses:{}},this.cameraCache={refs:{},uses:{}},this.lightCache={refs:{},uses:{}},this.sourceCache={},this.textureCache={},this.nodeNamesUsed={};let n=!1,r=-1,i=!1,a=-1;if(typeof navigator<`u`&&navigator.userAgent!==void 0){let e=navigator.userAgent;n=/^((?!chrome|android).)*safari/i.test(e)===!0;let t=e.match(/Version\/(\d+)/);r=n&&t?parseInt(t[1],10):-1,i=e.indexOf(`Firefox`)>-1,a=i?e.match(/Firefox\/([0-9]+)\./)[1]:-1}typeof createImageBitmap>`u`||n&&r<17||i&&a<98?this.textureLoader=new Gh(this.options.manager):this.textureLoader=new hg(this.options.manager),this.textureLoader.setCrossOrigin(this.options.crossOrigin),this.textureLoader.setRequestHeader(this.options.requestHeader),this.fileLoader=new Hh(this.options.manager),this.fileLoader.setResponseType(`arraybuffer`),this.options.crossOrigin===`use-credentials`&&this.fileLoader.setWithCredentials(!0)}setExtensions(e){this.extensions=e}setPlugins(e){this.plugins=e}parse(e,t){let n=this,r=this.json,i=this.extensions;this.cache.removeAll(),this.nodeCache={},this._invokeAll(function(e){return e._markDefs&&e._markDefs()}),Promise.all(this._invokeAll(function(e){return e.beforeRoot&&e.beforeRoot()})).then(function(){return Promise.all([n.getDependencies(`scene`),n.getDependencies(`animation`),n.getDependencies(`camera`)])}).then(function(t){let a={scene:t[0][r.scene||0],scenes:t[0],animations:t[1],cameras:t[2],asset:r.asset,parser:n,userData:{}};return WS(i,a,r),GS(a,r),Promise.all(n._invokeAll(function(e){return e.afterRoot&&e.afterRoot(a)})).then(function(){for(let e of a.scenes)e.updateMatrixWorld();e(a)})}).catch(t)}_markDefs(){let e=this.json.nodes||[],t=this.json.skins||[],n=this.json.meshes||[];for(let n=0,r=t.length;n{let n=this.associations.get(e);n!=null&&this.associations.set(t,n);for(let[n,r]of e.children.entries())i(r,t.children[n])};return i(n,r),r.name+=`_instance_`+e.uses[t]++,r}_invokeOne(e){let t=Object.values(this.plugins);t.push(this);for(let n=0;n=2&&p.setY(t,u[e*a+1]),a>=3&&p.setZ(t,u[e*a+2]),a>=4&&p.setW(t,u[e*a+3]),a>=5)throw Error(`THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.`)}p.normalized=d}return p})}loadTexture(e){let t=this.json,n=this.options,r=t.textures[e].source,i=t.images[r],a=this.textureLoader;if(i.uri){let e=n.manager.getHandler(i.uri);e!==null&&(a=e)}return this.loadTextureImage(e,r,a)}loadTextureImage(e,t,n){let r=this,i=this.json,a=i.textures[e],o=i.images[t],s=(o.uri||o.bufferView)+`:`+a.sampler;if(this.textureCache[s])return this.textureCache[s];let c=this.loadImageSource(t,n).then(function(t){t.flipY=!1,t.name=a.name||o.name||``,t.name===``&&typeof o.uri==`string`&&o.uri.startsWith(`data:image/`)===!1&&(t.name=o.uri);let n=(i.samplers||{})[a.sampler]||{};return t.magFilter=IS[n.magFilter]||1006,t.minFilter=IS[n.minFilter]||1008,t.wrapS=LS[n.wrapS]||1e3,t.wrapT=LS[n.wrapT]||1e3,t.generateMipmaps=!t.isCompressedTexture&&t.minFilter!==1003&&t.minFilter!==1006,r.associations.set(t,{textures:e}),t}).catch(function(){return null});return this.textureCache[s]=c,c}loadImageSource(e,t){let n=this,r=this.json,i=this.options;if(this.sourceCache[e]!==void 0)return this.sourceCache[e].then(e=>e.clone());let a=r.images[e],o=self.URL||self.webkitURL,s=a.uri||``,c=!1;if(a.bufferView!==void 0)s=n.getDependency(`bufferView`,a.bufferView).then(function(e){c=!0;let t=new Blob([e],{type:a.mimeType});return s=o.createObjectURL(t),s});else if(a.uri===void 0)throw Error(`THREE.GLTFLoader: Image `+e+` is missing URI and bufferView`);let l=Promise.resolve(s).then(function(e){return new Promise(function(n,r){let a=n;t.isImageBitmapLoader===!0&&(a=function(e){let t=new $u(e);t.needsUpdate=!0,n(t)}),t.load(pg.resolveURL(e,i.path),a,void 0,r)})}).then(function(e){return c===!0&&o.revokeObjectURL(s),GS(e,a),e.userData.mimeType=a.mimeType||ZS(a.uri),e}).catch(function(e){throw console.error(`THREE.GLTFLoader: Couldn't load texture`,s),e});return this.sourceCache[e]=l,l}assignTexture(e,t,n,r){let i=this;return this.getDependency(`texture`,n.index).then(function(a){if(!a)return null;if(n.texCoord!==void 0&&n.texCoord>0&&(a=a.clone(),a.channel=n.texCoord),i.extensions[aS.KHR_TEXTURE_TRANSFORM]){let e=n.extensions===void 0?void 0:n.extensions[aS.KHR_TEXTURE_TRANSFORM];if(e){let t=i.associations.get(a);a=i.extensions[aS.KHR_TEXTURE_TRANSFORM].extendTexture(a,e),i.associations.set(a,t)}}return r!==void 0&&(a.colorSpace=r),e[t]=a,a})}assignFinalMaterial(e){let t=e.geometry,n=e.material,r=t.attributes.tangent===void 0,i=t.attributes.color!==void 0,a=t.attributes.normal===void 0;if(e.isPoints){let e=`PointsMaterial:`+n.uuid,t=this.cache.get(e);t||(t=new tm,Kf.prototype.copy.call(t,n),t.color.copy(n.color),t.map=n.map,t.sizeAttenuation=!1,this.cache.add(e,t)),n=t}else if(e.isLine){let e=`LineBasicMaterial:`+n.uuid,t=this.cache.get(e);t||(t=new Vp,Kf.prototype.copy.call(t,n),t.color.copy(n.color),t.map=n.map,this.cache.add(e,t)),n=t}if(r||i||a){let e=`ClonedMaterial:`+n.uuid+`:`;r&&(e+=`derivative-tangents:`),i&&(e+=`vertex-colors:`),a&&(e+=`flat-shading:`);let t=this.cache.get(e);t||(t=n.clone(),i&&(t.vertexColors=!0),a&&(t.flatShading=!0),r&&(t.normalScale&&(t.normalScale.y*=-1),t.clearcoatNormalScale&&(t.clearcoatNormalScale.y*=-1)),this.cache.add(e,t),this.associations.set(t,this.associations.get(n))),n=t}e.material=n}getMaterialType(){return sh}loadMaterial(e){let t=this,n=this.json,r=this.extensions,i=n.materials[e],a,o={},s=i.extensions||{},c=[];if(s[aS.KHR_MATERIALS_UNLIT]){let e=r[aS.KHR_MATERIALS_UNLIT];a=e.getMaterialType(),c.push(e.extendParams(o,i,t))}else{let n=i.pbrMetallicRoughness||{};if(o.color=new Q(1,1,1),o.opacity=1,Array.isArray(n.baseColorFactor)){let e=n.baseColorFactor;o.color.setRGB(e[0],e[1],e[2],Hl),o.opacity=e[3]}n.baseColorTexture!==void 0&&c.push(t.assignTexture(o,`map`,n.baseColorTexture,Vl)),o.metalness=n.metallicFactor===void 0?1:n.metallicFactor,o.roughness=n.roughnessFactor===void 0?1:n.roughnessFactor,n.metallicRoughnessTexture!==void 0&&(c.push(t.assignTexture(o,`metalnessMap`,n.metallicRoughnessTexture)),c.push(t.assignTexture(o,`roughnessMap`,n.metallicRoughnessTexture))),a=this._invokeOne(function(t){return t.getMaterialType&&t.getMaterialType(e)}),c.push(Promise.all(this._invokeAll(function(t){return t.extendMaterialParams&&t.extendMaterialParams(e,o)})))}i.doubleSided===!0&&(o.side=2);let l=i.alphaMode||HS.OPAQUE;if(l===HS.BLEND?(o.transparent=!0,o.depthWrite=!1):(o.transparent=!1,l===HS.MASK&&(o.alphaTest=i.alphaCutoff===void 0?.5:i.alphaCutoff)),i.normalTexture!==void 0&&a!==Qf&&(c.push(t.assignTexture(o,`normalMap`,i.normalTexture)),o.normalScale=new X(1,1),i.normalTexture.scale!==void 0)){let e=i.normalTexture.scale;o.normalScale.set(e,e)}if(i.occlusionTexture!==void 0&&a!==Qf&&(c.push(t.assignTexture(o,`aoMap`,i.occlusionTexture)),i.occlusionTexture.strength!==void 0&&(o.aoMapIntensity=i.occlusionTexture.strength)),i.emissiveFactor!==void 0&&a!==Qf){let e=i.emissiveFactor;o.emissive=new Q().setRGB(e[0],e[1],e[2],Hl)}return i.emissiveTexture!==void 0&&a!==Qf&&c.push(t.assignTexture(o,`emissiveMap`,i.emissiveTexture,Vl)),Promise.all(c).then(function(){let n=new a(o);return i.name&&(n.name=i.name),GS(n,i),t.associations.set(n,{materials:e}),i.extensions&&WS(r,n,i),n})}createUniqueName(e){let t=Ng.sanitizeNodeName(e||``);return t in this.nodeNamesUsed?t+`_`+ ++this.nodeNamesUsed[t]:(this.nodeNamesUsed[t]=0,t)}loadGeometries(e){let t=this,n=this.extensions,r=this.primitiveCache;function i(e){return n[aS.KHR_DRACO_MESH_COMPRESSION].decodePrimitive(e,t).then(function(n){return tC(n,e,t)})}let a=[];for(let n=0,o=e.length;n$x(e,1)):o.mode===PS.TRIANGLE_FAN&&(e=e.then(e=>$x(e,2))),r[s]={primitive:o,promise:e},a.push(e)}}return Promise.all(a)}loadMesh(e){let t=this,n=this.json,r=this.extensions,i=n.meshes[e],a=i.primitives,o=[];for(let e=0,t=a.length;e0&&qS(d,i),d.name=t.createUniqueName(i.name||`mesh_`+e),GS(d,i),u.extensions&&WS(r,d,u),t.assignFinalMaterial(d),c.push(d)}for(let n=0,r=c.length;n0){let e=t.userData.pivot,r=n[0];t.pivot=new Z().fromArray(e),t.position.x-=e[0],t.position.y-=e[1],t.position.z-=e[2],r.position.set(0,0,0),delete t.userData.pivot}return t})}_loadNodeShallow(e){let t=this.json,n=this.extensions,r=this;if(this.nodeCache[e]!==void 0)return this.nodeCache[e];let i=t.nodes[e],a=i.name?r.createUniqueName(i.name):``,o=[],s=r._invokeOne(function(t){return t.createNodeMesh&&t.createNodeMesh(e)});return s&&o.push(s),i.camera!==void 0&&o.push(r.getDependency(`camera`,i.camera).then(function(e){return r._getNodeRef(r.cameraCache,i.camera,e)})),r._invokeAll(function(t){return t.createNodeAttachment&&t.createNodeAttachment(e)}).forEach(function(e){o.push(e)}),this.nodeCache[e]=Promise.all(o).then(function(t){let o;if(o=i.isBone===!0?new Cp:t.length>1?new Nd:t.length===1?t[0]:new Md,o!==t[0])for(let e=0,n=t.length;e1){let e=r.associations.get(o);r.associations.set(o,{...e})}return r.associations.get(o).nodes=e,o}),this.nodeCache[e]}loadScene(e){let t=this.extensions,n=this.json.scenes[e],r=this,i=new Nd;n.name&&(i.name=r.createUniqueName(n.name)),GS(i,n),n.extensions&&WS(t,i,n);let a=n.nodes||[],o=[];for(let e=0,t=a.length;e{let t=new Map;for(let[e,n]of r.associations)(e instanceof Kf||e instanceof $u)&&t.set(e,n);return e.traverse(e=>{let n=r.associations.get(e);n!=null&&t.set(e,n)}),t})(i),i})}_createAnimationTracks(e,t,n,r,i){let a=[],o=e.name?e.name:e.uuid,s=[];function c(e){e.morphTargetInfluences&&s.push(e.name?e.name:e.uuid)}BS[i.path]===BS.weights?(c(e),e.isGroup&&e.children.forEach(c)):s.push(o);let l;switch(BS[i.path]){case BS.weights:l=Oh;break;case BS.rotation:l=Ah;break;case BS.translation:case BS.scale:l=Mh;break;default:switch(n.itemSize){case 1:l=Oh;break;default:l=Mh;break}break}let u=r.interpolation===void 0?Nl:VS[r.interpolation],d=this._getArrayFromAccessor(n);for(let e=0,n=s.length;e0||e.is_downed||e.clinch_ticks>0||e.clinch_startup_ticks>0||e.taunt_ticks>0||e.is_foul_recovery_target||[`slip_left`,`slip_right`,`weave`,`pull`].includes(e.defense)}function iw(e,t,n={x:0,y:0,speed:0},r=0){let i=Math.max(2,Math.floor(YC*$C(e.conditioning,e.trauma.body)/100)),a=e.stunned_ticks-r,o=i;a>1?o=Math.max(2,Math.floor(i*O/100)):a<=0&&(t.defense===`guard_high`||t.defense===`guard_low`)&&(o=Math.max(2,Math.floor(i*XC/100))),o*=oe(e.style).moveSpeedPercent/100;let s=Math.hypot(t.moveX,t.moveY),c=s>1e3?1e3/s:1;return n.x=t.moveX*c*o/1e3,n.y=t.moveY*c*o/1e3,n.speed=o,n}function aw(e,t,n){return e.kind===`parry`&&e.target_id===t?Math.max(n,e.tick+e.amount):n}function ow(e,t,n){let r=e.stunned_ticks>0||e.action_id!==null&&e.action_id===n.cutActionId?0:Math.max(0,nw(e,t)-1),i=e.stunned_ticks>0?Math.min((n.rootedUntil??0)-t,e.stunned_ticks)-1:0;return Math.max(r,i)}function sw(e,t,n,r,i={}){if(n<=0||rw(e))return{dx:0,dy:0};let a=r===void 0?e.action===null?0:1/0:ow(e,r,i);if(a>=n)return{dx:0,dy:0};let o=e.velocity_x,s=e.velocity_y,c=0,l=0,u=Math.floor(n),d=n-u;for(let e=0;e=a,r=t(e);o=(o+(n?r.x:0))/2,s=(s+(n?r.y:0))/2;let i=Math.hypot(o,s);i>r.speed&&(o=o/i*r.speed,s=s/i*r.speed),c+=o,l+=s}if(d>0){let e=u>=a,n=t(u);c+=(o+(e?n.x:0))/2*d,l+=(s+(e?n.y:0))/2*d}return{dx:c,dy:l}}var cw=1e3,lw=2.5,uw=class{history=[];offset={dx:0,dy:0};mean={x:0,y:0,speed:0};sample={x:0,y:0,speed:0};horizon=null;holds={rootedUntil:0,cutActionId:null};observe(e,t){this.holds.rootedUntil=aw(e,t,this.holds.rootedUntil)}update(e,t,n,r,i,a,o,s){for(this.history.push({at:r,held:n?{...t,moveX:0,moveY:0}:t});this.history.length>2&&this.history[1].at0&&e.action_id!==null&&(this.holds.cutActionId=e.action_id),e!==null){let t=1e3/s,n=this.horizon;c=sw(e,i=>this.meanIntent(e,i,r-(n-1-i)*t,t),n,a,this.holds)}let l=lw*YC*s*o,u=c.dx-this.offset.dx,d=c.dy-this.offset.dy,f=Math.hypot(u,d),p=f>l?l/f:1;return this.offset.dx+=u*p,this.offset.dy+=d*p,{dx:this.offset.dx,dy:this.offset.dy}}meanIntent(e,t,n,r){let i=n-r/2,a=n+r/2,o=this.mean;o.x=0,o.y=0,o.speed=0;for(let n=0;nr?{x:i*(m-r),y:a*r}:h>n?{x:i*n,y:a*(m-n)}:{x:i*h,y:a*g}}function fw(e,t,n){let r=e.x+t.dx,i=e.y+t.dy;if(n!==null){let t=Math.min(QC,Math.hypot(e.x-n.x,e.y-n.y)),a=r-n.x,o=i-n.y,s=Math.hypot(a,o);s1e-9?(r=n.x+a/s*t,i=n.y+o/s*t):(r=e.x,i=e.y))}let a=dw(r,i);return{dx:a.x-e.x,dy:a.y-e.y}}function pw(e){return e===`slip_left`||e===`slip_right`||e===`weave`||e===`pull`}function mw(e,t,n){return e.stunned_ticks>0||e.taunt_ticks>0||e.clinch_ticks>0?e.defense:n===null?pw(e.defense)?e.defense:t.defense:n}var hw=class{kind=null;id=``;pressedAt=0;until=0;ticks=10;sequence=null;started=!1;press(e,t,n,r,i,a=10){this.kind=e,this.id=t,this.pressedAt=n,this.ticks=a,this.until=n+(Math.max(0,r)+a)*1e3/i,this.sequence=null,this.started=!1}acknowledge(e,t,n){if(this.kind===null)return;this.sequence??=n?.(this.id)??null,e.defense===this.kind&&(this.started=!0);let r=!t||e.stunned_ticks>0||e.clinch_ticks>0,i=!this.started&&this.sequence!==null&&e.last_input_sequence>=this.sequence&&e.queued_actions===0;(r||i)&&(this.kind=null)}pose(e){return this.kind!==null&&e>=this.until&&(this.kind=null),this.kind}holdsFeet(e,t){return this.kind!==null&&e{let r=new e.array.constructor(e.count*e.itemSize);l[n][t]=new e.constructor(r,e.itemSize,e.normalized)}))}let p=t*.5,m=10**Math.log10(1/t),h=p*m;for(let t=0;te*t,z:e=>-e*n}}var Dw={blue:1920728,red:12131356,neutral:15067115},Ow=[{radius:8.2,y:-.55,count:120,scale:1},{radius:10.6,y:.35,count:150,scale:1.04},{radius:13.2,y:1.35,count:180,scale:1.08},{radius:16,y:2.45,count:210,scale:1.12}],kw=.95,Aw=.42,jw=[3819104,5915960,3493980,6963002,4612159,5524835,7233348,9076852,4678266,7620714,10107450,11843778,3104688,11573822,2303534,2303534],Mw=[13081462,9067067,7225640,14726287,5517341,11105359],Nw=[1380364,2759442,4862496,7035464,9341572,11569742],Pw=.9,Fw=.235,Iw=-.35,Lw=-2.7,Rw=.32,zw=.26,Bw=6.2,Vw=.3;function Hw(e,t,n=Bw){return t>0&&Math.hypot(e,t)>n&&Math.abs(Math.atan2(e,t))<.3}var Uw=` +#ifdef USE_INSTANCING + vec4 crowdWorld = modelMatrix * instanceMatrix * vec4(transformed, 1.0); +#else + vec4 crowdWorld = modelMatrix * vec4(transformed, 1.0); +#endif +vCrowdLight = vec3(-crowdWorld.x, ${7.5.toFixed(1)} - crowdWorld.y, -crowdWorld.z); +`,Ww=` +#ifdef USE_COLOR + totalEmissiveRadiance *= vColor.rgb * (0.22 + 0.78 * max(0.0, dot(inverseTransformDirection(normal, viewMatrix), normalize(vCrowdLight)))); +#endif +`,Gw=(e,t,n)=>{let r=Math.min(1,Math.max(0,(n-e)/(t-e)));return r*r*(3-2*r)};function Kw(e,t){let n=[];for(let[r,i]of e.entries())for(let e=0;e0&&Math.abs(Math.atan2(s,c))<.3})}return n}function qw(e,t,n,r={rise:0,armLeft:0,armRight:0,sway:0}){let i=.2+.7*(1-e.keen),a=Gw(i-.12,i+.12,n),o=Math.abs(Math.sin(t*(2.3+n*3.2)+e.phase*1.7))*(.02+.1*a),s=Math.sin(t*6.2+e.phase)*.32*a,c=Iw+(Lw-Iw)*a;return r.rise=a*Rw+o,r.armLeft=c+s,r.armRight=c-s*.8,r.sway=Math.sin(t*1.6+e.phase)*(.04+n*.05),r}function Jw(e){let t=gw(e,!1);for(let t of e)t.dispose();if(t===null)throw Error(`crowd geometry could not be merged`);return t}function Yw(){let e=new B_(.16,.28,2,8);e.scale(1.35,1,.8),e.translate(0,.64,0);let t=new B_(.078,.3,1,5);t.rotateZ(Math.PI/2),t.translate(0,Pw-.01,0);let n=new B_(.11,.26,1,5);return n.rotateX(Math.PI/2),n.scale(1.7,1,1),n.translate(0,.4,.17),Jw([e,t,n])}function Xw(){let e=new Sv(.105,10,6);e.rotateY(-Math.PI/2),e.scale(.92,1.12,1),e.translate(0,1.12,.01);let t=new H_(.048,.056,.1,5,1,!0);t.translate(0,.99,0);let n=t.getAttribute(`uv`);for(let e=0;e{e.vertexShader=e.vertexShader.replace(`#include `,`#include +varying vec3 vCrowdLight;`).replace(`#include `,`#include \n${Uw}`),e.fragmentShader=e.fragmentShader.replace(`#include `,`#include +varying vec3 vCrowdLight;`).replace(`#include `,`#include \n${Ww}`)},e.customProgramCacheKey=()=>`hands-crowd`;let u=new Pv({color:724501,roughness:.96,metalness:0,side:2}),d=new c_(o[0],s,i),f=new c_(o[1],c,i),p=new c_(o[2],l,i),m=new c_(o[3],s,i),h=new c_(o[3],s,i),g=[d,f,p,m,h],_=[d,f,p];for(let e of g)e.instanceMatrix.setUsage(xp),e.frustumCulled=!1;let v=new Y;for(let t=0;t.12&&v.setHex(Nw[Math.floor(e()*Nw.length)]).multiplyScalar((.8+e()*.4)*n),p.setColorAt(t,v)}for(let e of g)e.instanceColor.needsUpdate=!0;let y=new Ig(o[4],u);y.name=`stands`,y.receiveShadow=!1,n.add(y,...g);let b=new J(0,1,0),x=new J,S=new cm,C=new J,w=new Mm,T=new Mm,E=new Mm,D=new Mm,O={rise:0,armLeft:0,armRight:0,sway:0},ee=(e,t,n,r)=>{E.makeTranslation(n*Fw,Pw,0),D.makeRotationX(r),T.multiplyMatrices(w,E).multiply(D),e.setMatrixAt(t,T)},te=0,k=!1,ne=!1,A=!1,re=0,ie=0,ae=r.flatMap((e,t)=>e.platform?[t]:[]),oe=new Mm().makeScale(0,0,0),j=(e,t)=>{let n=r[e];if(A&&n.platform){for(let n of t?g:_)n.setMatrixAt(e,oe);return}qw(n,re,ie,O),S.setFromAxisAngle(b,n.yaw+O.sway),w.compose(x.set(n.x,n.y+O.rise*n.scale,n.z),S,C.setScalar(n.scale)),d.setMatrixAt(e,w),f.setMatrixAt(e,w),p.setMatrixAt(e,w),t&&(ee(m,e,1,O.armLeft),ee(h,e,-1,O.armRight))},se=(e,t,n=!1)=>{let r=n||ne;ne=!1;let a=r?1:k?4:2,o=Math.ceil(i/a),s=te%a*o,c=Math.min(i,s+o);te+=1,re=e,ie=t;let l=!k||r;for(let e=s;e{e!==k&&(k=e,m.visible=!e,h.visible=!e,e||(ne=!0))},makeRoomForCamera:e=>{let t=Hw(e.x,e.z,A?Bw-Vw:Bw);if(t!==A){A=t;for(let e of ae)j(e,!0);for(let e of g)e.instanceMatrix.clearUpdateRanges(),e.instanceMatrix.needsUpdate=!0}},dispose:()=>{for(let e of o)e.dispose();for(let e of[s,c,l,u])e.dispose();a.dispose();for(let e of g)e.dispose()}}}var nT=.3,rT=.012,iT=e=>()=>(e=Math.imul(e^e>>>15,1|e),e^=e+Math.imul(e^e>>>7,61|e),((e^e>>>14)>>>0)/4294967296);function aT(){let e=new oh;e.name=`arena`;let t=[],n=[],r=[],i=new Pv({color:`#05070c`,roughness:.95});n.push(i);let a=new V_(30,40);t.push(a);let o=new Ig(a,i);o.rotation.x=-Math.PI/2,o.position.y=-1,o.receiveShadow=!0,e.add(o);let s=iT(20260727),c=Ow,l=tT(s);e.add(l.group);let u=document.createElement(`canvas`);u.width=1024,u.height=64;let d=u.getContext(`2d`);d!==null&&(d.fillStyle=`#070c1a`,d.fillRect(0,0,1024,64),d.fillStyle=`#13203f`,d.fillRect(0,0,1024,5),d.fillRect(0,59,1024,5),d.textBaseline=`middle`,d.font=`800 38px Inter, system-ui, sans-serif`,d.fillStyle=`#e9c46a`,d.fillText(`H A N D S`,28,33),d.fillStyle=`#c9d6f2`,d.font=`600 26px Inter, system-ui, sans-serif`,d.fillText(`CHAMPIONSHIP BOXING`,300,34),d.fillStyle=`#e9c46a`,d.font=`800 30px Inter, system-ui, sans-serif`,d.fillText(`FIGHT NIGHT`,700,34),d.fillStyle=`#b91c1c`,d.fillRect(948,22,22,22),d.fillStyle=`#1d4ed8`,d.fillRect(978,22,22,22));let f=new F_(u);f.colorSpace=hp,f.wrapS=Gd,f.repeat.set(-6,1),r.push(f);let p=new wg({map:f,side:1});n.push(p);let m=c[0].y+Aw,h=new H_(c[0].radius-kw-.03,c[0].radius-kw-.03,m- -1-.04,72,1,!0);t.push(h);let g=new Ig(h,p);g.name=`boards`,g.position.y=(m+-1)/2+.02,e.add(g);let _=new Float32Array(270);for(let e=0;e<90;e+=1){let t=c[Math.floor(s()*c.length)],n=s()*Math.PI*2;_[e*3]=Math.sin(n)*t.radius,_[e*3+1]=t.y+.9+s()*.5,_[e*3+2]=Math.cos(n)*t.radius}let v=new lg;v.setAttribute(`position`,new Jh(_,3)),t.push(v);let y=new D_({color:13623551,size:.09,transparent:!0,opacity:0,blending:2,depthWrite:!1});n.push(y);let b=new M_(v,y);e.add(b);let x=new Pv({color:`#11151d`,roughness:.6,metalness:.4});n.push(x);let S=new Pv({color:`#1a2230`,emissive:`#dfe9ff`,emissiveIntensity:1.1,roughness:.4});n.push(S);let C=new wg({color:`#a8c4ff`,transparent:!0,opacity:.006,blending:2,depthWrite:!1,side:2});n.push(C);let w=new z_(9,.18,.18);t.push(w);let T=new H_(.09,.14,.22,10);t.push(T);let E=new U_(1.7,6.6,20,1,!0);t.push(E);for(let t=0;t<2;t+=1){let n=new Ig(w,x);n.position.set(0,7.6,t===0?-1.6:1.6),e.add(n);for(let t=0;t<5;t+=1){let r=-3.6+t*1.8,i=new Ig(T,S);i.position.set(r,7.45,n.position.z),e.add(i);let a=new Ig(E,C);a.position.set(r,4.1,n.position.z*.4),e.add(a)}}let D=new Pv({color:`#070a12`,roughness:1});n.push(D);let O=new H_(24,24,14,32,1,!0);t.push(O);let ee=new Ig(O,D);ee.position.y=5,ee.material.side=1,e.add(ee);let te=document.createElement(`canvas`);te.width=512,te.height=256;let k=te.getContext(`2d`);k!==null&&(k.fillStyle=`#04060c`,k.fillRect(0,0,512,256),k.strokeStyle=`#f1cc72`,k.lineWidth=6,k.strokeRect(14,14,484,228),k.fillStyle=`#f1cc72`,k.font=`800 84px Inter, system-ui, sans-serif`,k.textAlign=`center`,k.textBaseline=`middle`,k.fillText(`H A N D S`,256,104),k.fillStyle=`#8fa3c8`,k.font=`600 30px Inter, system-ui, sans-serif`,k.fillText(`CHAMPIONSHIP BOXING`,256,186));let ne=new F_(te);ne.colorSpace=hp;let A=new wg({map:ne});n.push(A);let re=new z_(4.6,2.3,.3);t.push(re);let ie=new Ig(re,A);ie.position.set(0,6.4,-14.5),e.add(ie);let ae=new Ig(re,x);ae.position.set(0,6.4,-14.65),e.add(ae),r.push(ne);let oe=0,j=0,se=0,ce=!1;return{group:e,update:(e,t,n)=>{se=Math.max(0,se-t*nT),n&&!ce&&l.update(0,0,!0),ce=n,n||(l.update(e,se),f.offset.x=e*rT%1,oe-=t,oe<=0&&(j=.09+Math.random()*.08,oe=(.25+Math.random()*1.6)/(1+se*4)),j=Math.max(0,j-t),y.opacity=j>0?.85:0)},excite:e=>{se=Math.min(1,se+Math.max(0,e))},excitement:()=>se,setLowTier:l.setLowTier,makeRoomForCamera:l.makeRoomForCamera,dispose:()=>{for(let e of t)e.dispose();for(let e of n)e.dispose();for(let e of r)e.dispose();l.dispose()}}}var oT=2.05,sT=5.5,cT=1.05,lT=1.78,uT=3.75,dT=.85,fT=.45,pT=1.18,mT=2.5,hT=5.5;function gT(e,t,n){return e<2.5||t<3?null:(n+Math.floor((e-mT)/hT))%2}function _T(e){return Math.max(0,e-mT)%hT}var vT=.95,yT=2.6,bT=-25;function xT(e,t,n,r,i){let a=e===0?-1:1,o=r*Math.PI/180,s=-a*Math.SQRT1_2,c=a*Math.SQRT1_2,l=s*Math.cos(o)+c*Math.sin(o),u=-s*Math.sin(o)+c*Math.cos(o);return i.set(a*t+l*n,0,-a*t+u*n)}function ST(e,t,n,r,i){xT(e,t,yT,bT+Math.min(1,n/hT)*5,r).setY(1.45),xT(e,t,.62,27,i).setY(1.05)}var CT={top:2.42,bottom:.95,width:2.4};function wT(e,t,n,r=.09){let i=Math.max(.2,1-n-r),a=Math.max((CT.top-CT.bottom)/i,CT.width/Math.max(.1,e)),o=(CT.top+CT.bottom)/2;return{distance:a/(2*Math.tan(K.degToRad(t)/2)),height:o-(.5-(r+i/2))*a}}var TT=class{current=new J(0,oT,sT);look=new J(0,cT,0);swayPhase=0;update(e,t,n,r,i,a,o,s,c=!1,l=null){this.swayPhase+=e;let u=a&&l!==null?dT:0,d=(n.x+r.x)/2+((l?.x??0)-(n.x+r.x)/2)*u,f=(n.z+r.z)/2+((l?.z??0)-(n.z+r.z)/2)*u,p=K.clamp(d,-1.4,1.4),m=K.clamp(f,-1.1,1.1),h=c?K.clamp(uT+i*.45-(a?.6:0),3.3,5.6):K.clamp(sT+i*.6-(a?.9:0),4.6,8.4),g=(c?lT:oT)+i*.08-(a?.45:0),_=s?0:Math.sin(this.swayPhase*.21)*.35,v=s?0:Math.sin(this.swayPhase*.13)*.3,y=p*.32+_,b=h+m*.2,x=g+v*.2;if(this.current.x+=(y-this.current.x)*(1-Math.exp(-2.6*e)),this.current.y+=(x-this.current.y)*(1-Math.exp(-2.6*e)),this.current.z+=(b-this.current.z)*(1-Math.exp(-2.6*e)),this.look.x+=(p-this.look.x)*(1-Math.exp(-3.2*e)),this.look.y+=((a?u>0?fT:.7:c?pT:cT)-this.look.y)*(1-Math.exp(-3.2*e)),this.look.z+=(m*.6-this.look.z)*(1-Math.exp(-3.2*e)),!s&&o>5e-4){let e=t*61;this.current.x+=Math.sin(e*1.31)*o,this.current.y+=Math.cos(e*1.97)*o*.6,this.look.x+=Math.sin(e*1.53)*o*.5,this.look.y+=Math.cos(e*2.11)*o*.35}return{position:this.current,lookAt:this.look}}},ET=2.45,DT=2.25,OT=1.35,kT=3.5,AT=1.25,jT=.45;function MT(e,t,n,r,i,a,o=31){let s=K.degToRad(o),c=Math.cos(s)*ET,l=Math.sin(s)*ET,u=-r,d=n,f=-n*c+u*l,p=-r*c+d*l,m=(e,t)=>Math.abs(t)<1e-9?1:K.clamp((Math.sign(t)*kT-e)/t,0,1),h=Math.min(m(e.x,f),m(e.z,p));i.set(K.clamp(e.x+f*h,-3.5,kT),DT,K.clamp(e.z+p*h,-3.5,kT)),a.set(t.x,OT,t.z)}var NT=class{angle=Math.PI;closeness=0;initialised=!1;target=new J;position=new J;look=new J;aim=new J;get forwardX(){return Math.sin(this.angle)}get forwardZ(){return Math.cos(this.angle)}reset(){this.initialised=!1}update(e,t,n,r,i,a,o=16/9){let s=r.x-n.x,c=r.z-n.z,l=Math.hypot(s,c),u=l>.05?Math.atan2(s,c):this.angle,d=K.clamp((1.3-l)/.8,0,1);if(!this.initialised)this.angle=u,this.closeness=d;else{let t=Math.atan2(Math.sin(u-this.angle),Math.cos(u-this.angle));this.angle+=t*(1-Math.exp(-4.5*e)),this.closeness+=(d-this.closeness)*(1-Math.exp(-3*e))}let f=K.clamp(o,jT,1);if(MT(n,r,this.forwardX,this.forwardZ,this.target,this.aim,K.lerp(31,60,this.closeness)*f),!this.initialised)this.position.copy(this.target),this.look.copy(this.aim),this.initialised=!0;else{let t=1-Math.exp(-10*e);this.position.lerp(this.target,t),this.look.lerp(this.aim,t)}let p={position:this.position,lookAt:this.look,tight:!0};if(a||i<=5e-4)return p;let m=t*61;return this.target.set(this.position.x+Math.sin(m*1.31)*i,this.position.y+Math.cos(m*1.97)*i*.6,this.position.z),this.aim.set(this.look.x+Math.sin(m*1.53)*i*.5,this.look.y+Math.cos(m*2.11)*i*.35,this.look.z),{position:this.target,lookAt:this.aim,tight:!0}}},PT=54,FT=136,IT=110,LT=168,RT=74,zT=72,BT={right:220,wideTop:64,compactTop:120,height:34},VT=200,HT={reach:192,bottom:80},UT=`rgba(3,6,12,0.86)`,WT=`rgb(4,7,13)`,GT={play:`#f6d57a`,colour:`#9ec7ff`,announcer:`#f6d57a`},KT=[`#4f86d9`,`#d9483c`],qT=`Inter, system-ui, sans-serif`;function JT(e){let{width:t,height:n}=e,r=t<640,i=n<480,a=i?.78:r?.85:1,o=Math.min(680,t-24),s=e.resultTop,c=s===null&&e.touch&&e.phase!==`complete`,l=qt(t,n,c);if(c&&e.paused===!0&&e.phase===`countdown`)return null;if(s===null&&e.phase===`countdown`){let e=l.x+l.width/2,r=t-(c&&t>n?Gt(t,n,l.y+l.height):12);return{x:e,y:l.y,maxWidth:Math.min(o,2*Math.min(e-12,r-e)),anchor:`top`,scale:a,cover:{width:l.width,height:l.height}}}if(s===null&&!e.touch&&!i){let r=n-(e.hint?LT:IT),i=e.cornerPanelTop===void 0||e.cornerPanelTop===null?r:Math.min(r,e.cornerPanelTop-8);if(i-RT*a>=n*.45)return{x:t/2,y:i,maxWidth:o,anchor:`bottom`,scale:a,cover:null}}if(s===null&&e.viewerDown)return null;let u=s===null&&r?FT:PT;s===null&&(e.phase===`rest`||e.phase===`foul_recovery`||c&&e.paused===!0?u=Math.max(u,l.y+l.height+8):e.phase===`knockdown`&&(u=Math.max(u,cn(n,r)+zT)),e.replay&&t/2-o/2d)return null;let f=c&&e.lag===!0&&!r&&!e.replay&&uMath.round(8*e)+Math.round(10*e)+Math.round(4*e)+2*Math.round(Math.round(17*e)*1.3)+Math.round(5*e);function XT(e,t,n,r){let i=r?1:t.opacity;if(i<=.01)return;let a=r||n.cover!==null?0:t.entering*8*(n.anchor===`bottom`?1:-1);e.save(),e.globalAlpha=i,e.textBaseline=`alphabetic`;let o=i*i;t.line.card===null?$T(e,t.line,n,a,o):tE(e,t.line.card,n,a,o),e.restore()}function ZT(e,t,n){if(e.measureText(t).width<=n)return t;let r=t;for(;r.length>1&&e.measureText(`${r}…`).width>n;)r=r.slice(0,-1);return`${r}…`}function QT(e,t){let n=e.length/2,r=null,i=1/0;for(let a=e.indexOf(` `);a>=0;a=e.indexOf(` `,a+1)){let o=e.slice(0,a),s=e.slice(a+1),c=Math.abs(a-n);t(o)&&t(s)&&c0?[e.slice(0,a),e.slice(a+1)]:[e,``]}function $T(e,t,n,r,i){let a=n.scale,o=Math.round(14*a),s=Math.round(10*a),c=n.maxWidth-o*2-4,l=(t,n)=>(e.font=`700 ${n}px ${qT}`,e.measureText(t).width),u=Tt(e=>l(t.text,e),c,Math.round(17*a),Math.round(13*a)),d=l(t.text,u)<=c?[t.text]:QT(t.text,e=>l(e,u)<=c).filter(e=>e.length>0),f=Math.max(...d.map(e=>Math.min(c,l(e,u)))),p=Xu[t.speaker];e.font=`800 ${s}px ${qT}`;let m=e.measureText(p).width,h=Math.min(n.maxWidth,Math.max(f,m)+o*2+4),g=Math.round(u*1.3),_=Math.round(8*a)+s,v=_+Math.round(4*a)+d.length*g+Math.round(5*a),y=(n.anchor===`bottom`?n.y-v:n.y)+r,b=n.x-h/2,x=GT[t.speaker];e.fillStyle=UT,e.fillRect(b,y,h,v),e.fillStyle=x,e.fillRect(b,y,4,v),e.globalAlpha=i,e.textAlign=`left`,e.font=`800 ${s}px ${qT}`,e.fillText(p,b+4+o,y+_),e.fillStyle=`#f6f7fb`,e.font=`700 ${u}px ${qT}`,d.forEach((t,n)=>e.fillText(ZT(e,t,c),b+4+o,y+_+Math.round(4*a)+g*(n+1)-Math.round(u*.28)))}function eE(e,t,n){let r=Math.round(11*e),i=Math.round(28*e),a=Math.round(6*e),o={pad:Math.round(10*e),gap:Math.round(7*e)},s={pad:Math.max(4,Math.round(6*e)),gap:Math.max(3,Math.round(4*e))},c=t?Math.round(10*e):0,l=[{...o,detail:c},{...s,detail:c},{...s,detail:0}],u=(e,t)=>e.pad*2+r+e.gap+t+(e.detail===0?0:a+e.detail);for(let e of l)if(n===null||u(e,i)<=n)return{kicker:r,title:i,...e,height:u(e,i)};let d=l[2],f=Math.max(12,Math.min(i,(n??1/0)-u(d,0)));return{kicker:r,title:f,...d,height:u(d,f)}}function tE(e,t,n,r,i){let a=n.scale,o=Math.round(18*a),s=n.maxWidth-o*2,c=eE(a,t.detail.length>0,n.cover?.height??null),l=c.kicker,u=c.detail,d=(t,n,r)=>(e.font=`${n} ${r}px ${qT}`,e.measureText(t).width),f=Tt(e=>d(t.title,900,e),s,c.title,Math.min(c.title,Math.round(15*a))),p=Math.max(d(t.kicker,800,l),Math.min(s,d(t.title,900,f)),u===0?0:d(t.detail,700,u)),m=Math.min(n.maxWidth,Math.max(240*a,p+o*2,n.cover?.width??0)),h=c.pad+l+c.gap+f+(u===0?0:Math.round(6*a)+u)+c.pad,g=Math.max(h,n.cover?.height??0),_=(n.anchor===`bottom`?n.y-g:n.y)+r,v=n.x-m/2,y=t.corner===null?GT.announcer:KT[t.corner];e.fillStyle=n.cover===null?UT:WT,e.fillRect(v,_,m,g),e.fillStyle=y,e.fillRect(v,_,m,3),e.globalAlpha=i,e.textAlign=`center`;let b=_+(g-h)/2+c.pad+l;e.font=`800 ${l}px ${qT}`,e.fillText(ZT(e,t.kicker,s),n.x,b),b+=c.gap+f,e.fillStyle=`#ffffff`,e.font=`900 ${f}px ${qT}`,e.fillText(ZT(e,t.title,s),n.x,b-Math.round(f*.12)),u>0&&(b+=Math.round(6*a)+u,e.fillStyle=`#93a3bd`,e.font=`700 ${u}px ${qT}`,e.fillText(ZT(e,t.detail,s),n.x,b))}var nE=128,rE=4,iE=.12,aE=.82,oE=e=>()=>(e=Math.imul(e^e>>>15,1|e),e^=e+Math.imul(e^e>>>7,61|e),((e^e>>>14)>>>0)/4294967296);function sE(e){let t=oE(e);return{lobes:Array.from({length:5+Math.floor(t()*4)},(e,n)=>{let r=t()*Math.PI*2,i=n===0?0:6+t()*12,a=n===0?16+t()*6:8+t()*9;return{x:64+Math.cos(r)*i,y:64+Math.sin(r)*i,rx:a,ry:a*(.55+t()*.45),turn:t()*Math.PI}}),fill:.55+t()*.2,rim:.95,satellites:6+Math.floor(t()*6)}}function cE(e){let t=document.createElement(`canvas`);t.width=nE,t.height=nE;let n=t.getContext(`2d`);if(n!==null){let t=sE(e),r=oE(e^1540483477);n.clearRect(0,0,nE,nE),n.fillStyle=`rgba(255,255,255,1)`,n.strokeStyle=`rgba(255,255,255,1)`,n.globalAlpha=t.rim,n.lineWidth=3.5;for(let e of t.lobes)n.beginPath(),n.ellipse(e.x,e.y,e.rx,e.ry,e.turn,0,Math.PI*2),n.stroke();let i=()=>{n.beginPath();for(let e of t.lobes){let t=Math.max(1,e.rx-1.5);n.moveTo(e.x+Math.cos(e.turn)*t,e.y+Math.sin(e.turn)*t),n.ellipse(e.x,e.y,t,Math.max(1,e.ry-1.5),e.turn,0,Math.PI*2)}n.fill()};n.globalCompositeOperation=`destination-out`,n.globalAlpha=1,i(),n.globalCompositeOperation=`source-over`,n.globalAlpha=t.fill,i();for(let e=0;e{let t=t=>Math.round((e>>t&255)*.8);return`rgb(${t(16)},${t(8)},${t(0)})`},uE=class{scene;size;mesh;canvas;context;texture;source;uploader=null;region=new cb;regionAt=new q;material;geometry;stamps=[];scratch;scratchContext;pixelsPerMetre;dirty=!1;sinceUpload=0;uploadInterval=iE;painted=0;constructor(e,t=1024){this.scene=e,this.size=t,this.canvas=document.createElement(`canvas`),this.canvas.width=t,this.canvas.height=t,this.context=this.canvas.getContext(`2d`),this.context?.clearRect(0,0,t,t),this.scratch=document.createElement(`canvas`),this.scratch.width=nE,this.scratch.height=nE,this.scratchContext=this.scratch.getContext(`2d`);for(let e=0;e3.05||Math.abs(t)>3.05)return;let c=this.context,l=this.scratchContext;if(this.painted+=1,this.dirty=!0,this.mesh.visible=!0,c===null||l===null)return;l.globalCompositeOperation=`copy`,l.drawImage(this.stamps[Math.abs(Math.trunc(s))%rE],0,0),l.globalCompositeOperation=`source-in`,l.fillStyle=lE(o),l.fillRect(0,0,nE,nE),l.globalCompositeOperation=`source-over`;let u=this.pixel(e,t),d=Math.max(1,n*this.pixelsPerMetre),f=Math.max(1,r*this.pixelsPerMetre);this.grow(u.x,u.y,Math.hypot(d,f)/2),c.save(),c.globalAlpha=K.clamp(a,0,1),c.translate(u.x,u.y),c.rotate(Number.isFinite(i)?i:0),c.drawImage(this.scratch,-d/2,-f/2,d,f),c.restore()}pool(e,t,n,r,i){this.stain(e,t,n*2.3,n*2,i*1.7,r*.8,4851467,i);let a=this.context;if(a===null||Math.abs(e)>3.05||Math.abs(t)>3.05)return;let o=this.pixel(e,t),s=Math.max(1,n*1.15*this.pixelsPerMetre);this.grow(o.x,o.y,s);let c=a.createRadialGradient(o.x,o.y,0,o.x,o.y,s);c.addColorStop(0,`rgba(40,3,6,0.9)`),c.addColorStop(.65,`rgba(58,5,9,0.65)`),c.addColorStop(1,`rgba(70,7,11,0)`),a.save(),a.globalAlpha=K.clamp(r,0,1),a.fillStyle=c,a.beginPath(),a.ellipse(o.x,o.y,s,s*.86,i*.9,0,Math.PI*2),a.fill(),a.restore()}update(e){if(this.sinceUpload+=Number.isFinite(e)?Math.max(0,e):0,!this.dirty||this.sinceUploadt.min.x&&t.max.y>t.min.y?this.uploader.copyTextureToTexture(this.source,this.texture,t,this.regionAt.copy(t.min)):this.texture.needsUpdate=!0,t.makeEmpty(),this.dirty=!1,this.sinceUpload=0}clear(){this.context?.clearRect(0,0,this.size,this.size),this.painted=0,this.dirty=!1,this.region.makeEmpty(),this.texture.needsUpdate=!0,this.mesh.visible=!1}dispose(){this.scene.remove(this.mesh),this.texture.dispose(),this.source.dispose(),this.material.dispose(),this.geometry.dispose()}},dE=` +uniform float uGearBlood; +varying vec3 vGearPos; +`,fE=` +#ifdef USE_MAP +{ + float scanned = dot(sampledDiffuseColor.rgb, vec3(0.2126, 0.7152, 0.0722)); + vec3 cloth = diffuse * clamp(scanned / 0.035, 0.45, 1.5) * 0.85; + vec3 piping = vec3(0.68) * clamp(scanned / 0.22, 0.6, 1.15); + gearTrim = smoothstep(0.07, 0.12, scanned); + diffuseColor.rgb = mix(cloth, piping, gearTrim); + float ragged = 0.08 * sin(vGearPos.y * 1.9 + vGearPos.z * 2.3) * sin(vGearPos.z * 1.3 - vGearPos.x * 0.7); +#ifdef GEAR_TRUNKS + // Blood runs down the chest into the front of the waistband and soaks down from there in runs. + float runs = 0.5 + 0.5 * sin(vGearPos.x * 0.9 + 1.3) * sin(vGearPos.x * 2.3); + float depth = mix(2.0, 22.0, uGearBlood) * (0.45 + 0.55 * runs) + ragged * 20.0; + float front = smoothstep(-1.0, 3.0, vGearPos.z) * (1.0 - smoothstep(9.0, 15.0, abs(vGearPos.x))); + gearSoaked = (1.0 - smoothstep(depth, depth + 3.0, 80.0 - vGearPos.y)) * front * min(1.0, uGearBlood * 2.5); +#else + float knuckles = smoothstep(46.0, 55.0, abs(vGearPos.x)); + float reach = mix(1.05, 0.5, uGearBlood) + ragged; + gearSoaked = smoothstep(reach, reach + 0.22, knuckles) * min(1.0, uGearBlood * 3.0); +#endif + diffuseColor.rgb = mix(diffuseColor.rgb, vec3(0.11, 0.003, 0.005), gearSoaked * 0.9); +} +#endif +`,pE=` +roughnessFactor = mix(mix(roughnessFactor, 0.8, gearTrim), 0.2, gearSoaked); +`;function mE(e,t=!1,n=`gloves`){let r={value:0};return n===`trunks`&&(e.defines={...e.defines,GEAR_TRUNKS:``}),e.onBeforeCompile=e=>{e.uniforms.uGearBlood=r,e.vertexShader=e.vertexShader.replace(`#include `,`#include \n${t?`attribute vec3 bindPosition;`:``}\n${dE}`).replace(`#include `,`#include \nvGearPos = ${t?`bindPosition`:`position`};`),e.fragmentShader=e.fragmentShader.replace(`#include `,`#include \n${dE}\nfloat gearSoaked = 0.0;\nfloat gearTrim = 0.0;`).replace(`#include `,`#include \n${fE}`).replace(`#include `,`#include \n${pE}`)},e.customProgramCacheKey=()=>`hands-gear-${t?`baked`:`live`}`,e.needsUpdate=!0,r}function hE(){let e=new vv(1,2);e.deleteAttribute(`normal`),e.deleteAttribute(`uv`);let t=vw(e,1e-4);e.dispose();let n=t.getAttribute(`position`),r=new J;for(let e=0;e=95?t>=120?2:1:+(e===`hit`&&t>=115):0}function vE(e,t){return Math.min(140,Math.round(Math.max(0,e)*1.4+Math.max(0,t-50)*.5))}var yE=1.1,bE=.6,xE=.35,SE=1.2;function CE(){let e=new Sv(1,8,6),t=e.getAttribute(`position`);for(let e=0;e1+(e-1)*28+t%28;for(let e=0;e<28;e+=1)a.push(0,o(1,e+1),o(1,e));for(let e=1;e<6;e+=1)for(let t=0;t<28;t+=1)a.push(o(e,t),o(e,t+1),o(e+1,t)),a.push(o(e,t+1),o(e+1,t+1),o(e+1,t));let s=new lg;return s.setAttribute(`position`,new Zh(r,3)),s.setAttribute(`uv`,new Zh(i,2)),s.setIndex(a),s.computeVertexNormals(),s}function OE(e,t){let n=e.getAttribute(`position`),r=e.getIndex();if(r===null)return[];let i=new Map,a=new Int32Array(n.count),o=new Uint8Array(n.count),s=new J;for(let e=0;eo[e]===1);for(let e=0;e<3;e+=1){let r=a[t[e]],o=a[t[(e+1)%3]],s=r=2&&l.push(Math.floor(e/n.count),e%n.count);return l}var kE=.5,AE=new J,jE=new J,ME=new J,NE=new J,PE=new J;function FE(e,t,n){PE.copy(e).addScaledVector(t,-e.dot(t)).multiplyScalar(kE/n).add(t).normalize()}function IE(e,t,n,r,i=.01,a){let o=Math.floor(t.length/6),s=e.getAttribute(`position`),c=e.getAttribute(`normal`),l=e.getAttribute(`uv`);(s===void 0||c===void 0||l===void 0||s.count!==o*3)&&(s=new Jh(new Float32Array(o*9),3).setUsage(xp),c=new Jh(new Float32Array(o*9),3).setUsage(xp),l=new Jh(new Float32Array(o*6),2).setUsage(xp),e.setAttribute(`position`,s),e.setAttribute(`normal`,c),e.setAttribute(`uv`,l)),AE.set(1,0,0),Math.abs(r.x)>.9&&AE.set(0,0,1),a!==void 0&&a.lengthSq()>1e-8&&Math.abs(a.dot(r))<.95*a.length()&&AE.copy(a),jE.crossVectors(r,AE).normalize(),AE.crossVectors(jE,r).normalize();let u=1e-6;for(let e=0;e(n=Math.imul(n^n>>>15,1|n),n^=n+Math.imul(n^n>>>7,61|n),((n^n>>>14)>>>0)/4294967296);for(let e=0;e<46;e+=1){let e=r()*Math.PI*2,n=14+r()*42;t.fillStyle=r()<.5?`rgba(150,28,30,0.55)`:`rgba(40,4,8,0.5)`,t.beginPath(),t.ellipse(64+Math.cos(e)*n,64+Math.sin(e)*n,3+r()*6,2+r()*3,e,0,Math.PI*2),t.fill()}let i=(e,n,r,i)=>{t.fillStyle=i,t.beginPath(),t.arc(e,n,r,0,Math.PI*2),t.fill()};i(64,38,10,`#8f7c64`),i(61,36,4.5,`#6a2a25`),i(66,78,7,`#7a3c34`),i(66,78,4.5,`#150203`),i(41,64,3,`#1d0204`),i(90,69,2.6,`#1d0204`);for(let e=0;e<70;e+=1){let e=r()*Math.PI*2,t=r()*60;i(64+Math.cos(e)*t,64+Math.sin(e)*t,.6+r()*1.6,r()<.4?`rgba(190,60,50,0.5)`:`rgba(20,2,4,0.55)`)}RE(t)}let n=new F_(e);return n.colorSpace=hp,n}function RE(e){e.lineWidth=7,e.strokeStyle=`rgba(214,178,92,0.85)`,e.beginPath(),e.arc(64,64,55,0,Math.PI*2),e.stroke(),e.lineWidth=4,e.strokeStyle=`#b8775c`,e.beginPath(),e.arc(64,64,61,0,Math.PI*2),e.stroke()}function zE(){let e=document.createElement(`canvas`);e.width=128,e.height=128;let t=e.getContext(`2d`);if(t!==null){let e=t.createRadialGradient(64,64,6,64,64,64);e.addColorStop(0,`#741014`),e.addColorStop(.7,`#52090d`),e.addColorStop(1,`#300407`),t.fillStyle=e,t.fillRect(0,0,128,128);let n=(e,n,r,i)=>{t.fillStyle=i,t.beginPath(),t.arc(e,n,r,0,Math.PI*2),t.fill()};for(let[e,t,r]of[[46,60,13],[84,66,10]])n(e,t,r,`#e4d8c0`),n(e,t,r*.62,`#a4483c`),n(e-r*.2,t-r*.2,r*.25,`rgba(220,110,90,0.6)`);for(let e=0;e<9;e+=1){let t=Math.PI*(.15+e/8*.7);n(64+Math.cos(t)*34,64+Math.sin(t)*34,3.2,`#d9c9b3`)}n(30,84,3.4,`#1a0204`),n(98,46,3,`#1a0204`),n(66,30,2.6,`#1a0204`),RE(t)}let n=new F_(e);return n.colorSpace=hp,n}function BE(){let e=.09,t=.007,n=[];for(let e=0;e<9;e+=1){let t=e/9*Math.PI*2,r=.32*(.62+.38*Math.abs(Math.sin(e*2.3+.4)));n.push([Math.cos(t)*r,Math.sin(t)*r])}let r=(t,n,r)=>{let i=Math.hypot(t,n,1);return[t/i*r,n/i*r,1/i*r-e]},i=[],a=[],o=[.07,.035,.025],s=[.78,.7,.58],c=[.42,.05,.05],l=(e,t,n,r)=>{i.push(...e,...t,...n);for(let e=0;e<3;e+=1)a.push(...r)};for(let i=0;i<9;i+=1){let[a,u]=n[i],[d,f]=n[(i+1)%9];l(r(0,0,e),r(a,u,e),r(d,f,e),o),l(r(0,0,e-t),r(d,f,e-t),r(a,u,e-t),i%3==0?s:c),l(r(a,u,e),r(a,u,e-t),r(d,f,e),s),l(r(d,f,e),r(a,u,e-t),r(d,f,e-t),s)}let u=new lg;return u.setAttribute(`position`,new Zh(i,3)),u.setAttribute(`color`,new Zh(a,3)),u.computeVertexNormals(),u.computeBoundingSphere(),u}function VE(){let e=document.createElement(`canvas`);e.width=128,e.height=128;let t=e.getContext(`2d`);if(t!==null){let e=t.createRadialGradient(64,60,6,64,64,66);e.addColorStop(0,`#7a1016`),e.addColorStop(.75,`#530a0e`),e.addColorStop(1,`#300408`),t.fillStyle=e,t.fillRect(0,0,128,128),t.fillStyle=`#8a2a2e`,t.beginPath(),t.ellipse(64,50,22,17,0,0,Math.PI*2),t.fill(),t.fillStyle=`rgba(220,120,120,0.35)`,t.beginPath(),t.ellipse(60,45,9,5,-.3,0,Math.PI*2),t.fill(),t.strokeStyle=`#a99c86`,t.lineWidth=5,t.beginPath(),t.ellipse(64,62,50,50,0,Math.PI*.08,Math.PI*.92),t.stroke();for(let e=0;e<12;e+=1){let n=Math.PI*(.14+e/11*.72);t.save(),t.translate(64+Math.cos(n)*42,62+Math.sin(n)*42),t.rotate(n+Math.PI/2),t.fillStyle=e%5==2?`#3a0508`:`#ece4cf`,t.fillRect(-3.5,-4.5,7,9),t.restore()}let n=625341585,r=()=>(n=Math.imul(n^n>>>15,1|n),n^=n+Math.imul(n^n>>>7,61|n),((n^n>>>14)>>>0)/4294967296);for(let e=0;e<60;e+=1)t.fillStyle=r()<.5?`rgba(150,20,24,0.6)`:`rgba(20,2,4,0.5)`,t.beginPath(),t.arc(r()*128,r()*128,.8+r()*2.2,0,Math.PI*2),t.fill();RE(t)}let n=new F_(e);return n.colorSpace=hp,n}function HE(){let e=document.createElement(`canvas`);e.width=128,e.height=64;let t=e.getContext(`2d`);if(t!==null){let e=t.createLinearGradient(0,0,128,0);e.addColorStop(0,`#e9ddd4`),e.addColorStop(.42,`#e9ddd4`),e.addColorStop(.62,`#8a2a2c`),e.addColorStop(.85,`#4a070b`),e.addColorStop(1,`#e9ddd4`),t.fillStyle=e,t.fillRect(0,0,128,64),t.strokeStyle=`rgba(170,20,26,0.75)`,t.lineWidth=.8;for(let e=0;e<14;e+=1){let n=e/14*Math.PI*2;t.beginPath(),t.moveTo(32+Math.cos(n)*30,32+Math.sin(n)*26),t.quadraticCurveTo(32+Math.cos(n+.3)*20,32+Math.sin(n+.3)*18,32+Math.cos(n)*11,32+Math.sin(n)*11),t.stroke()}let n=t.createRadialGradient(32,32,2,32,32,9);n.addColorStop(0,`#6b4524`),n.addColorStop(.8,`#4a2d14`),n.addColorStop(1,`#1e1208`),t.fillStyle=n,t.beginPath(),t.arc(32,32,9,0,Math.PI*2),t.fill(),t.fillStyle=`#050302`,t.beginPath(),t.arc(32,32,4,0,Math.PI*2),t.fill(),t.fillStyle=`rgba(255,255,255,0.85)`,t.beginPath(),t.arc(29.5,29.5,1.4,0,Math.PI*2),t.fill()}let n=new F_(e);return n.colorSpace=hp,n}var UE=[{name:`leftEye`,position:[2.4,120,3.4],radius:3.4,cutHalfLength:0,bleeds:!1},{name:`rightEye`,position:[-2.8,120.1,3.2],radius:3.4,cutHalfLength:0,bleeds:!1},{name:`leftBrow`,position:[2.9,123.4,3],radius:2.2,cutHalfLength:1.1,bleeds:!0},{name:`rightBrow`,position:[-2.7,123,3.3],radius:2.2,cutHalfLength:1.1,bleeds:!0},{name:`nose`,position:[0,117,5.3],radius:1.6,cutHalfLength:0,bleeds:!0},{name:`mouth`,position:[0,114.3,4.6],radius:1.8,cutHalfLength:.9,bleeds:!0},{name:`leftCheek`,position:[3.4,117.8,3.4],radius:3,cutHalfLength:.8,bleeds:!0},{name:`rightCheek`,position:[-3.4,117.9,3.3],radius:3,cutHalfLength:.8,bleeds:!0},{name:`chin`,position:[0,110.8,4.2],radius:2.2,cutHalfLength:0,bleeds:!1},{name:`leftJaw`,position:[4.6,113,1.4],radius:2.6,cutHalfLength:0,bleeds:!1},{name:`rightJaw`,position:[-4.6,113,1.4],radius:2.6,cutHalfLength:0,bleeds:!1},{name:`forehead`,position:[0,125.5,3.5],radius:3.2,cutHalfLength:1.3,bleeds:!0}],WE=[{name:`leftRibs`,position:[13.5,98,1],radius:7.5,cutHalfLength:0,bleeds:!1},{name:`rightRibs`,position:[-13.5,98,1],radius:7.5,cutHalfLength:0,bleeds:!1},{name:`solarPlexus`,position:[.5,96.5,5.5],radius:6,cutHalfLength:0,bleeds:!1},{name:`leftPec`,position:[9,96.2,4.4],radius:6,cutHalfLength:0,bleeds:!1},{name:`rightPec`,position:[-8.8,96.2,4.4],radius:6,cutHalfLength:0,bleeds:!1},{name:`neckSmear`,position:[.4,105.5,5.8],radius:4,cutHalfLength:0,bleeds:!0},{name:`leftShoulderSmear`,position:[10.5,104,4.5],radius:3,cutHalfLength:0,bleeds:!0},{name:`rightShoulderSmear`,position:[-10.5,104,4.5],radius:3,cutHalfLength:0,bleeds:!0},{name:`unused0`,position:[0,-100,0],radius:0,cutHalfLength:0,bleeds:!1},{name:`unused1`,position:[0,-100,0],radius:0,cutHalfLength:0,bleeds:!1},{name:`unused2`,position:[0,-100,0],radius:0,cutHalfLength:0,bleeds:!1},{name:`unused3`,position:[0,-100,0],radius:0,cutHalfLength:0,bleeds:!1}],GE=112.4,KE=.3,qE=114.5,JE=1.8,YE=1e3,XE=` +uniform float uInjurySever; +uniform vec2 uInjurySeverShape; +float injuryCutLine(vec3 p) { + float around = atan(p.z + 3.6, p.x); + return uInjurySever - uInjurySeverShape.x * (p.z - (${(-4).toFixed(1)})) + uInjurySeverShape.y * (0.4 * sin(around * 6.0) + 0.22 * sin(around * 13.0 + 1.7)); +} +`,ZE=[0,119,-3.5,0],QE=[0,0,-2,1],$E=[[2.65,120.65,3.6],[-2.75,120.75,3.4]],eD=` uniform vec4 uInjurySite[12]; uniform float uInjurySwell[12]; +uniform vec4 uInjuryCore; +uniform vec4 uInjuryLid[2]; +uniform vec2 uInjuryEyeOut; uniform float uInjuryJaw; uniform float uInjuryJawLevel; uniform vec4 uInjuryImpact; uniform vec3 uInjuryImpactPush; uniform vec4 uInjuryNose; +uniform mat3 uInjuryBindToPart; varying vec3 vInjuryPos; -`,cC=` -vInjuryPos = transformed; +`,tD=e=>` { + vec3 injuryBind = ${e?`bindPosition`:`transformed`}; + vInjuryPos = injuryBind; + vec3 injuryMoved = injuryBind; float injurySwell = 0.0; for (int i = 0; i < 12; i++) { float radius = uInjurySite[i].w; if (radius <= 0.0) continue; - float d = distance(transformed, uInjurySite[i].xyz); + float d = distance(injuryMoved, uInjurySite[i].xyz); float w = 1.0 - smoothstep(0.0, radius * 1.15, d); injurySwell += uInjurySwell[i] * w * w; } - transformed += objectNormal * injurySwell; - float noseWeight = 1.0 - smoothstep(0.0, 2.6, distance(transformed, uInjuryNose.xyz)); - transformed.x += uInjuryNose.w * 1.1 * noseWeight * noseWeight; - transformed.z -= abs(uInjuryNose.w) * 0.5 * noseWeight * noseWeight; - float impactDistance = distance(transformed, uInjuryImpact.xyz); + vec3 injuryCore = uInjuryCore.w > 0.5 ? vec3(uInjuryCore.x, injuryMoved.y, uInjuryCore.z) : uInjuryCore.xyz; + injuryMoved += normalize(injuryMoved - injuryCore + vec3(0.0, 0.0, 0.001)) * injurySwell; + float noseWeight = 1.0 - smoothstep(0.0, 2.6, distance(injuryMoved, uInjuryNose.xyz)); + injuryMoved.x += uInjuryNose.w * 1.1 * noseWeight * noseWeight; + injuryMoved.z -= abs(uInjuryNose.w) * 0.5 * noseWeight * noseWeight; + float impactDistance = distance(injuryMoved, uInjuryImpact.xyz); float impactWeight = 1.0 - smoothstep(0.0, max(uInjuryImpact.w, 0.001), impactDistance); - transformed += uInjuryImpactPush * (impactWeight * impactWeight); + injuryMoved += uInjuryImpactPush * (impactWeight * impactWeight); + // An eye forced out leaves its socket hollow. + for (int e = 0; e < 2; e++) { + float gone = e == 0 ? uInjuryEyeOut.x : uInjuryEyeOut.y; + if (gone <= 0.0) continue; + float socket = 1.0 - smoothstep(0.0, 1.7, distance(injuryMoved, uInjuryLid[e].xyz)); + injuryMoved.z -= 1.1 * socket * socket * gone; + } // Only the mandible: the mask fades out again under the chin, above the throat and the seam with the body. - float jawMask = (1.0 - smoothstep(uInjuryJawLevel - 2.5, uInjuryJawLevel + 1.5, transformed.y)) - * smoothstep(uInjuryJawLevel - 6.0, uInjuryJawLevel - 4.0, transformed.y) - * smoothstep(-6.0, 0.0, transformed.z); - transformed.x += uInjuryJaw * 1.7 * jawMask; - transformed.y -= uInjuryJaw * 0.9 * jawMask; - transformed.z -= uInjuryJaw * 0.4 * jawMask; + float jawMask = (1.0 - smoothstep(uInjuryJawLevel - 2.5, uInjuryJawLevel + 1.5, injuryMoved.y)) + * smoothstep(uInjuryJawLevel - 6.0, uInjuryJawLevel - 4.0, injuryMoved.y) + * smoothstep(-6.0, 0.0, injuryMoved.z); + injuryMoved.x += uInjuryJaw * 1.7 * jawMask; + injuryMoved.y -= uInjuryJaw * 0.9 * jawMask; + injuryMoved.z -= uInjuryJaw * 0.4 * jawMask; + ${e?`transformed += uInjuryBindToPart * (injuryMoved - injuryBind);`:`transformed = injuryMoved;`} } -`,lC=` +`,nD=` uniform vec4 uInjurySite[12]; uniform vec4 uInjuryCut[12]; uniform float uInjuryBruise[12]; uniform float uInjuryBlood[12]; uniform float uInjuryWetness; +uniform float uInjuryWash; +uniform float uInjuryRaw; +uniform vec4 uInjuryLid[2]; +uniform vec2 uInjuryEyeOut; varying vec3 vInjuryPos; float injuryWet = 0.0; +${XE} float injuryHash(float n) { return fract(sin(n) * 43758.5453123); } -`,uC=` +`,rD=`HANDS_INJURY_OPEN`,iD=` { vec3 injuryPos = vInjuryPos; - vec3 hematomaFresh = vec3(0.31, 0.05, 0.08); - vec3 hematomaDeep = vec3(0.12, 0.035, 0.13); +#ifdef ${rD} + { + float below = injuryCutLine(injuryPos) - injuryPos.y; + if (below < 0.0) discard; + float torn = 1.0 - smoothstep(0.0, 1.8, below); + diffuseColor.rgb = mix(diffuseColor.rgb, vec3(0.2, 0.008, 0.012), torn * 0.94); + injuryWet = max(injuryWet, torn); + } +#endif + vec3 hematomaFresh = vec3(0.42, 0.09, 0.11); + vec3 hematomaDeep = vec3(0.2, 0.04, 0.07); vec3 bloodColor = vec3(0.30, 0.008, 0.012); - vec3 cutColor = vec3(0.13, 0.004, 0.005); + vec3 bloodEdge = vec3(0.13, 0.0, 0.004); + vec3 cutColor = vec3(0.1, 0.002, 0.004); + vec3 rawFlesh = vec3(0.42, 0.05, 0.06); for (int i = 0; i < 12; i++) { float radius = uInjurySite[i].w; if (radius <= 0.0) continue; vec3 site = uInjurySite[i].xyz; - float d = distance(injuryPos, site); float bruise = uInjuryBruise[i]; if (bruise > 0.001) { - float mottle = 0.92 + 0.08 * sin(injuryPos.x * 3.1 + injuryPos.y * 2.3) * sin(injuryPos.z * 2.7 - injuryPos.y * 1.9); - float w = (1.0 - smoothstep(radius * 0.2, radius, d)) * mottle; - vec3 tone = mix(hematomaFresh, hematomaDeep, clamp(bruise * 1.2 - 0.2, 0.0, 1.0)); - vec3 stained = mix(diffuseColor.rgb * tone * 2.0, tone * 1.1, 0.35); - diffuseColor.rgb = mix(diffuseColor.rgb, stained, clamp(bruise, 0.0, 1.0) * w * 0.8); + // Wider than tall, with an uneven edge: a bruise follows the bone under it, not a compass. + vec3 offset = injuryPos - site; + float ragged = 1.0 + 0.18 * sin(atan(offset.y, offset.x) * 5.0 + float(i) * 2.3) + 0.08 * sin(offset.z * 3.1 + offset.x * 2.2); + float spread = length(vec3(offset.x, offset.y * 1.35, offset.z)) / ragged; + float mottle = 0.85 + 0.15 * sin(injuryPos.x * 3.1 + injuryPos.y * 2.3) * sin(injuryPos.z * 2.7 - injuryPos.y * 1.9); + float w = (1.0 - smoothstep(radius * 0.15, radius * 1.05, spread)) * mottle; + float core = 1.0 - smoothstep(0.0, radius * 0.6, spread); + vec3 tone = mix(hematomaFresh, hematomaDeep, clamp(bruise * 1.1 - 0.15 + core * 0.35, 0.0, 1.0)); + vec3 stained = diffuseColor.rgb * tone / vec3(0.6, 0.32, 0.19); + diffuseColor.rgb = mix(diffuseColor.rgb, mix(stained, tone, 0.25), clamp(bruise, 0.0, 1.0) * w * 0.85); } float cut = uInjuryCut[i].y; float halfLength = uInjuryCut[i].x; if (cut > 0.001 && halfLength > 0.0) { + // A split: a dark gash between swollen raw lips, tapering to its ends. Without blood it is closed, + // a thin dark line with no raw flesh and nothing wet about it. vec3 local = injuryPos - site; float along = clamp(local.x, -halfLength, halfLength); + float taper = 1.0 - 0.6 * (abs(along) / halfLength) * (abs(along) / halfLength); vec2 delta = vec2(local.x - along, local.y - 0.18 * sin(local.x * 3.0)); float slit = length(delta) + abs(local.z) * 0.25; - float w = 1.0 - smoothstep(0.06 + cut * 0.06, 0.24 + cut * 0.12, slit); - float lip = (1.0 - smoothstep(0.24, 0.55, slit)) * 0.6; - diffuseColor.rgb = mix(diffuseColor.rgb, hematomaFresh * 0.8, cut * lip); - diffuseColor.rgb = mix(diffuseColor.rgb, cutColor, cut * w); - injuryWet = max(injuryWet, cut * w); + float gash = 1.0 - smoothstep((0.03 + cut * 0.05 * uInjuryRaw) * taper, (0.09 + cut * 0.09 * uInjuryRaw) * taper, slit); + float lip = (1.0 - smoothstep(0.1 * taper, (0.22 + cut * 0.12) * taper, slit)) * (1.0 - gash) * uInjuryRaw; + diffuseColor.rgb = mix(diffuseColor.rgb, rawFlesh, cut * lip * 0.75); + diffuseColor.rgb = mix(diffuseColor.rgb, mix(vec3(0.12, 0.06, 0.05), cutColor, uInjuryRaw), cut * gash); + injuryWet = max(injuryWet, cut * max(gash, lip * 0.6) * uInjuryRaw); } + } + // An eye swollen shut: the lids meet over it in a tight, shiny, purple-red mound closed to a dark slit. + vec3 lidSkin = vec3(0.6, 0.32, 0.19) * diffuse; + for (int e = 0; e < 2; e++) { + vec4 lid = uInjuryLid[e]; + if (lid.w <= 0.001) continue; + vec3 q = injuryPos - lid.xyz; + float across = q.x / 1.85; + float lifted = q.y + 0.1 * q.x * q.x; + float upDown = lifted / (lifted > 0.0 ? 0.8 + 0.35 * lid.w : 0.5 + 0.22 * lid.w); + float reach = length(vec2(across, upDown)) + max(0.0, -q.z - 1.0) * 1.5; + float closed = (1.0 - smoothstep(0.7, 1.08, reach)) * smoothstep(0.0, 0.85, lid.w); + if (closed <= 0.0) continue; + float dome = 1.0 - reach * reach * 0.6; + vec3 puffed = lidSkin * mix(vec3(0.95, 0.62, 0.72), vec3(0.78, 0.4, 0.62), lid.w) * (0.8 + 0.35 * dome); + float crease = (1.0 - smoothstep(0.035, 0.07 + 0.12 * (1.0 - lid.w), abs(lifted + 0.12))) * (1.0 - smoothstep(0.55, 0.95, abs(across))); + diffuseColor.rgb = mix(diffuseColor.rgb, mix(puffed, vec3(0.03, 0.006, 0.01), crease * 0.92), closed); + injuryWet = max(injuryWet, closed * 0.35 * (1.0 - crease)); + } + // An empty socket: a raw red rim round a dark hollow where the eye was. + for (int e = 0; e < 2; e++) { + float gone = e == 0 ? uInjuryEyeOut.x : uInjuryEyeOut.y; + if (gone <= 0.0) continue; + vec3 q = injuryPos - uInjuryLid[e].xyz; + float reach = length(vec2(q.x / 1.45, (q.y + 0.1 * q.x * q.x) / 1.0)) + max(0.0, -q.z - 1.6) * 1.5; + float hollow = (1.0 - smoothstep(0.7, 1.05, reach)) * gone; + vec3 inside = mix(vec3(0.03, 0.0, 0.004), rawFlesh * 1.15, smoothstep(0.3, 0.85, reach)); + diffuseColor.rgb = mix(diffuseColor.rgb, inside, hollow); + injuryWet = max(injuryWet, hollow); + } + for (int i = 0; i < 12; i++) { + float radius = uInjurySite[i].w; float blood = uInjuryBlood[i]; - if (blood > 0.001) { - vec3 origin = site + vec3(0.0, -radius * 0.35, 0.0); - float length = 3.5 + blood * 9.0; - float drop = origin.y - injuryPos.y; - if (drop > -0.4 && drop < length && injuryPos.z > origin.z - 3.2) { - float t = clamp(drop / length, 0.0, 1.0); - float wander = 0.4 * sin(injuryPos.y * 1.6 + float(i) * 1.7) + 0.25 * sin(injuryPos.y * 4.3 + float(i)); - float dx = abs(injuryPos.x - origin.x - wander * t); - float width = 0.28 + 0.28 * t + 0.25 * blood; - float edge = width + 0.18; - float stream = (1.0 - smoothstep(width * 0.6, edge, dx)) * (1.0 - t * t) * smoothstep(-0.4, 0.15, drop); - float head = (1.0 - smoothstep(0.0, radius * 0.9, distance(injuryPos, origin))) * 0.7; - float w = clamp(max(stream, head) * min(1.0, blood * 1.4), 0.0, 1.0); - diffuseColor.rgb = mix(diffuseColor.rgb, bloodColor, w * 0.96); - injuryWet = max(injuryWet, w); - } + if (radius <= 0.0 || blood <= 0.001) continue; + vec3 origin = uInjurySite[i].xyz + vec3(0.0, -radius * 0.35, 0.0); + float drop = origin.y - injuryPos.y; + if (drop < -0.6 || injuryPos.z < origin.z - 3.2) continue; + float flow = smoothstep(0.0, 0.3, blood); + float well = (1.0 - smoothstep(0.0, 0.3 + blood * 0.4, distance(injuryPos.xy, origin.xy))) * flow; + float w = well; + // Two rivulets of their own lengths run from each wound, wandering, thinning, with a bead at each tip. + for (int k = 0; k < 2; k++) { + float fk = float(k); + if (fk > 0.5 && blood < 0.45) break; + float seed = fract(sin(float(i) * 12.9898 + fk * 78.233) * 43758.5453); + float reach = (2.0 + blood * 9.0) * (fk < 0.5 ? 1.0 : 0.55 + 0.3 * seed); + float start = (fk < 0.5 ? -0.3 : 0.4) * (0.6 + 0.6 * seed) * radius * 0.35; + float t = clamp(drop / reach, 0.0, 1.0); + float wander = (0.35 * sin(injuryPos.y * 1.3 + float(i) * 1.7 + fk * 2.1) + 0.15 * sin(injuryPos.y * 3.7 + fk)) * t; + float dx = abs(injuryPos.x - origin.x - start - wander); + float width = (0.26 - 0.12 * t) * (0.7 + 0.45 * min(1.0, blood)); + float run = (1.0 - smoothstep(width * 0.55, width, dx)) * step(drop, reach) * smoothstep(-0.4, 0.1, drop); + float tipY = origin.y - reach; + vec2 tip = vec2(origin.x + start + 0.35 * sin(tipY * 1.3 + float(i) * 1.7 + fk * 2.1) + 0.15 * sin(tipY * 3.7 + fk), tipY); + float bead = 1.0 - smoothstep(width * 1.15, width * 1.15 + 0.1, distance(injuryPos.xy, tip)); + float stream = max(run, bead) * flow; + float edge = smoothstep(width * 0.2, width * 0.85, dx); + diffuseColor.rgb = mix(diffuseColor.rgb, mix(bloodColor, bloodEdge, edge * 0.6), stream * 0.95); + w = max(w, stream); + } + diffuseColor.rgb = mix(diffuseColor.rgb, bloodColor, well * 0.92); + if (uInjuryWash > 0.5) { + // On the body, sweat thins the blood into a broad wash that the gloves smear across the chest. + float t = clamp(drop / (radius * 3.2 + blood * 10.0), 0.0, 1.0); + float across = abs(injuryPos.x - origin.x + 0.9 * sin(injuryPos.y * 0.45 + float(i))); + float wash = (1.0 - smoothstep(radius * (0.5 + 0.4 * t), radius * (1.0 + 0.7 * t), across)) * (1.0 - t * t) * smoothstep(-0.6, 0.6, drop) * flow; + diffuseColor.rgb = mix(diffuseColor.rgb, diffuseColor.rgb * vec3(0.95, 0.26, 0.26) + vec3(0.07, 0.0, 0.0), wash * min(1.0, blood) * 0.7); + w = max(w, wash * 0.5); } + injuryWet = max(injuryWet, w); } +#ifdef ${rD} + // Where a cut or a burst opens the head, its inside shows as raw flesh rather than nothing. + if (!gl_FrontFacing) { + diffuseColor.rgb = vec3(0.2, 0.012, 0.022); + injuryWet = 1.0; + } +#endif injuryWet *= uInjuryWetness; } -`,dC=` +`,aD=` roughnessFactor = mix(roughnessFactor, 0.18, injuryWet); -`,fC=class{material;sites;uniforms={uInjurySite:{value:[]},uInjuryCut:{value:[]},uInjurySwell:{value:new Float32Array(12)},uInjuryBruise:{value:new Float32Array(12)},uInjuryBlood:{value:new Float32Array(12)},uInjuryJaw:{value:0},uInjuryJawLevel:{value:114.5},uInjuryImpact:{value:new ed(0,-1e3,0,1)},uInjuryImpactPush:{value:new Z},uInjuryNose:{value:new ed(0,-1e3,0,0)},uInjuryWetness:{value:1}};index=new Map;constructor(e,t){if(this.material=e,this.sites=t,t.length!==12)throw Error(`injury shading requires 12 sites`);for(let[e,n]of t.entries())this.index.set(n.name,e),this.uniforms.uInjurySite.value.push(new ed(n.position[0],n.position[1],n.position[2],n.radius)),this.uniforms.uInjuryCut.value.push(new ed(n.cutHalfLength,0,0,0));if(e===null)return;let n=this.uniforms;e.onBeforeCompile=e=>{Object.assign(e.uniforms,n),e.vertexShader=e.vertexShader.replace(`#include `,`#include \n${sC}`).replace(`#include `,`#include \n${cC}`),e.fragmentShader=e.fragmentShader.replace(`#include `,`#include \n${lC}`).replace(`#include `,`#include \n${uC}`).replace(`#include `,`#include \n${dC}`)},e.customProgramCacheKey=()=>`hands-injury-12`,e.needsUpdate=!0}slot(e){let t=this.index.get(e);if(t===void 0)throw Error(`unknown injury site ${e}`);return t}set(e,t){let n=this.slot(e);t.bruise!==void 0&&(this.uniforms.uInjuryBruise.value[n]=Pu.clamp(t.bruise,0,1.2)),t.swell!==void 0&&(this.uniforms.uInjurySwell.value[n]=Pu.clamp(t.swell,0,2.2)),t.cut!==void 0&&(this.uniforms.uInjuryCut.value[n].y=Pu.clamp(t.cut,0,1)),t.blood!==void 0&&(this.uniforms.uInjuryBlood.value[n]=Pu.clamp(t.blood,0,1.4))}impact(e,t,n){let r=this.uniforms.uInjurySite.value[this.slot(e)];this.uniforms.uInjuryImpact.value.set(r.x,r.y,r.z,n),this.uniforms.uInjuryImpactPush.value.set(t[0],t[1],t[2])}setNoseShift(e){let t=this.index.get(`nose`);if(t===void 0)return;let n=this.uniforms.uInjurySite.value[t];this.uniforms.uInjuryNose.value.set(n.x,n.y,n.z,Pu.clamp(e,-1.4,1.4)||0)}update(e){let t=this.uniforms.uInjuryImpactPush.value;t.lengthSq()<1e-6||(t.multiplyScalar(Math.exp(-9*e)),t.lengthSq()<1e-4&&t.set(0,0,0))}get impactDepth(){return this.uniforms.uInjuryImpactPush.value.length()}setJaw(e){this.uniforms.uInjuryJaw.value=Pu.clamp(e,0,1)}clear(){this.uniforms.uInjuryBruise.value.fill(0),this.uniforms.uInjurySwell.value.fill(0),this.uniforms.uInjuryBlood.value.fill(0);for(let e of this.uniforms.uInjuryCut.value)e.y=0;this.uniforms.uInjuryJaw.value=0}level(e){let t=this.slot(e);return{bruise:this.uniforms.uInjuryBruise.value[t],swell:this.uniforms.uInjurySwell.value[t],cut:this.uniforms.uInjuryCut.value[t].y,blood:this.uniforms.uInjuryBlood.value[t]}}},pC=e=>e===`off`?0:e===`reduced`?.35:1;function mC(e,t,n){let r=pC(n),i=n===`full`?1:.6,a=e=>Math.min(1.2,e/380+t.swelling/900),o=e=>Math.min(2.2,(e/520+t.swelling/700)*1.6*i);e.set(`leftEye`,{bruise:a(t.left_eye),swell:o(t.left_eye)}),e.set(`rightEye`,{bruise:a(t.right_eye),swell:o(t.right_eye)});let s=e=>Math.min(1,e/220),c=e=>Math.min(1.4,(e/260+t.bleeding/520)*r);e.set(`leftBrow`,{bruise:Math.min(1,t.left_eye/700),swell:Math.min(1.2,t.left_eye/900)*i,cut:s(t.left_cut),blood:c(t.left_cut)}),e.set(`rightBrow`,{bruise:Math.min(1,t.right_eye/700),swell:Math.min(1.2,t.right_eye/900)*i,cut:s(t.right_cut),blood:c(t.right_cut)});let l=Math.min(1,t.head/950+t.swelling/1100);e.set(`leftCheek`,{bruise:l*.9,swell:Math.min(1.4,t.swelling/800)*i,cut:Math.min(1,Math.max(0,t.left_cut-350)/500),blood:Math.min(1,Math.max(0,t.left_cut-350)/400)*r}),e.set(`rightCheek`,{bruise:l*.85,swell:Math.min(1.4,t.swelling/800)*i,cut:Math.min(1,Math.max(0,t.right_cut-350)/500),blood:Math.min(1,Math.max(0,t.right_cut-350)/400)*r}),e.set(`nose`,{bruise:Math.min(1,t.head/800),swell:Math.min(.8,t.head/1400)*i,blood:Math.min(1.4,(Math.max(0,t.head-160)/520+t.bleeding/420)*r)}),e.set(`mouth`,{bruise:Math.min(.8,t.head/1100),cut:Math.min(1,Math.max(0,t.head-420)/700),blood:Math.min(1.2,(Math.max(0,t.head-280)/650+t.bleeding/500)*r)}),e.set(`chin`,{bruise:Math.min(.7,t.head/1300)});let u=t.left_eye>=t.right_eye?-1:1;e.setNoseShift(u*Math.min(1.1,Math.max(0,t.head-520)/450)*i),e.set(`leftJaw`,{bruise:Math.min(.9,t.left_eye/900+t.head/1600)}),e.set(`rightJaw`,{bruise:Math.min(.9,t.right_eye/900+t.head/1600)}),e.set(`forehead`,{bruise:Math.min(.6,t.swelling/1200),cut:Math.min(1,Math.max(0,t.swelling-520)/480),blood:Math.min(1.2,Math.max(0,t.swelling-520)/420)*r})}function hC(e,t,n){let r=pC(n),i=Math.min(1.1,t.body/620);e.set(`leftRibs`,{bruise:i,swell:Math.min(1,t.body/1300)}),e.set(`rightRibs`,{bruise:i*.9,swell:Math.min(1,t.body/1400)}),e.set(`solarPlexus`,{bruise:Math.min(1,t.body/800)}),e.set(`leftPec`,{bruise:Math.min(.8,t.body/1100)}),e.set(`rightPec`,{bruise:Math.min(.8,t.body/1150)});let a=Math.min(1.4,(t.bleeding/380+(t.left_cut+t.right_cut)/900)*r);e.set(`neckSmear`,{blood:a}),e.set(`leftShoulderSmear`,{blood:Math.min(1,t.left_cut/600)*r}),e.set(`rightShoulderSmear`,{blood:Math.min(1,t.right_cut/600)*r})}var gC={hips:rC.hips,spine:rC.spine,chest:`Spine1_024`,upperChest:rC.chest,neck:`Neck_012`,head:rC.head,clavicleL:`LeftShoulder_09`,shoulderL:rC.shoulderL,elbowL:rC.elbowL,gloveL:rC.gloveL,clavicleR:`RightShoulder_020`,shoulderR:rC.shoulderR,elbowR:rC.elbowR,gloveR:rC.gloveR,hipL:rC.hipL,kneeL:rC.kneeL,ankleL:rC.ankleL,toeL:`LeftToeBase_011`,hipR:rC.hipR,kneeR:rC.kneeR,ankleR:rC.ankleR,toeR:`RightToeBase_022`},_C=new Z,vC=new Z,yC=new Z,bC=new ad,xC=new Fu,SC=new Fu,CC=new Z,wC=new Z,TC=new Z,EC=new Z,DC=new Z,OC=new Z,kC=new Z,AC=new Z,jC=new Z,MC=new Z;function NC(e,t){return t.setFromMatrixPosition(e.matrixWorld)}function PC(e,t){return e.matrixWorld.decompose(CC,t,wC),t}function FC(e,t,n,r){return vC.copy(e).normalize(),n===`z`?(yC.copy(t).addScaledVector(vC,-t.dot(vC)),yC.lengthSq()<1e-8&&yC.set(0,0,1).addScaledVector(vC,-vC.z),yC.normalize(),_C.crossVectors(vC,yC).normalize()):(_C.copy(t).addScaledVector(vC,-t.dot(vC)),_C.lengthSq()<1e-8&&_C.set(1,0,0).addScaledVector(vC,-vC.x),_C.normalize(),yC.crossVectors(_C,vC).normalize()),bC.makeBasis(_C,vC,yC),r.setFromRotationMatrix(bC)}var IC=class{bones;metrics;restLocal=new Map;restLocalPosition=new Map;limb={reached:!1,joint:new Z};constructor(e){let t=new Map;e.traverse(e=>{e instanceof Cp&&t.set(e.name,e)});let n={};for(let[e,r]of Object.entries(gC)){let i=t.get(r);if(i===void 0)throw Error(`fighter rig missing bone ${r} (${e})`);n[e]=i}this.bones=n,e.updateMatrixWorld(!0);for(let e of t.values())this.restLocal.set(e,e.quaternion.clone()),this.restLocalPosition.set(e,e.position.clone());let r=(e,t)=>NC(n[e],TC).distanceTo(NC(n[t],EC)),i=NC(n.hips,TC).clone(),a=NC(n.hips.parent??e,EC).clone(),o=n.hips.position.length(),s=i.distanceTo(a);this.metrics={hipsHeight:i.y,headHeight:NC(n.head,TC).y,chestHeight:NC(n.upperChest,TC).y,ankleHeight:NC(n.ankleL,TC).y,armL:{upper:r(`shoulderL`,`elbowL`),lower:r(`elbowL`,`gloveL`)},armR:{upper:r(`shoulderR`,`elbowR`),lower:r(`elbowR`,`gloveR`)},legL:{upper:r(`hipL`,`kneeL`),lower:r(`kneeL`,`ankleL`)},legR:{upper:r(`hipR`,`kneeR`),lower:r(`kneeR`,`ankleR`)},footLength:r(`ankleL`,`toeL`),unitsPerMeter:s>1e-6?o/s:1}}resetToRest(){for(let[e,t]of this.restLocal){e.quaternion.copy(t);let n=this.restLocalPosition.get(e);n!==void 0&&e.position.copy(n)}}restLocalQuaternion(e){return this.restLocal.get(e)??e.quaternion}position(e,t){return NC(this.bones[e],t)}setWorldRotation(e,t){let n=e.parent;n===null?e.quaternion.copy(t):(PC(n,SC),e.quaternion.copy(SC.invert().multiply(t)).normalize()),e.updateWorldMatrix(!1,!1)}setWorldFrame(e,t,n,r){this.setWorldRotation(e,FC(t,n,r,xC))}setWorldPosition(e,t){let n=e.parent;n===null?e.position.copy(t):(e.position.copy(t),n.worldToLocal(e.position)),e.updateWorldMatrix(!1,!1)}aimAt(e,t,n){if(e.updateWorldMatrix(!1,!1),NC(e,TC),DC.subVectors(t,TC),DC.lengthSq()<1e-10)return;PC(e,xC);let r=n===`z`?MC.set(0,0,1):MC.set(1,0,0);this.setWorldFrame(e,DC,r.applyQuaternion(xC),n)}solveLimb(e,t,n,r,i,a,o,s=.01){e.updateWorldMatrix(!1,!1);let c=NC(e,TC);DC.subVectors(r,c);let l=n.upper+n.lower-s,u=Pu.clamp(DC.length(),Math.abs(n.upper-n.lower)+.005,l),d=DC.length()<=l;DC.lengthSq()<1e-10&&DC.set(0,-1,0),DC.normalize(),OC.copy(i).addScaledVector(DC,-i.dot(DC)),OC.lengthSq()<1e-8&&(OC.set(0,0,1).addScaledVector(DC,-DC.z),OC.lengthSq()<1e-8&&OC.set(1,0,0)),OC.normalize();let f=Pu.clamp((n.upper*n.upper+u*u-n.lower*n.lower)/(2*n.upper*u),-1,1),p=n.upper*f,m=n.upper*Math.sqrt(Math.max(0,1-f*f));kC.copy(c).addScaledVector(DC,p).addScaledVector(OC,m),AC.crossVectors(OC,DC).multiplyScalar(o).normalize(),jC.copy(kC).sub(c),this.setWorldFrame(e,jC,AC,a),t.updateWorldMatrix(!1,!1);let h=NC(t,EC);return jC.copy(c).addScaledVector(DC,u).sub(h),this.setWorldFrame(t,jC,AC,a),this.limb.reached=d,this.limb.joint.copy(h),this.limb}},LC=new Z(.2018,.081,-.027),RC=new Z(-.2018,.081,-.027),zC=(e,t,n)=>new Z(e,t,n),BC=(e,t,n)=>{let r=Pu.clamp((n-e)/Math.max(1e-6,t-e),0,1);return r*r*(3-2*r)},VC=(e,t=1.7)=>Pu.clamp(e,0,1)**+t,HC=(e,t=2)=>1-(1-Pu.clamp(e,0,1))**t,UC=new Fu,WC=new Z,GC=new Z,KC=new Z,qC=new Z,JC=new Z,YC=new Fu,XC=new Fu,ZC=new hd,QC=new Z(0,1,0),$C=[[`spine`,.25,.2],[`chest`,.55,.5],[`upperChest`,1,1]],ew=class{rig;chestLocal=new Z;constructor(e){this.rig=e}toWorld(e,t){return t.copy(e).applyQuaternion(UC).add(WC)}dirToWorld(e,t){return t.copy(e).applyQuaternion(UC)}setYawPitchRoll(e,t,n,r){ZC.set(n,t,r,`YXZ`),YC.setFromEuler(ZC),XC.copy(UC).multiply(YC),this.rig.setWorldRotation(e,XC)}apply(e,t){let n=this.rig,r=n.bones;n.resetToRest(),e.updateMatrixWorld(!0),PC(e,UC),NC(e,WC);let i=t.torso;this.toWorld(i.hips,GC),n.setWorldPosition(r.hips,GC),this.setYawPitchRoll(r.hips,i.hipsYaw,i.hipsPitch,i.hipsRoll);let a=i.shouldersYaw-i.hipsYaw;for(let e=0;e<$C.length;e+=1){let[t,n,o]=$C[e];this.setYawPitchRoll(r[t],i.hipsYaw+a*n,i.hipsPitch+i.spinePitch*o,i.hipsRoll+i.spineRoll*o)}let o=i.shouldersYaw+(i.headYaw-i.shouldersYaw)*.45,s=i.hipsPitch+i.spinePitch+(i.headPitch-i.spinePitch)*.5;this.setYawPitchRoll(r.neck,o,s,i.hipsRoll+i.spineRoll*.6+i.headRoll*.4),this.setYawPitchRoll(r.head,i.headYaw,i.headPitch,i.headRoll),i.headOffset.lengthSq()>0&&(NC(r.head,GC),this.dirToWorld(i.headOffset,KC),GC.add(KC),n.setWorldPosition(r.head,GC)),this.solveArm(`L`,t.handL,t.shrugL),this.solveArm(`R`,t.handR,t.shrugR),this.solveLeg(`L`,t.footL),this.solveLeg(`R`,t.footR)}solveArm(e,t,n){let r=this.rig,i=r.bones,a=e===`L`?i.clavicleL:i.clavicleR,o=e===`L`?i.shoulderL:i.shoulderR,s=e===`L`?i.elbowL:i.elbowR,c=e===`L`?i.gloveL:i.gloveR,l=e===`L`?r.metrics.armL:r.metrics.armR,u=e===`L`?LC:RC;this.chestLocal.copy(u),this.chestLocal.z+=n*.06,this.chestLocal.y+=n*.035,this.chestLocal.x*=1-n*.08,qC.copy(this.chestLocal).multiplyScalar(r.metrics.unitsPerMeter),i.upperChest.localToWorld(qC),r.aimAt(a,qC,`z`),o.updateWorldMatrix(!1,!1),this.toWorld(t.position,GC),this.dirToWorld(t.pole,KC),r.solveLimb(o,s,l,GC,KC,`z`,e===`L`?1:-1,.012),c.updateWorldMatrix(!1,!1),this.dirToWorld(t.knuckles,qC),this.dirToWorld(t.palm,JC),r.setWorldFrame(c,qC,JC,`z`)}solveLeg(e,t){let n=this.rig,r=n.bones,i=e===`L`?r.hipL:r.hipR,a=e===`L`?r.kneeL:r.kneeR,o=e===`L`?r.ankleL:r.ankleR,s=e===`L`?r.toeL:r.toeR,c=e===`L`?n.metrics.legL:n.metrics.legR;this.toWorld(t.position,GC),this.dirToWorld(t.toe,qC).normalize();let l=n.metrics.footLength;JC.copy(qC).multiplyScalar(l).addScaledVector(QC,-n.metrics.ankleHeight).normalize(),t.heel!==0&&(GC.addScaledVector(JC,l),YC.setFromAxisAngle(KC.crossVectors(qC,QC).normalize(),-t.heel),JC.applyQuaternion(YC),GC.addScaledVector(JC,-l)),this.dirToWorld(t.pole,KC),n.solveLimb(i,a,c,GC,KC,`x`,-1,.02),o.updateWorldMatrix(!1,!1),n.setWorldFrame(o,JC,QC,`z`),s.updateWorldMatrix(!1,!1),n.setWorldFrame(s,qC,QC,`z`)}},tw={bladeYaw:-1.05,hipsHeight:.8,leadFoot:zC(.12,0,.24),rearFoot:zC(-.1,0,-.34),leadToe:zC(-.34,0,.94),rearToe:zC(-.77,0,.64),relaxedLead:zC(.11,-.22,.36),relaxedRear:zC(-.11,-.16,.1),guardHighLead:zC(.14,-.02,.17),guardHighRear:zC(-.1,-.02,.11),guardLowLead:zC(.15,-.36,.22),guardLowRear:zC(-.13,-.34,.12),leadPole:zC(.45,-1,.2),rearPole:zC(-.5,-1,-.05),leadKnuckles:zC(.1,.55,.8),leadPalm:zC(-.9,.15,-.35),rearKnuckles:zC(-.05,.7,.7),rearPalm:zC(.95,.1,-.25)};function nw(e,t,n=new Z){return n.set(e.x*t,e.y,e.z)}var rw={MHeadMat0:`head`,GlovesMat0:`gloves`,MBodyMat0:`body`,ShoesMat0:`shoes`,PantsMat0:`pants`},iw=new Map,aw=new Map,ow=null,sw=null;function cw(e){return e.colorSpace=Vl,e.flipY=!1,e}function lw(e){let t=iw.get(e);if(t!==void 0)return Promise.resolve(t);let n=aw.get(e);if(n!==void 0)return n;let r=new Gh().loadAsync(iC[e]).then(t=>(iw.set(e,cw(t)),t));return aw.set(e,r),r}function uw(){return Promise.all(Object.keys(iC).map(lw))}async function dw(){let e=Uint8Array.from(atob(nC),e=>e.charCodeAt(0)),t=new Response(e).body;if(t===null)throw Error(`fighter_glb_stream_unavailable`);return new Response(t.pipeThrough(new DecompressionStream(`gzip`))).arrayBuffer()}function fw(){if(ow===null){let e=dw().then(e=>new Promise((t,n)=>{new nS().parse(e,``,t,e=>n(e instanceof Error?e:Error(String(e))))}));ow=Promise.all([e,uw()]).then(([e])=>(sw=e.scene,e))}return ow}function pw(){mw(sw,iw.values())}function mw(e,t){e?.traverse(e=>{e instanceof up&&e.geometry.dispose()});for(let e of t)e.dispose()}function hw(e){e.traverse(e=>{e instanceof Sp&&e.skeleton.dispose()})}function gw(e){let t=iw.get(e);if(t!==void 0)return t;let n=cw(new Gh().load(iC[e]));return iw.set(e,n),n}function _w(e,t){let n=t.bodyMap!==void 0,r=[],i=[],a=new Map,o=null,s=null,c=null;if(e.traverse(e=>{if(!(e instanceof Sp)||Array.isArray(e.material))return;let l=e.material.name,u=rw[l];if(u===void 0)throw Error(`fighter GLB has unsupported material ${l}`);let d=a.get(l);if(d===void 0){let e=l===`MHeadMat0`||l===`MBodyMat0`,f=l===`GlovesMat0`?t.gear:l===`PantsMat0`?t.pants??t.gear:16777215,p=l===`MBodyMat0`&&t.bodyMap!==void 0?t.bodyMap:gw(u);d=e&&!n?new ch({map:p,color:f,roughness:.58,metalness:.02,clearcoat:.25,clearcoatRoughness:.6}):new sh({map:p,color:f,roughness:e?l===`MBodyMat0`?.85:.58:.4,metalness:e?.02:.03}),d.name=l,a.set(l,d),i.push(d),e&&r.push(d),l===`GlovesMat0`&&(o=d),l===`MHeadMat0`&&(s=new fC(n?null:d,aC)),l===`MBodyMat0`&&(c=new fC(n?null:d,oC))}e.material=d,e.castShadow=!n,e.receiveShadow=!0,e.frustumCulled=!1}),r.length!==2||o===null||i.length!==5||s===null||c===null)throw Error(`fighter GLB material contract failed: ${r.length} skin, ${i.length} total`);return{skin:r,gloves:o,owned:i,headInjury:s,bodyInjury:c}}var vw=(e,t,n,r)=>e+(t-e)*(1-Math.exp(-n*r)),yw=(e,t,n,r)=>e+(((t-e+Math.PI)%(Math.PI*2)+Math.PI*2)%(Math.PI*2)-Math.PI)*(1-Math.exp(-n*r)),bw=Pu.clamp,xw=class{root=new Nd;bones=new Map;rig;metrics;skinMaterials;ownedMaterials;gearMaterial;headMeshes=[];handMeshes={left:[],right:[]};decapitated=!1;dismemberedHands={left:!1,right:!1};gearBaseColor;skinBaseColor;headInjury;bodyInjury;constructor(e,t){let n=eS(e.scene);n.scale.setScalar(1),this.root.add(n);let r=_w(n,t);this.skinMaterials=r.skin,this.ownedMaterials=r.owned,this.gearMaterial=r.gloves,this.headInjury=r.headInjury,this.bodyInjury=r.bodyInjury,this.gearBaseColor=new Q(t.gear),this.skinBaseColor=new Q(t.skin),n.traverse(e=>{e instanceof Sp&&e.name===`BoxerHead`&&this.headMeshes.push(e),e instanceof Sp&&e.name===`BoxerGloveLeft`&&this.handMeshes.left.push(e),e instanceof Sp&&e.name===`BoxerGloveRight`&&this.handMeshes.right.push(e),e instanceof Cp&&this.bones.set(e.name,e)});let i=Object.values(rC).filter(e=>!this.bones.has(e));if(i.length>0)throw Error(`fighter GLB missing required bones: ${i.join(`, `)}`);if(this.headMeshes.length!==1)throw Error(`fighter GLB requires one BoxerHead mesh, found ${this.headMeshes.length}`);for(let e of[`left`,`right`])if(this.handMeshes[e].length!==1)throw Error(`fighter GLB requires one ${e} glove mesh, found ${this.handMeshes[e].length}`);this.root.updateMatrixWorld(!0),this.rig=new IC(this.root);let a=this.rig.metrics;this.metrics={armUpper:a.armL.upper,armFore:a.armL.lower,legThigh:a.legL.upper,legShin:a.legL.lower,headRestY:a.headHeight,chestRestY:a.chestHeight,ankleRestY:a.ankleHeight}}bone(e){return this.bones.get(rC[e]??e)??null}get gloveGear(){return this.gearMaterial}get headMesh(){return this.headMeshes[0]}gloveMesh(e){return this.handMeshes[e][0]}get skin(){return this.skinMaterials[0]}get isDecapitated(){return this.decapitated}setDecapitated(e){this.decapitated=e;for(let t of this.headMeshes)t.visible=!e;this.bone(`head`).visible=!e}isHandDismembered(e){return this.dismemberedHands[e]}setHandDismembered(e,t){this.dismemberedHands[e]=t;for(let n of this.handMeshes[e])n.visible=!t}setSkinClearcoat(e){for(let t of this.skinMaterials)t instanceof ch&&(t.clearcoat=e)}dispose(){for(let e of this.ownedMaterials)e.dispose();hw(this.root)}},Sw=new Q(6031886),Cw=(e,t,n,r,i)=>{let a=e.value,o=e.velocity;o.addScaledVector(a,-n*t).multiplyScalar(Math.exp(-r*t)),a.addScaledVector(o,t),a.length()>i&&a.setLength(i)},ww=.06,Tw=150,Ew=.75,Dw=.38,Ow=.72,kw=.5,Aw=.2,jw=.55,Mw=.7,Nw=class{mapping;boxer;solver;yaw=0;yawInitialized=!1;rootX=null;rootZ=0;punchActive=!1;punchAgeTicks=0;punchTotalTicks=1;punchClass=`jab`;punchHand=`left`;punchTarget=`head`;punchPower=`normal`;punchTiming=Ib(`jab`,`head`,`normal`);actionId=null;completedActionId=null;ownActionId=null;ownLeadTicks=0;ownAuthoritativeAge=null;ownWaitedTicks=0;ownExpectedTicks=0;ownPulled=!1;ownSequence=null;ownStarted=!1;ownPressedAt=0;ownRefused=!1;ownQueued=null;contactId=null;contactParried=!1;shownFighter=null;shownOpponent=null;retiredOwnIds=[];hitstop=0;hitstopScale=1;downState=`up`;fallAge=0;riseProgress=0;riseFrom=0;fallSide=0;fallProne=!1;landProne=!1;landSide=0;landShift=new Z;landSettled=!1;liveOpponentHead=new Z;hasLiveHead=!1;guardOpponentHead=new Z;opponentHeadOffset=new Z;opponentSteadySeconds=0;followHead=0;aimActionId=null;headKick={value:new Z,velocity:new Z};torsoKick={value:new Z,velocity:new Z};rootKick={value:new Z,velocity:new Z};coverKick={value:new Z,velocity:new Z};foldDelay=0;pendingFold=0;guardKick=0;guardHigh=0;guardLow=0;slip=0;weave=0;weaveTime=0;pull=0;crouchExtra=0;bouncePhase=0;weightPhase=0;stunPhase=0;feintTimer=2;feint=0;stunAmount=0;tired=0;clinchWeight=0;foulWeight=0;tauntWeight=0;tauntEnd=null;lastSpeed=0;stool;stoolRoot=new Z;stoolYaw=0;enswell;treatTarget=new Z;treatScratch=new Z;treatFacing=new Z(0,0,-1);treatFacingLocal=new Z;treatSide=1;treating=!1;treatWeight=0;resting=!1;seated=0;stillTime=0;celebrateTime=0;celebration=0;waveTime=0;wave=0;breakTime=0;breakWeight=0;breakFighters=[new Z,new Z];breakAimed=!1;attending=!1;attendWeight=0;countdownTicks=null;countdownEnd=null;touchWeight=0;touchOffset=new Z;touchVelocity=new Z;debugHoldImpact=!1;feet=[{planted:new Z,from:new Z,to:new Z,progress:1,stepping:!1,duration:.2},{planted:new Z,from:new Z,to:new Z,progress:1,stepping:!1,duration:.2}];feetInitialized=!1;dislocation=null;scratch=new Z;scratchB=new Z;scratchC=new Z;scratchD=new Z;scratchE=new Z;clinchCentre=new Z;clinchForward=new Z;clinchTemp=new Z;scratchQ=new Fu;headWorld=new Z;hand={L:this.makeHand(),R:this.makeHand()};punchGuard=this.makeHand();punchDir=new Z;punchContact=new Z;punchStart=new Z;punchSweepFrom=new Z;punchSweepTo=new Z;punchMid=new Z;foot={L:this.makeFoot(),R:this.makeFoot()};torso={hips:new Z,hipsYaw:0,hipsPitch:0,hipsRoll:0,shouldersYaw:0,spinePitch:0,spineRoll:0,headYaw:0,headPitch:0,headRoll:0,headOffset:new Z};pose;downStanding=rT();downFrom=rT();downTo=rT();downResult=rT();downStand=1;fallStart=rT();fallStartStand=1;fallFromGetUp=!1;referee;refereeCount=0;refereeCounting=!1;countChangedTick=null;constructor(e,t,n={}){this.mapping=t,this.boxer=e,this.referee=n.referee===!0,this.solver=new ew(e.rig),this.pose={torso:this.torso,handL:this.hand.L,handR:this.hand.R,footL:this.foot.L,footR:this.foot.R,shrugL:0,shrugR:0},this.stool=TT(),this.stool.group.position.set(0,0,.02),e.root.add(this.stool.group),this.enswell=wT(),e.rig.bones.gloveL.add(this.enswell.group)}setResting(e){this.resting=e}get stoolVisible(){return this.stool.group.visible}celebrate(e=4.4){this.celebrateTime=e}resetTransient(e=!1){this.downState=e?`down`:`up`,this.landSettled=!1,this.fallAge=e?Ew:0,this.riseProgress=0,this.hitstop=0,this.hitstopScale=1;for(let e of[this.headKick,this.torsoKick,this.rootKick,this.coverKick])e.value.set(0,0,0),e.velocity.set(0,0,0);this.foldDelay=0,this.pendingFold=0,this.guardKick=0,this.stunAmount=0,this.celebrateTime=0,this.celebration=0,this.waveTime=0,this.wave=0,this.seated=0,this.stillTime=0,this.rootX=null,this.yawInitialized=!1,this.feetInitialized=!1,this.retirePunch(),this.ownQueued=null,this.completedActionId=null,this.retiredOwnIds.length=0}waveOff(e=2.6){this.waveTime=e}setCountdown(e){this.countdownTicks=e}attend(e){this.attending=e}treat(e,t,n=1){this.treating=e!==null,e!==null&&(this.treatTarget.copy(e),t!==void 0&&this.treatFacing.copy(t),this.treatSide=n)}breakClinch(e=1.3){this.breakTime=e}get breaking(){return this.breakTime>0}aimBreak(e,t){this.breakFighters[0].copy(e),this.breakFighters[1].copy(t),this.breakAimed=!0}makeHand(){return{position:new Z,knuckles:new Z(0,1,0),palm:new Z(0,0,1),pole:new Z(0,-1,0)}}makeFoot(){return{position:new Z,toe:new Z(0,0,1),heel:0,pole:new Z(0,0,1)}}get currentRoot(){return{x:this.rootX??0,z:this.rootZ}}get isDown(){return this.downState!==`up`}setRefereeCount(e,t){(e!==this.refereeCounting||t!==this.refereeCount)&&(this.countChangedTick=null),this.refereeCounting=e,this.refereeCount=t}predict(e,t,n,r=0,i){if(e.kind!==`punch`||e.id===void 0)return;let a={action:{...e,id:e.id},timing:i??Ib(e.class,e.target,e.power),leadTicks:r,pressedAt:t,sequence:null,started:!1,waited:0};if(this.punchActive&&this.ownActionId!==null&&this.ownAuthoritativeAge===null&&!this.ownPulled&&!this.ownStarted&&(t-this.ownPressedAt)*n0||e.clinch_ticks>0||e.clinch_startup_ticks>0,a=this.ownQueued;if(a!==null&&(a.sequence??=r?.(a.action.id)??null,e.action_id===a.action.id?a.started=!0:!a.started&&(i||a.sequence!==null&&e.last_input_sequence>=a.sequence&&e.queued_actions===0)&&(this.ownQueued=null)),!this.punchActive||this.ownActionId===null||this.ownPulled||this.ownRefused)return;this.ownStarted||(this.ownSequence??=r?.(this.ownActionId)??null);let o=e.action_id===this.ownActionId;o&&(this.ownStarted=!0);let s=this.ownQueued;if(s!==null&&s.sequence!==null&&r!==null&&!this.ownStarted&&this.ownSequence===null&&this.ownAuthoritativeAge===null){this.ownActionId=null,this.ownQueued=null,this.retirePunch(),this.startOwnPunch(s,Math.max(0,s.leadTicks-s.waited));return}(o?i&&e.action_contact_tick===null:!this.ownStarted&&this.ownAuthoritativeAge===null&&(i||this.ownSequence!==null&&e.last_input_sequence>=this.ownSequence&&e.queued_actions===0))&&(this.ownRefused=!0,this.punchAgeTicks0)?Nb(this.punchTiming):this.punchTotalTicks+1}releaseQueuedPress(e,t){let n=this.ownQueued;n!==null&&(n.waited+=e*30,!(this.punchActive&&!this.ownPulled&&this.punchAgeTicks6&&this.retiredOwnIds.shift()),this.actionId=null,this.punchActive=!1,this.ownActionId=null,this.ownLeadTicks=0,this.ownAuthoritativeAge=null,this.ownWaitedTicks=0,this.ownExpectedTicks=0,this.ownPulled=!1,this.ownSequence=null,this.ownStarted=!1,this.ownRefused=!1}landedHit(e){this.hitstop=Math.max(this.hitstop,e?.045:.078)}setArcadeDislocation(e){this.dislocation=e,this.boxer.headInjury.setJaw(+(e===`jaw`))}react(e,t=`head`,n=1,r=null,i=null,a=90){let o=bw(a/Tw,0,1),s=(.55+o*1.05)*(e===`block`?.35:1),c=i===`left`?1:i===`right`||n>=0?-1:1;if(t===`body`){switch(r){case`hook`:this.torsoKick.velocity.x+=3*s,this.torsoKick.velocity.z+=4.6*s*c,this.rootKick.velocity.x+=.5*s*c,this.coverKick.velocity.x+=2.2*s*c;break;case`uppercut`:this.rootKick.velocity.y+=.9*s,this.torsoKick.velocity.x-=1.6*s,this.foldDelay=.1,this.pendingFold=5.2*s;break;default:this.torsoKick.velocity.x+=4.6*s;break}this.rootKick.velocity.z-=.45*s,this.headKick.velocity.z-=.6*s,this.headKick.velocity.y-=.7*s,this.guardKick=Math.max(this.guardKick,.6*s)}else{switch(e===`hit`&&(this.guardKick=Math.max(this.guardKick,.4*s)),r){case`hook`:this.headKick.velocity.x+=2.6*s*c,this.headKick.velocity.z-=.7*s,this.torsoKick.velocity.z+=.9*s*c;break;case`uppercut`:this.headKick.velocity.y+=2.2*s,this.headKick.velocity.z-=1.2*s,this.torsoKick.velocity.x-=1.2*s;break;case`straight`:this.headKick.velocity.z-=2.4*s,this.torsoKick.velocity.x-=.6*s;break;default:this.headKick.velocity.z-=1.5*s,this.torsoKick.velocity.x-=.35*s;break}this.rootKick.velocity.z-=.45*s,e===`block`&&(this.guardKick=Math.max(this.guardKick,.9*s))}e===`hit`&&(this.dentSurface(t,c,r,o),this.fallSide=r===`hook`?c:c*.5,this.fallProne=r===`hook`||t===`body`)}dentSurface(e,t,n,r){let i=1.1+r*2.1;if(e===`body`){let e=n===`hook`?t>0?`rightRibs`:`leftRibs`:`solarPlexus`;this.boxer.bodyInjury.impact(e,[n===`hook`?t*i*.8:0,0,-i],9);return}n===`hook`?this.boxer.headInjury.impact(t>0?`rightCheek`:`leftCheek`,[t*i,0,-i*.35],5.5):n===`uppercut`?this.boxer.headInjury.impact(`chin`,[0,i*.7,-i*.6],5):this.boxer.headInjury.impact(n===`jab`?`nose`:`mouth`,[0,0,-i],4.5)}stepFeet(e,t,n,r,i,a){let o=this.scratch,s=this.scratchQ.setFromAxisAngle(ET,a);for(let a=0;a<2;a+=1){let c=this.feet[a],l=a===0==t>0?tw.leadFoot:tw.rearFoot;if(this.referee?o.set(a===0?.16:-.16,0,a===0?.02:-.02).applyQuaternion(s).add(i):o.set(l.x*t,0,l.z).applyQuaternion(s).add(i),o.y=0,!this.feetInitialized){c.planted.copy(o),c.progress=1,c.stepping=!1;continue}if(c.stepping){c.progress=Math.min(1,c.progress+e/c.duration),c.progress>=1&&(c.stepping=!1,c.planted.copy(c.to));continue}let u=this.feet[1-a];c.planted.distanceTo(o)>(n>.05?.09:.14)&&!u.stepping&&(c.stepping=!0,c.progress=0,c.duration=bw(.24-n*.07,.15,.24),c.from.copy(c.planted),c.to.copy(o).addScaledVector(r,c.duration*.6),c.to.y=0)}this.feetInitialized=!0}footWorld(e,t){let n=this.feet[e];if(!n.stepping)return t.copy(n.planted),0;let r=HC(n.progress,1.6);return t.copy(n.from).lerp(n.to,r),Math.sin(n.progress*Math.PI)*.055}update(e,t,n,r,i,a,o,s){let c=this.boxer,l=e.stance===`orthodox`?1:-1,u=i?.35:1,d=this.touchWalk(e,t,n,o),f=this.mapping.x(e.x)+d.x,p=this.mapping.z(e.y)+d.z;this.rootX===null&&(this.rootX=f,this.rootZ=p);let m=LT*Math.max(n,1/60);this.rootX+=bw(f-this.rootX,-m,m),this.rootZ+=bw(p-this.rootZ,-m,m);let h=Math.atan2(e.facing_x,-e.facing_y);this.yawInitialized||=(this.yaw=h,!0);let g=this.punchActive?3:this.downState===`up`?9:4;this.yaw=yw(this.yaw,h,g,n),c.root.position.set(this.rootX,0,this.rootZ),c.root.rotation.set(0,this.yaw,0),this.hitstop=Math.max(0,this.hitstop-n),this.hitstopScale=this.hitstop>0?.12:1;let _=n*this.hitstopScale;this.syncAction(e,t,o,_),this.debugHoldImpact||(this.boxer.headInjury.update(_),this.boxer.bodyInjury.update(_)),s!==void 0&&this.trackOpponentHead(t,s,n);let v=this.scratchB.set(this.mapping.x(e.velocity_x)*30,0,this.mapping.z(e.velocity_y)*30).add(this.touchVelocity),y=v.length();this.lastSpeed=y,this.stillTime=y<.03?this.stillTime+n:0;let b=this.resting&&this.stillTime>.2&&this.downState===`up`;this.seated=vw(this.seated,+!!b,b?2.2:4,n);let x=this.resting&&this.stillTime>.05&&this.downState===`up`;x&&(this.stoolRoot.set(this.rootX,0,this.rootZ),this.stoolYaw=this.yaw),this.stool.group.visible=x||this.seated>Fw&&this.downState===`up`,this.stool.group.position.set(this.stoolRoot.x-this.rootX,0,this.stoolRoot.z-this.rootZ).add(this.scratch.set(0,0,.02).applyAxisAngle(ET,this.stoolYaw)).applyAxisAngle(ET,-this.yaw),this.stool.group.rotation.y=this.stoolYaw-this.yaw,this.celebrateTime=Math.max(0,this.celebrateTime-n),this.celebration=vw(this.celebration,+(this.celebrateTime>0&&this.downState===`up`),3.5,n),this.waveTime=Math.max(0,this.waveTime-n),this.wave=vw(this.wave,+(this.waveTime>0&&this.downState===`up`),4,n),this.breakTime=Math.max(0,this.breakTime-n),this.breakWeight=vw(this.breakWeight,+(this.breakTime>0&&this.downState===`up`),6,n),this.attendWeight=vw(this.attendWeight,+!!this.attending,2.5,n),this.treatWeight=vw(this.treatWeight,+!!this.treating,3,n),this.enswell.group.visible=this.treatWeight>.4;let S=this.countdownTicks!==null&&this.countdownTicks<=Bw&&this.countdownTicks>=Vw;this.touchWeight=vw(this.touchWeight,+!!S,6,n);let C=e.stamina/Math.max(1,e.maximum_stamina);this.tired=vw(this.tired,bw((.55-C)/.5,0,1),2,n),this.stunAmount=vw(this.stunAmount,Math.min(1,e.stunned_ticks/24),8,n);let w=e.defense;this.guardHigh=vw(this.guardHigh,+(w===`guard_high`),14,n),this.guardLow=vw(this.guardLow,+(w===`guard_low`),14,n);let T=w===`slip_left`?1:w===`slip_right`?-1:0;this.slip=vw(this.slip,T,16,n);let E=w===`weave`;this.weaveTime=E?this.weaveTime+n:0,this.weave=vw(this.weave,+!!E,14,n),this.pull=vw(this.pull,+(w===`pull`),14,n),this.clinchWeight=vw(this.clinchWeight,+(e.clinch_ticks>0||e.clinch_startup_ticks>0),10,n),this.foulWeight=vw(this.foulWeight,+!!e.is_foul_recovery_target,8,n),this.tauntWeight=vw(this.tauntWeight,+(e.taunt_ticks>0),10,n),this.tauntEnd=ST(this.tauntEnd,o,e.taunt_ticks>0?e.taunt_ticks:null),Cw(this.headKick,n,190,7.5,.24),Cw(this.torsoKick,n,150,7,.7),Cw(this.rootKick,n,120,8,.12),Cw(this.coverKick,n,110,15,.3),this.foldDelay>0&&(this.foldDelay-=n,this.foldDelay<=0&&(this.torsoKick.velocity.x+=this.pendingFold,this.pendingFold=0)),this.guardKick=Math.max(0,this.guardKick-n*2.4),this.updateDownState(e,n,y);let D=(1.9-this.tired*.7)*(1-this.stunAmount*.6);this.bouncePhase+=n*D*Math.PI*2*u,this.weightPhase+=n*.55*Math.PI*2*u,this.stunPhase+=n*5.5,this.feintTimer-=n,this.feintTimer<=0&&!this.punchActive&&y<.05&&this.downState===`up`&&(this.feintTimer=2.5+this.bouncePhase%1.7,this.feint=1),this.feint=Math.max(0,this.feint-n*3.5);let O=this.scratchC.set(this.rootX,0,this.rootZ);(this.downState===`up`||this.downState===`rising`)&&this.stepFeet(n,l,y,v,O,this.yaw);let ee=this.scratchQ.setFromAxisAngle(ET,-this.yaw),k=l>0?this.foot.L:this.foot.R,A=l>0?this.foot.R:this.foot.L,te=l>0?0:1,j=+(l>0),M=this.footWorld(te,k.position),ne=this.footWorld(j,A.position);k.position.sub(O).applyQuaternion(ee),A.position.sub(O).applyQuaternion(ee);let N=this.boxer.rig.metrics.ankleHeight;k.position.y=N+M,A.position.y=N+ne,nw(tw.leadToe,l,k.toe),nw(tw.rearToe,l,A.toe),k.heel=0,A.heel=0,k.pole.copy(k.toe).setY(.35).normalize(),A.pole.copy(A.toe).setY(.35).normalize();let P=this.torso,F=tw.bladeYaw*l,re=Math.sin(this.bouncePhase)*.012*(1-this.stunAmount)*(y>.6?.6:1),ie=Math.sin(this.weightPhase)*.02,ae=.012+this.tired*.03+this.stunAmount*.05;P.hips.set(ie*l+Math.sin(this.stunPhase*.9)*.05*this.stunAmount,tw.hipsHeight-ae+re,0),P.hipsYaw=F,P.hipsPitch=.02+this.tired*.08,P.hipsRoll=Math.sin(this.stunPhase)*.09*this.stunAmount,P.shouldersYaw=F*.88,P.spinePitch=.16+this.tired*.16+Math.sin(r*(2.1+this.tired*1.8))*.012,P.spineRoll=Math.sin(this.stunPhase*1.3)*.06*this.stunAmount,P.headYaw=-.12*l+Math.sin(r*.9)*.05+Math.sin(this.stunPhase*.7)*.25*this.stunAmount,P.headPitch=.12+Math.sin(r*1.7)*.02+this.stunAmount*.18,P.headRoll=Math.sin(this.stunPhase*1.1)*.14*this.stunAmount,P.headOffset.set(0,0,0);let I=l>0?this.hand.L:this.hand.R,L=l>0?this.hand.R:this.hand.L,oe=this.headRestChar(l),se=bw(this.guardHigh+this.guardLow,0,1),ce=this.scratch.copy(tw.relaxedLead).lerp(tw.guardHighLead,this.guardHigh).lerp(tw.guardLowLead,this.guardLow),le=this.scratchB.copy(tw.relaxedRear).lerp(tw.guardHighRear,this.guardHigh).lerp(tw.guardLowRear,this.guardLow);ce.y-=this.tired*.12+this.stunAmount*.22,le.y-=this.tired*.1+this.stunAmount*.2,ce.z+=this.feint*.1+Math.sin(r*2.3)*.012*u,ce.x+=Math.sin(r*1.9)*.01*u,le.y+=Math.cos(r*2.7)*.01*u,ce.z-=this.guardKick*.07,le.z-=this.guardKick*.05,ce.y-=this.guardKick*.02,ce.add(oe),le.add(oe),I.position.set(ce.x*l,ce.y,ce.z),L.position.set(le.x*l,le.y,le.z),nw(tw.leadKnuckles,l,I.knuckles),nw(tw.leadPalm,l,I.palm),nw(tw.rearKnuckles,l,L.knuckles),nw(tw.rearPalm,l,L.palm),nw(tw.leadPole,l,I.pole),nw(tw.rearPole,l,L.pole);let ue=.15*se,de=.25*se;if(Math.abs(this.slip)>.001){let e=this.slip;P.headOffset.x+=e*.13,P.spineRoll+=-e*.28,P.hips.x+=e*.03,P.hips.y-=Math.abs(e)*.05,P.spinePitch+=Math.abs(e)*.12,P.headRoll+=-e*.2}if(this.weave>.001){let e=this.weave,t=bw(this.weaveTime/.42,0,1),n=Math.sin(t*Math.PI);P.hips.y-=e*.2,P.spinePitch+=e*.42,P.headOffset.x+=e*(1-2*t)*.16*l,P.headOffset.y-=e*n*.08,P.spineRoll+=e*(1-2*t)*.18*l,I.position.y-=e*.1,L.position.y-=e*.1}if(this.pull>.001){let e=this.pull;P.spinePitch-=e*.34,P.hips.z-=e*.11,P.hips.x-=e*.05*l,P.headOffset.z-=e*.06,P.headPitch-=e*.1}P.headOffset.add(this.headKick.value),P.headPitch+=-this.headKick.value.z*2.4+this.headKick.value.y*1.6,P.headYaw+=this.headKick.value.x*2.2,P.headRoll+=-this.headKick.value.x*.9,P.spinePitch+=this.torsoKick.value.x,P.spineRoll+=this.torsoKick.value.z*.5,P.hips.x+=this.rootKick.value.x,P.hips.z+=this.rootKick.value.z,P.hips.y-=Math.max(0,this.torsoKick.value.x)*.08,this.torsoKick.value.x>.05&&(I.position.y-=this.torsoKick.value.x*.25,L.position.y-=this.torsoKick.value.x*.2,I.position.z-=this.torsoKick.value.x*.1);let fe=Math.max(0,this.rootKick.value.y);P.hips.y+=fe,k.heel+=fe*6,A.heel+=fe*6;let pe=this.coverKick.value.x;if(Math.abs(pe)>.002){let e=pe>0?this.hand.R:this.hand.L;e.position.y-=Math.abs(pe)*.9,e.position.x+=pe*.2,e.position.z-=Math.abs(pe)*.25,e.pole.y-=Math.abs(pe)*3}if(this.punchActive){ue=0,de=0;let e=this.applyPunch(l,I,L,k,A,oe);this.punchHand===`left`==l>0?ue=e:de=e}if(this.referee&&this.applyRefereePose(l,I,L,k,A,oe,r,o),this.clinchWeight>.001&&this.applyClinchPose(this.clinchWeight,r,l,e,t,I,L,oe),this.foulWeight>.001){let e=this.foulWeight;P.hips.y-=e*.16,P.spinePitch+=e*.55,P.headPitch+=e*.2,I.position.lerp(this.scratch.set(.06*l,.72,.16),e),L.position.lerp(this.scratch.set(-.05*l,.7,.14),e),I.pole.set(.6*l,-.6,.5),L.pole.set(-.6*l,-.6,.5)}if(this.tauntWeight>.001){let e=this.tauntEnd===null?0:bw(this.tauntEnd-o,0,Jw);this.applyTauntPose(this.tauntWeight,(Jw-e)/Jw*Yw,l,I,L,oe)}if(this.dislocation===`shoulder_left`||this.dislocation===`shoulder_right`){let e=this.dislocation===`shoulder_left`?this.hand.L:this.hand.R,t=this.dislocation===`shoulder_left`?1:-1;e.position.set(.24*t,.55,.05),e.pole.set(.9*t,-.3,.2),e.knuckles.set(0,-1,.1),e.palm.set(-t,0,.2)}this.touchWeight>.001&&this.downState===`up`&&this.applyTouchGlovesPose(this.touchWeight,l,I,L),this.celebration>.001&&this.downState===`up`&&this.applyCelebratePose(this.celebration,r,l,I,L,k,A),this.wave>.001&&this.downState===`up`&&this.applyWaveOffPose(this.wave,r,l,I,L),this.breakWeight>.001&&this.downState===`up`&&this.applyBreakPose(this.breakWeight,l,I,L),this.attendWeight>.001&&this.downState===`up`&&this.applyAttendPose(this.attendWeight,r,l,I,L),this.treatWeight>.001&&this.downState===`up`&&this.applyTreatPose(this.treatWeight,r,l,I,L,k,A),this.seated>.001&&this.downState===`up`&&this.applySeatedPose(this.seated,r,l,I,L,k,A),this.downState!==`up`&&this.applyDownPose(l,I,L,k,A),ae=0;let me=this.pose;me.shrugL=l>0?ue:de,me.shrugR=l>0?de:ue,this.solver.apply(c.root,this.pose),this.applyDislocation();let he=Math.min(1,(t.trauma.bleeding+t.trauma.left_cut+t.trauma.right_cut)/620*(a===`off`?0:a===`reduced`?.3:1.5));c.gloveGear.color.copy(c.gearBaseColor).lerp(Sw,he),mC(c.headInjury,e.trauma,a),hC(c.bodyInjury,e.trauma,a),c.setSkinClearcoat(.25+(1-C)*.4)}applyTauntPose(e,t,n,r,i,a){let o=this.torso,s=.5-.5*Math.cos(t*Math.PI*2);i.position.lerp(this.scratch.set(-.3*n,a.y-.62,.02),e),i.pole.lerp(this.scratch.set(-.5*n,-.4,-.75),e).normalize(),i.knuckles.lerp(this.scratch.set(-.1*n,-.95,.3),e).normalize(),i.palm.lerp(this.scratch.set(0,.3,.95),e).normalize(),r.position.lerp(this.scratch.set(.26*n,a.y-.36+s*.1,.52-s*.24),e),r.pole.lerp(this.scratch.set(.7*n,-.7,.1),e).normalize(),r.knuckles.lerp(this.scratch.set(.1*n,.25+s*.6,.95-s*.7),e).normalize(),r.palm.lerp(this.scratch.set(-.2*n,.95,-.2-s*.5),e).normalize(),o.spinePitch-=e*.12,o.headPitch-=e*.22,o.headYaw+=Math.sin(t*Math.PI)*.15*e,o.shouldersYaw+=Math.sin(t*Math.PI*3)*.1*e,o.hips.x+=Math.sin(t*Math.PI)*.03*e*n,o.headOffset.z+=e*.03}applyClinchPose(e,t,n,r,i,a,o,s){let c=this.torso,l=r.player_idy&&(h.x*=y/v,h.z*=y/v),h.x+=o,h.z+=u,t.position.lerp(h,e),t.pole.lerp(this.clinchTemp.set(r*.85,l?.35:-.6,.25),e).normalize(),t.knuckles.lerp(this.clinchTemp.set(-p*r*.9+f.x*.3,l?-.25:.1,-m*r*.9+f.z*.3),e).normalize(),t.palm.lerp(this.clinchTemp.set(-f.x,l?.1:.35,-f.z),e).normalize()}}headRestChar(e){let t=this.boxer.rig.metrics;return this.headWorld.set(-.08,t.headHeight-t.hipsHeight+this.torso.hips.y,.02)}syncAction(e,t,n,r){this.shownFighter=e,this.shownOpponent=t;let i=this.ownQueued;if(i!==null&&e.action_id===i.action.id&&(this.ownQueued=null,this.retirePunch(),this.startOwnPunch(i,0)),e.action_id!==null&&e.action_id!==this.actionId&&e.action_id!==this.completedActionId&&!this.retiredOwnIds.includes(e.action_id)){let t=e.action;if(t!==null){let r=e.action_hand??(e.stance===`orthodox`?`left`:`right`),i=this.ownActionId===e.action_id&&this.punchActive,a=this.punchTiming;this.actionId=e.action_id,this.punchClass=t,this.punchHand=r,this.punchTarget=e.action_target??`head`,this.punchPower=e.action_power??`normal`,this.punchTiming={...Ib(this.punchClass,this.punchTarget,this.punchPower),startup:e.action_startup_ticks,active:e.action_active_ticks,recovery:e.action_recovery_ticks},this.punchTotalTicks=Math.max(1,e.action_startup_ticks+e.action_active_ticks+e.action_recovery_ticks);let o=Math.max(0,n-e.action_start_tick);i?(this.punchAgeTicks=IT(this.punchAgeTicks,a,this.punchTiming),this.ownAuthoritativeAge=o,this.ownRefused||(this.ownPulled=!1,o-this.punchAgeTicks>1&&(this.punchAgeTicks=o))):(this.punchAgeTicks=o,this.ownActionId=null,this.ownLeadTicks=0,this.ownAuthoritativeAge=null),this.ownWaitedTicks=0,this.punchActive=!0}}if(e.action_id===null)this.actionId=null,this.completedActionId=null;else if(e.action_id===this.actionId&&this.punchActive){let t=Math.max(0,n-e.action_start_tick);this.ownActionId===this.actionId?(this.ownAuthoritativeAge=t,!this.ownPulled&&t-this.punchAgeTicks>1.5&&(this.punchAgeTicks=t)):Math.abs(this.punchAgeTicks-t)>1.5&&(this.punchAgeTicks=t)}this.punchActive&&this.ownActionId!==null&&this.ownAuthoritativeAge===null&&!this.ownPulled&&(this.ownWaitedTicks+=r*30,this.ownWaitedTicks>this.ownExpectedTicks*1.5+OT&&this.punchAgeTicks=this.punchTotalTicks&&(this.retirePunch(),a=!0)),e.is_downed&&(this.punchActive&&this.retirePunch(),this.ownQueued=null),this.releaseQueuedPress(r,a);let o=this.actionId??this.ownActionId;o!==this.aimActionId&&(this.aimActionId=o,this.followHead=0),this.followHead=vw(this.followHead,+(o!==null&&o===this.contactId),PT,r)}trackOpponentHead(e,t,n){let r=this.guardOpponentHead.set(this.mapping.x(e.x),0,this.mapping.z(e.y));this.opponentSteadySeconds=MT.has(e.defense)?0:this.opponentSteadySeconds+n,(!this.hasLiveHead||this.opponentSteadySeconds>=NT)&&this.opponentHeadOffset.copy(t).sub(r),r.add(this.opponentHeadOffset),this.liveOpponentHead.copy(t),this.hasLiveHead=!0}ownPunchRate(){if(this.ownActionId===null)return 1;let e=this.punchTiming.startup,t=this.ownAuthoritativeAge;return t===null?this.punchAgeTicks0,p=f?t:n,m=f?n:t,h=f?1:-1,g=+(this.punchPower===`power`),_=+(this.punchTarget===`body`),v,y,b,x=1,S=1;if(c0){let t=this.scratchQ.setFromAxisAngle(ET,-N*e*C);r.toe.applyQuaternion(t),r.pole.applyQuaternion(t)}if(M>.3){let t=this.scratchQ.setFromAxisAngle(ET,.45*e*C);i.toe.applyQuaternion(t),i.pole.applyQuaternion(t)}let ue=this.scratchE;switch(this.punchClass){case`hook`:ue.set(.22*h*e,-.06,-.12);break;case`uppercut`:ue.set(.05*h*e,-.42,-.02);break;case`straight`:ue.set(.02*h*e,-.03,-.09-.05*g);break;default:ue.set(0,-.01,-.05-.04*g);break}let de=this.punchStart.copy(E).addScaledVector(ue,Math.max(y,this.punchClass===`hook`||this.punchClass===`uppercut`?(1-w)*.6*x:0));if(this.punchClass===`hook`){let t=Math.hypot(de.x-I.x,de.z-I.z),n=Math.hypot(le.x-I.x,le.z-I.z),r=this.punchSweepFrom.copy(de).sub(I).setY(0).normalize(),i=this.punchSweepTo.copy(le).sub(I).setY(0).normalize(),a=Math.acos(bw(r.dot(i),-1,1)),o=r.z*i.x-r.x*i.z>=0?1:-1,s=BC(0,1,w),c=r.applyAxisAngle(ET,a*s*o).normalize();p.position.copy(I).addScaledVector(c,Pu.lerp(t,n,s)),p.position.y=Pu.lerp(de.y,le.y,s),p.pole.set(.95*h*e,.08,.3).normalize(),p.knuckles.copy(c).applyAxisAngle(ET,o*Math.PI/2).setY(.05).normalize(),p.palm.set(0,-1,0)}else if(this.punchClass===`uppercut`){let t=BC(0,1,w),n=de,r=this.punchMid.copy(le).lerp(n,.5);r.y=Math.min(n.y,le.y)-.04,r.z+=.08,p.position.copy(n).lerp(r,t*2>1?1:t*2),t>.5&&p.position.copy(r).lerp(le,(t-.5)*2),p.pole.set(.3*h*e,-.2,1),p.knuckles.set(.05*h*e,.9,.35).normalize(),p.palm.set(-.2*h*e,.3,-.95).normalize()}else{p.position.copy(de).lerp(le,Math.min(1,w)),p.position.y+=Math.sin(bw(w,0,1)*Math.PI)*.025,p.pole.set(.55*h*e,-.9,.35);let t=BC(.55,1,w);p.knuckles.copy(L).lerp(this.scratchC.set(.1*h*e,.55,.8),1-t).normalize(),p.palm.set(-.9*h*e,.2,-.3).lerp(this.scratchC.set(0,-1,.1),t).normalize()}if(T>0&&(p.position.lerp(E,T),p.knuckles.lerp(D,T).normalize(),p.palm.lerp(O,T).normalize(),p.pole.lerp(ee,T).normalize()),S<1){let t=(1-p.pole.dot(ee)/Math.max(1e-6,p.pole.length()*ee.length()))/2;p.knuckles.lerp(D,1-S).normalize(),p.palm.lerp(O,1-S).normalize(),p.pole.lerp(ee,1-S).addScaledVector(this.scratchC.set(h*e,0,0),FT*t*Math.sin(Math.PI*S)).normalize()}let fe=this.scratchE.set(-.1*h*e,a.y-.04,.12+a.z);return m.position.lerp(fe,C*.7),C}applyRefereePose(e,t,n,r,i,a,o,s){let c=this.torso;c.hipsYaw=0,c.shouldersYaw=0,c.hipsPitch=0,c.spinePitch=.06,c.headYaw=Math.sin(o*.7)*.08,c.headPitch=.08,c.hips.y=tw.hipsHeight+.03,c.hips.x=0,c.hips.z=0,r.toe.set(.12*e,0,1).normalize(),i.toe.set(-.12*e,0,1).normalize(),r.pole.copy(r.toe).setY(.3).normalize(),i.pole.copy(i.toe).setY(.3).normalize();let l=Math.sin(o*1.3)*.01;if(t.position.set(.27*e,.86+l,.12),n.position.set(-.27*e,.86-l,.12),t.pole.set(.5*e,-.4,-.8).normalize(),n.pole.set(-.5*e,-.4,-.8).normalize(),t.knuckles.set(.15*e,-.9,.35).normalize(),n.knuckles.set(-.15*e,-.9,.35).normalize(),t.palm.set(-.9*e,.1,.35).normalize(),n.palm.set(.9*e,.1,.35).normalize(),this.refereeCounting){this.countChangedTick??=s;let r=bw((s-this.countChangedTick)/Iw,0,1),i=r=Ew&&(this.downState=`down`);else{let n=(e.get_up_required>0?bw(e.get_up_meter/e.get_up_required,0,1):0)*Ow;this.riseProgress+=bw(n-this.riseProgress,-.4*t,jw*t)}return}if((this.downState===`down`||this.downState===`falling`)&&(this.downState=`rising`,this.riseFrom=this.riseProgress,this.feetInitialized=!1),this.downState!==`rising`)return;let r=n>.3||this.punchActive,i=(1-this.riseFrom)/(r?Aw:kw);this.riseProgress=Math.min(1,this.riseProgress+(this.riseProgress=1&&(this.downState=`up`)}applyTouchGlovesPose(e,t,n,r){let i=this.torso,a=Pu.lerp;i.hips.z=a(i.hips.z,.06,e),i.hipsYaw=a(i.hipsYaw,i.hipsYaw*.4,e),i.shouldersYaw=a(i.shouldersYaw,0,e),i.spinePitch=a(i.spinePitch,.12,e),i.headPitch=a(i.headPitch,.1,e),n.position.lerp(Xw.set(.11*t,1.24,.5),e),r.position.lerp(Xw.set(-.11*t,1.22,.47),e),n.palm.lerp(Xw.set(-t,0,0),e).normalize(),r.palm.lerp(Xw.set(t,0,0),e).normalize(),n.knuckles.lerp(Xw.set(0,.15,1),e).normalize(),r.knuckles.lerp(Xw.set(0,.15,1),e).normalize(),n.pole.lerp(Xw.set(.7*t,-.7,0),e).normalize(),r.pole.lerp(Xw.set(-.7*t,-.7,0),e).normalize()}touchWalk(e,t,n,r){this.countdownEnd=ST(this.countdownEnd,r,this.countdownTicks);let i=this.countdownEnd===null?0:CT(this.countdownEnd-r),a=this.mapping.x(t.x)-this.mapping.x(e.x),o=this.mapping.z(t.y)-this.mapping.z(e.y),s=Math.hypot(a,o),c=s>Hw?(s-Hw)/2/s*i:0;return this.touchVelocity.set(a*c-this.touchOffset.x,0,o*c-this.touchOffset.z).divideScalar(Math.max(n,.001)).clampLength(0,qw),this.touchOffset.set(a*c,0,o*c)}applyCelebratePose(e,t,n,r,i,a,o){let s=this.torso,c=Pu.lerp,l=Math.sin(t*6)*.04;s.hips.y=c(s.hips.y,tw.hipsHeight+.035+l*.5,e),s.hipsYaw=c(s.hipsYaw,0,e),s.hipsPitch=c(s.hipsPitch,-.12,e),s.hipsRoll=c(s.hipsRoll,0,e),s.shouldersYaw=c(s.shouldersYaw,0,e),s.spinePitch=c(s.spinePitch,-.1,e),s.headPitch=c(s.headPitch,-.25,e),r.position.lerp(Xw.set(.3*n,1.86+l,.08),e),i.position.lerp(Xw.set(-.3*n,1.84+l,.06),e),r.palm.lerp(Xw.set(0,0,1),e).normalize(),i.palm.lerp(Xw.set(0,0,1),e).normalize(),r.knuckles.lerp(Xw.set(.15*n,1,0),e).normalize(),i.knuckles.lerp(Xw.set(-.15*n,1,0),e).normalize(),r.pole.lerp(Xw.set(n,.1,-.3),e).normalize(),i.pole.lerp(Xw.set(-n,.1,-.3),e).normalize(),a.heel=c(a.heel,.45,e),o.heel=c(o.heel,.45,e)}applyAttendPose(e,t,n,r,i){let a=this.torso,o=Pu.lerp,s=Math.sin(t*3.1)*.03;a.hips.z=o(a.hips.z,.12,e),a.hips.y=o(a.hips.y,tw.hipsHeight-.04,e),a.hipsPitch=o(a.hipsPitch,.3,e),a.spinePitch=o(a.spinePitch,.42,e),a.headPitch=o(a.headPitch,.4,e),r.position.lerp(Xw.set(.2*n,1.3+s,.62),e),i.position.lerp(Xw.set(-.18*n,1.24-s,.58),e),r.palm.lerp(Xw.set(0,-.6,.8),e).normalize(),i.palm.lerp(Xw.set(0,-.6,.8),e).normalize(),r.knuckles.lerp(Xw.set(.1*n,.5,1),e).normalize(),i.knuckles.lerp(Xw.set(-.1*n,.5,1),e).normalize(),r.pole.lerp(Xw.set(.8*n,-.5,-.1),e).normalize(),i.pole.lerp(Xw.set(-.8*n,-.5,-.1),e).normalize()}applyTreatPose(e,t,n,r,i,a,o){let s=this.torso,c=Pu.lerp,l=this.treatScratch.set(this.rootX??0,0,this.rootZ),u=this.scratchQ.setFromAxisAngle(ET,-this.yaw),d=Xw.copy(this.treatTarget).sub(l).applyQuaternion(u),f=this.treatFacingLocal.copy(this.treatFacing).applyQuaternion(u),p=f.z,m=-f.x,h=Math.sin(t*2.6)*.006;s.hips.lerp(this.treatScratch.set(0,.62,.04),e),s.hipsYaw=c(s.hipsYaw,0,e),s.hipsPitch=c(s.hipsPitch,.22,e),s.hipsRoll=c(s.hipsRoll,0,e),s.shouldersYaw=c(s.shouldersYaw,0,e),s.spinePitch=c(s.spinePitch,.3,e),s.headPitch=c(s.headPitch,.2,e),a.position.x=c(a.position.x,.24*n,e),a.position.z=c(a.position.z,.14,e),o.position.x=c(o.position.x,-.2*n,e),o.position.z=c(o.position.z,-.2,e),o.heel=c(o.heel,.55,e),a.toe.lerp(this.treatScratch.set(.2*n,0,1),e).normalize(),o.toe.lerp(this.treatScratch.set(-.1*n,0,1),e).normalize(),a.pole.lerp(this.treatScratch.set(.25*n,.4,1),e).normalize(),o.pole.lerp(this.treatScratch.set(-.2*n,.4,1),e).normalize();let g=.08-h;r.position.lerp(this.treatScratch.set(d.x+f.x*g,d.y-.07,d.z+f.z*g),e),r.knuckles.lerp(this.treatScratch.set(0,1,.05),e).normalize(),r.palm.lerp(this.treatScratch.set(-f.x,0,-f.z),e).normalize(),r.pole.lerp(this.treatScratch.set(.85*n,-.35,.1),e).normalize();let _=this.treatSide*.135;i.position.lerp(this.treatScratch.set(d.x-p*_+f.x*.05,d.y-.2,d.z-m*_+f.z*.05),e),i.knuckles.lerp(this.treatScratch.set(0,.9,.4),e).normalize(),i.palm.lerp(this.treatScratch.set(p*this.treatSide*.8-f.x*.6,.1,m*this.treatSide*.8-f.z*.6),e).normalize(),i.pole.lerp(this.treatScratch.set(-.85*n,-.4,.1),e).normalize()}applyBreakPose(e,t,n,r){let i=this.torso,a=Pu.lerp;i.hipsPitch=a(i.hipsPitch,.14,e),i.spinePitch=a(i.spinePitch,.08,e);let o=this.scratchC.set(.48*t,1.22,.52),s=this.scratchD.set(-.48*t,1.22,.52);if(this.breakAimed){let[e,n]=this.breakFighters,r=this.scratch.subVectors(n,e).setY(0).normalize(),i=this.scratchQ.setFromAxisAngle(ET,-this.yaw),a=this.scratchB.set(this.rootX??0,0,this.rootZ);o.copy(e).addScaledVector(r,Rw).sub(a).applyQuaternion(i),s.copy(n).addScaledVector(r,-Rw).sub(a).applyQuaternion(i),o.x*tEw*.15;let s=this.downResult;if(this.downState!==`falling`){let a=this.riseProgress,c=0;if(a=n||(n=u.length()+o,this.landProne=s,this.landSide=c,this.landShift.copy(u))}}ropeShift(e,t){let n=this.scratchQ.setFromAxisAngle(ET,this.yaw);$w.set(e.hipsPitch+e.spinePitch*.7,e.hipsYaw,e.hipsRoll,`YXZ`),hT[0].set(0,pT,0).applyEuler($w).add(e.hips),hT[1].set(.2,mT,0).applyEuler($w).add(e.hips),hT[2].set(-.2,mT,0).applyEuler($w).add(e.hips),hT[3].copy(e.leadHand),hT[4].copy(e.rearHand),hT[5].copy(e.leadFoot).z+=e.leadHeel>0?.25:.19,hT[6].copy(e.rearFoot).z+=e.rearHeel>0?.25:.19,hT[7].copy(e.hips);let r=1/0,i=-1/0,a=1/0,o=-1/0;for(let e=0;enew Z),gT=[.17,.22,.22,.13,.13,.12,.12,.2],_T=[[!1,1,0],[!1,0,.15],[!1,-1,.3],[!0,1,.4],[!0,0,.5],[!0,-1,.6]];function vT(e,t){return e<-fT&&t>fT?-(e+t)/2:e<-fT?-fT-e:t>fT?fT-t:0}function yT(e,t){return bT(e,t,t,0)}function bT(e,t,n,r){let i=Pu.lerp;return e.hips.copy(t.hips).lerp(n.hips,r),e.hipsYaw=i(t.hipsYaw,n.hipsYaw,r),e.hipsPitch=i(t.hipsPitch,n.hipsPitch,r),e.hipsRoll=i(t.hipsRoll,n.hipsRoll,r),e.shouldersYaw=i(t.shouldersYaw,n.shouldersYaw,r),e.spinePitch=i(t.spinePitch,n.spinePitch,r),e.headPitch=i(t.headPitch,n.headPitch,r),e.leadHand.copy(t.leadHand).lerp(n.leadHand,r),e.rearHand.copy(t.rearHand).lerp(n.rearHand,r),e.leadFoot.copy(t.leadFoot).lerp(n.leadFoot,r),e.rearFoot.copy(t.rearFoot).lerp(n.rearFoot,r),e.leadHeel=i(t.leadHeel,n.leadHeel,r),e.rearHeel=i(t.rearHeel,n.rearHeel,r),e.palmsDown=i(t.palmsDown,n.palmsDown,r),e}var xT=3;function ST(e,t,n){if(n===null)return null;let r=t+n;return e===null||Math.abs(r-e)>xT?r:e}function CT(e){return(1-BC(Ww,Uw,e))*BC(Kw,Gw,e)}function wT(){let e=new Nd;e.name=`enswell`,e.visible=!1;let t=new gm(.03,.03,.008,16),n=new gm(.008,.008,.06,8),r=new sh({color:13159634,roughness:.3,metalness:.9}),i=new up(t,r);i.rotation.x=Math.PI/2,i.position.set(0,.07,.075),e.add(i);let a=new up(n,r);return a.position.set(0,.035,.075),e.add(a),{group:e,dispose:()=>{t.dispose(),n.dispose(),r.dispose()}}}function TT(){let e=new Nd;e.name=`stool`,e.visible=!1;let t=new gm(.17,.17,.035,18),n=new gm(.012,.014,Pw-.02,8),r=new sh({color:13222581,roughness:.85}),i=new sh({color:2764083,roughness:.45,metalness:.6}),a=new up(t,r);a.position.y=Pw-.0175,a.castShadow=!0,e.add(a);for(let t=0;t<4;t+=1){let r=Math.PI/4+t*Math.PI/2,a=new up(n,i);a.position.set(Math.cos(r)*.13,(Pw-.02)/2,Math.sin(r)*.13),a.rotation.set(-Math.sin(r)*.12,0,Math.cos(r)*.12),a.castShadow=!0,e.add(a)}return{group:e,dispose:()=>{t.dispose(),n.dispose(),r.dispose(),i.dispose()}}}var ET=new Z(0,1,0),DT=6,OT=5,kT=1.5,AT=.5,jT=new Set([`hit`,`counter_hit`,`block`,`perfect_block`]),MT=new Set([`slip_left`,`slip_right`,`weave`,`pull`]),NT=.25,PT=30,FT=2;function IT(e,t,n){if(e0)||e>1e3||(this.elapsed+=e,this.frames+=1,this.elapsede.before*.9)return this.scale=e.previous,this.holdWindows=Math.min(VT,zT*2**this.fruitlessAttempts),this.fruitlessAttempts+=1,!0;this.fruitlessAttempts=0}if(this.raised!==null){let e=this.raised;if(t>this.options.slowMs)return this.raised=null,this.scale=e.previous,this.fastWindows=0,this.raiseHoldWindows=Math.min(VT,BT*2**this.failedRaises),this.failedRaises+=1,!0;e.windows+=1,e.windows>=UT&&(this.raised=null,this.failedRaises=0)}if(t>this.options.slowMs){if(this.fastWindows=0,this.holdWindows>0)return--this.holdWindows,!1;if(this.scale<=this.options.minimum)return!1;let e=this.scale;return this.scale=Math.max(this.options.minimum,this.scale-this.options.step),this.pending={before:t,previous:e},!0}return t0?(--this.raiseHoldWindows,!1):this.fastWindows>=HT&&this.scalee.tick>=i-1&&e.tick<=a+1),s=o[0],c=o.at(-1);if(s===void 0||c===void 0||o.length<8||s.tick>t.tick-12||c.tickZT?Math.min(1,(i-ZT)/4):0;this.strength+=(a-this.strength)*Math.min(1,t*18);let o=this.history.length>=YT?this.history.shift():new Z;if(o.copy(e),this.history.push(o),this.strength<.02||this.history.length<3){this.mesh.visible=!1;return}let s=this.history.length;for(let e=0;e{Object.assign(e.uniforms,r),e.vertexShader=e.vertexShader.replace(`#include `,`#include \n${eD}`).replace(`#include `,`#include \n${tD(i)}`),e.fragmentShader=e.fragmentShader.replace(`#include `,`#include \n${nD}`).replace(`#include `,`#include \n${iD}`).replace(`#include `,`#include \n${aD}`)},e.customProgramCacheKey=()=>`hands-injury-12${i?`-baked`:``}`,e.needsUpdate=!0}slot(e){let t=this.index.get(e);if(t===void 0)throw Error(`unknown injury site ${e}`);return t}set(e,t){let n=this.slot(e);t.bruise!==void 0&&(this.uniforms.uInjuryBruise.value[n]=K.clamp(t.bruise,0,1.2)),t.swell!==void 0&&(this.uniforms.uInjurySwell.value[n]=K.clamp(t.swell,0,2.2)),t.cut!==void 0&&(this.uniforms.uInjuryCut.value[n].y=K.clamp(t.cut,0,1)),t.blood!==void 0&&(this.uniforms.uInjuryBlood.value[n]=K.clamp(t.blood,0,1.4))}impact(e,t,n){let r=this.uniforms.uInjurySite.value[this.slot(e)];this.uniforms.uInjuryImpact.value.set(r.x,r.y,r.z,n),this.uniforms.uInjuryImpactPush.value.set(t[0],t[1],t[2])}setNoseShift(e){let t=this.index.get(`nose`);if(t===void 0)return;let n=this.uniforms.uInjurySite.value[t];this.uniforms.uInjuryNose.value.set(n.x,n.y,n.z,K.clamp(e,-1.4,1.4)||0)}update(e){let t=this.uniforms.uInjuryImpactPush.value;t.lengthSq()<1e-6||(t.multiplyScalar(Math.exp(-9*e)),t.lengthSq()<1e-4&&t.set(0,0,0))}get impactDepth(){return this.uniforms.uInjuryImpactPush.value.length()}setSevered(e){this.uniforms.uInjurySever.value=e?GE:YE,this.uniforms.uInjurySeverShape.value.set(KE,1),this.setOpen(e)}setBurst(e){this.uniforms.uInjurySever.value=e?qE:YE,this.uniforms.uInjurySeverShape.value.set(e?0:KE,e?JE:1),this.setOpen(e)}get open(){return this.material?.defines?.[rD]!==void 0}setOpen(e){let t=this.material;if(!(t===null||e===this.open)){for(let n of[t,this.shadow]){if(n===null)continue;let t=n.defines??={};e?t[rD]=``:delete t[rD],n.needsUpdate=!0}t.side=e?2:0}}shadowMaterial(){let e=new Iv,t={uInjurySever:this.uniforms.uInjurySever,uInjurySeverShape:this.uniforms.uInjurySeverShape};return e.onBeforeCompile=e=>{Object.assign(e.uniforms,t),e.vertexShader=e.vertexShader.replace(`#include `,`#include +varying vec3 vInjuryPos;`).replace(`#include `,`#include +vInjuryPos = transformed;`),e.fragmentShader=e.fragmentShader.replace(`#include `,`#include \nvarying vec3 vInjuryPos;\n${XE}`).replace(`#include `,`#include \n#ifdef ${rD}\nif (vInjuryPos.y > injuryCutLine(vInjuryPos)) discard;\n#endif`)},e.customProgramCacheKey=()=>`hands-injury-shadow`,this.open&&(e.defines={[rD]:``}),this.shadow=e,e}copyFrom(e){let t=e.uniforms,n=this.uniforms;n.uInjuryBruise.value.set(t.uInjuryBruise.value),n.uInjurySwell.value.set(t.uInjurySwell.value),n.uInjuryBlood.value.set(t.uInjuryBlood.value);for(let[e,r]of n.uInjuryCut.value.entries())r.y=t.uInjuryCut.value[e]?.y??0;for(let[e,r]of n.uInjuryLid.value.entries())r.w=t.uInjuryLid.value[e]?.w??0;n.uInjuryEyeOut.value.copy(t.uInjuryEyeOut.value),n.uInjuryNose.value.copy(t.uInjuryNose.value),n.uInjuryJaw.value=t.uInjuryJaw.value,n.uInjuryWetness.value=t.uInjuryWetness.value,n.uInjuryRaw.value=t.uInjuryRaw.value,n.uInjuryImpactPush.value.set(0,0,0)}setBindToPart(e){this.uniforms.uInjuryBindToPart.value.copy(e)}setRawCuts(e){this.uniforms.uInjuryRaw.value=K.clamp(e,0,1)||0}setJaw(e){this.uniforms.uInjuryJaw.value=K.clamp(e,0,1)}setEyesShut(e,t){let[n,r]=this.uniforms.uInjuryLid.value;n.y>-500&&(n.w=K.clamp(e,0,1)||0),r.y>-500&&(r.w=K.clamp(t,0,1)||0)}setEyeOut(e){let t=this.uniforms.uInjuryLid.value,n=e===`left`&&t[0].y>-500,r=e===`right`&&t[1].y>-500;this.uniforms.uInjuryEyeOut.value.set(+!!n,+!!r),this.index.has(`leftEye`)&&this.set(`leftEye`,{blood:n?1.3:0}),this.index.has(`rightEye`)&&this.set(`rightEye`,{blood:r?1.3:0})}get eyeOut(){let e=this.uniforms.uInjuryEyeOut.value;return e.x>0?`left`:e.y>0?`right`:null}get eyesShut(){return[this.uniforms.uInjuryLid.value[0].w,this.uniforms.uInjuryLid.value[1].w]}clear(){this.uniforms.uInjuryBruise.value.fill(0),this.uniforms.uInjurySwell.value.fill(0),this.uniforms.uInjuryBlood.value.fill(0);for(let e of this.uniforms.uInjuryCut.value)e.y=0;this.uniforms.uInjuryJaw.value=0;for(let e of this.uniforms.uInjuryLid.value)e.w=0;this.uniforms.uInjuryEyeOut.value.set(0,0),this.uniforms.uInjuryNose.value.w=0,this.uniforms.uInjuryImpactPush.value.set(0,0,0)}level(e){let t=this.slot(e);return{bruise:this.uniforms.uInjuryBruise.value[t],swell:this.uniforms.uInjurySwell.value[t],cut:this.uniforms.uInjuryCut.value[t].y,blood:this.uniforms.uInjuryBlood.value[t]}}},sD=e=>e===`off`?0:e===`reduced`?.35:1,cD=330;function lD(e,t){return K.clamp((e-cD)/(d-cD)+Math.max(0,t-200)/1600,0,1)}var uD=1.8,dD=750,fD=380;function pD(e,t,n){let r=sD(n),i=n===`full`?1:.6;e.setRawCuts(n===`full`?1:n===`reduced`?.6:0);let a=e=>Math.min(1.2,e/380+t.swelling/900),o=e=>Math.min(2,(e/600+t.swelling/900)*i);e.set(`leftEye`,{bruise:a(t.left_eye),swell:o(t.left_eye)}),e.set(`rightEye`,{bruise:a(t.right_eye),swell:o(t.right_eye)}),e.setEyesShut(lD(t.left_eye,t.swelling),lD(t.right_eye,t.swelling));let s=e=>Math.min(1,e/220),c=e=>Math.min(1.4,(e/260+t.bleeding/520)*r);e.set(`leftBrow`,{bruise:Math.min(1,t.left_eye/700),swell:Math.min(1.2,t.left_eye/900)*i,cut:s(t.left_cut),blood:c(t.left_cut)}),e.set(`rightBrow`,{bruise:Math.min(1,t.right_eye/700),swell:Math.min(1.2,t.right_eye/900)*i,cut:s(t.right_cut),blood:c(t.right_cut)});let l=Math.min(1,t.head/950+t.swelling/1100),u=Math.min(uD,t.swelling/520)*i,d=e=>Math.min(1,Math.max(0,e-350)/(dD-350));e.set(`leftCheek`,{bruise:l*.9,swell:u,cut:d(t.left_cut),blood:Math.min(1,Math.max(0,t.left_cut-350)/400)*r}),e.set(`rightCheek`,{bruise:l*.85,swell:u,cut:d(t.right_cut),blood:Math.min(1,Math.max(0,t.right_cut-350)/400)*r}),e.set(`nose`,{bruise:Math.min(1,t.head/800),swell:Math.min(.8,t.head/1400)*i,blood:Math.min(1.4,(Math.max(0,t.head-160)/520+t.bleeding/420)*r)}),e.set(`mouth`,{bruise:Math.min(.8,t.head/1100),cut:Math.min(1,Math.max(0,t.head-420)/700),blood:Math.min(1.2,(Math.max(0,t.head-280)/650+t.bleeding/500)*r)}),e.set(`chin`,{bruise:Math.min(.7,t.head/1300)});let f=t.left_eye>=t.right_eye?-1:1;e.setNoseShift(f*Math.min(1.1,Math.max(0,t.head-520)/450)*i),e.set(`leftJaw`,{bruise:Math.min(.9,t.left_eye/900+t.head/1600)}),e.set(`rightJaw`,{bruise:Math.min(.9,t.right_eye/900+t.head/1600)}),e.set(`forehead`,{bruise:Math.min(.6,t.swelling/1200),cut:Math.min(1,Math.max(0,t.swelling-fD)/400),blood:Math.min(1.2,Math.max(0,t.swelling-fD)/360)*r})}function mD(e,t){return Math.min(1,(Math.max(0,e.bleeding-40)/420+Math.max(0,e.left_cut+e.right_cut-150)/1300)*sD(t))}function hD(e,t,n){let r=sD(n),i=Math.min(1.1,t.body/620);e.set(`leftRibs`,{bruise:i,swell:Math.min(1,t.body/1300)}),e.set(`rightRibs`,{bruise:i*.9,swell:Math.min(1,t.body/1400)}),e.set(`solarPlexus`,{bruise:Math.min(1,t.body/800)}),e.set(`leftPec`,{bruise:Math.min(.8,t.body/1100)}),e.set(`rightPec`,{bruise:Math.min(.8,t.body/1150)});let a=Math.min(1.4,(t.bleeding/380+(t.left_cut+t.right_cut)/900)*r);e.set(`neckSmear`,{blood:a}),e.set(`leftShoulderSmear`,{blood:Math.min(1,t.left_cut/600)*r}),e.set(`rightShoulderSmear`,{blood:Math.min(1,t.right_cut/600)*r})}var gD={tint:16777215,hair:4862755,shaved:0,beard:0,jaw:0,nose:0,brow:0,skull:0},_D={referee:{tint:15916235,hair:9210502,shaved:0,beard:0,jaw:-.4,nose:.3,brow:-.2,skull:.2},blueCorner:{tint:11109480,hair:1380364,shaved:1,beard:.8,jaw:.7,nose:.5,brow:.6,skull:.4},redCorner:{tint:16772829,hair:8007452,shaved:0,beard:.6,jaw:.2,nose:-.5,brow:.1,skull:-.3},blueCutman:{tint:14202008,hair:2234128,shaved:0,beard:0,jaw:-.6,nose:.8,brow:-.5,skull:-.5},redCutman:{tint:9070160,hair:1380364,shaved:1,beard:.45,jaw:.5,nose:.2,brow:.8,skull:.6}},vD=[16777215,16047307,14268057,12555896,10717820,9071708,7296328],yD=[1380364,2234128,3810326,5913120,9071164,11045976,7222304,9210502];function bD(e){let t=2166136261;for(let n=0;n>>0}function xD(e){return()=>(e=Math.imul(e^e>>>15,1|e),e^=e+Math.imul(e^e>>>7,61|e),((e^e>>>14)>>>0)/4294967296)}function SD(e){let t=xD(bD(e)||1),n=e=>e[Math.floor(t()*e.length)],r=()=>Math.round((t()*2-1)*100)/100;return{tint:n(vD),hair:n(yD),shaved:+(t()<.28),beard:t()<.45?0:Math.round((.35+t()*.65)*100)/100,jaw:r(),nose:r(),brow:r(),skull:r()}}var CD=`vec3(0.84, 0.39, 0.175)`,wD=` +uniform vec3 uLookHair; +uniform vec4 uLookFace; +uniform vec2 uLookGroom; +varying vec3 vLookPos; +/** Where the scanned man has hair: above a hairline that runs from the forehead past the ear to the nape. */ +float lookHairZone(vec3 p) { + float behind = mix(115.3, 120.6, smoothstep(-6.8, -4.6, p.z)); + float line = mix(behind, 125.5, smoothstep(-4.6, 1.6, p.z)); + float nape = smoothstep(116.0, 121.0, p.y); + float reach = mix(3.8, 9.0, nape); + return smoothstep(line - 0.5, line + 0.5, p.y) * (1.0 - smoothstep(reach, reach + 1.0, abs(p.x)) * (1.0 - nape)); +} +`,TD=` +{ + vec3 p = position; + vLookPos = p; + float jaw = (1.0 - smoothstep(113.5, 118.0, p.y)) * smoothstep(108.5, 112.0, p.y) * smoothstep(-7.0, -2.0, p.z); + transformed.x += p.x * 0.12 * uLookFace.x * jaw; + float nose = 1.0 - smoothstep(0.0, 2.6, distance(p, vec3(0.0, 117.2, 5.0))); + transformed.x += p.x * 0.4 * uLookFace.y * nose; + transformed.z += 0.5 * uLookFace.y * nose; + float brow = (1.0 - smoothstep(0.0, 2.2, abs(p.y - 123.2))) * smoothstep(1.5, 3.5, p.z); + transformed.z += 0.55 * uLookFace.z * brow; + transformed.x += p.x * 0.06 * uLookFace.w * smoothstep(112.0, 116.0, p.y); + // A shaved head loses the thickness of the scanned hair. + float crown = lookHairZone(p) * uLookGroom.x; + float fringe = smoothstep(-1.0, 3.0, p.z) * smoothstep(123.5, 126.0, p.y); + float crownDepth = mix(0.9, 1.9, fringe) * crown; + vec3 crownAway = normalize(vec3(p.x, (p.y - 120.0) * 1.1, p.z + 2.5)); + transformed.x -= crownAway.x * crownDepth; + transformed.y -= crownAway.y * crownDepth; + transformed.z -= crownAway.z * crownDepth; +} +`,ED=(e,t,n)=>{let r=Math.min(1,Math.max(0,(n-e)/(t-e)));return r*r*(3-2*r)},DD=(e,t,n)=>e+(t-e)*n;function OD(e,t,n){let r=DD(DD(115.3,120.6,ED(-6.8,-4.6,n)),125.5,ED(-4.6,1.6,n)),i=ED(116,121,t),a=DD(3.8,9,i);return ED(r-.5,r+.5,t)*(1-ED(a,a+1,Math.abs(e))*(1-i))}function kD(e,t,n){let{x:r,y:i,z:a}=e,o=(1-ED(113.5,118,i))*ED(108.5,112,i)*ED(-7,-2,a),s=1-ED(0,2.6,Math.hypot(r,i-117.2,a-5)),c=(1-ED(0,2.2,Math.abs(i-123.2)))*ED(1.5,3.5,a),l=r*.12*t.jaw*o+r*.4*t.nose*s+r*.06*t.skull*ED(112,116,i),u=0,d=.5*t.nose*s+.55*t.brow*c,f=DD(.9,1.9,ED(-1,3,a)*ED(123.5,126,i))*OD(r,i,a)*t.shaved,p=r,m=(i-120)*1.1,h=a+2.5,g=Math.hypot(p,m,h)||1;return l-=p/g*f,u-=m/g*f,d-=h/g*f,n.set(r+l,i+u,a+d)}var AD=` +vLookPos = bindPosition; +`,jD=` +#ifdef USE_MAP +{ + vec3 p = vLookPos; + float scanned = dot(sampledDiffuseColor.rgb, vec3(0.2126, 0.7152, 0.0722)); + float hair = lookHairZone(p) * max(1.0 - smoothstep(0.13, 0.26, scanned), smoothstep(126.2, 127.4, p.y)); + vec3 dyed = uLookHair * clamp(scanned / 0.075, 0.45, 1.7); + vec3 scalp = mix(${CD}, uLookHair, 0.1) * clamp(scanned / 0.075, 0.85, 1.12) * 0.9; + diffuseColor.rgb = mix(diffuseColor.rgb, mix(dyed, scalp * diffuse, uLookGroom.x), hair); + // The beard covers the chin and runs back along the jaw to the ear, clear of the neck beneath it. + float front = mix(1.4, -4.8, smoothstep(111.2, 114.6, p.y)); + float jaw = (1.0 - smoothstep(115.4, 116.8, p.y)) * smoothstep(109.6, 110.6, p.y) * smoothstep(front - 0.6, front + 0.6, p.z); + float lips = 1.0 - smoothstep(0.8, 2.0, length(vec3(p.x * 0.55, p.y - 114.3, (p.z - 4.8) * 0.4))); + float grain = 0.8 + 0.2 * sin(p.x * 41.0 + p.z * 13.0) * sin(p.y * 47.0 + p.z * 29.0); + diffuseColor.rgb = mix(diffuseColor.rgb, mix(uLookHair * 0.8, ${CD} * diffuse, 0.12), jaw * (1.0 - lips) * uLookGroom.y * grain * 0.88); +} +#endif +`,MD=class{material;uniforms={uLookHair:{value:new Y(gD.hair)},uLookFace:{value:new Dm},uLookGroom:{value:new q}};current=gD;constructor(e,t=!1){this.material=e;let n=this.uniforms,r=e.onBeforeCompile,i=Object.hasOwn(e,`customProgramCacheKey`)?e.customProgramCacheKey():`plain`;e.onBeforeCompile=(i,a)=>{r.call(e,i,a),Object.assign(i.uniforms,n),i.vertexShader=i.vertexShader.replace(`#include `,`#include \n${t?`attribute vec3 bindPosition;`:``}\n${wD}`).replace(`#include `,`${t?AD:TD}\n#include `),i.fragmentShader=i.fragmentShader.replace(`#include `,`#include \n${wD}`).replace(`#include `,`#include \n${jD}`)},e.customProgramCacheKey=()=>`${i}-look-${t?`baked`:`live`}`,e.needsUpdate=!0}set(e){this.current=e,this.material.color.setHex(e.tint),this.uniforms.uLookHair.value.setHex(e.hair),this.uniforms.uLookFace.value.set(e.jaw,e.nose,e.brow,e.skull),this.uniforms.uLookGroom.value.set(e.shaved,e.beard)}get look(){return this.current}},ND=.026,PD=.038,FD=22,ID=[[.55,.85],[.55,.1],[.4,-.12],[-.4,-.12],[-.55,.05],[-.55,.6],[-.32,.62],[-.3,.16],[.3,.16],[.32,.85]],LD=.03;function RD(){let e=[],t=[],n=ID.length,r=new J,i=new J,a=new J;for(let t=0;t<=FD;t+=1){let n=t/FD*2-1;r.set(n*ND,0,PD*(1-n*n)-PD*.5),i.set(ND,0,-2*PD*n).normalize(),a.set(i.z,0,-i.x),a.dot(r.clone().setY(0).add(new J(0,0,PD*.5)))<0&&a.negate();for(let[t,n]of ID)e.push(r.x+a.x*t*.01,n*.01,r.z+a.z*t*.01)}for(let e=0;e=95:e===`hit`&&t>=110:!1}var BD=9.81,VD=.32,HD=4.5,UD=.04,WD=3;function GD(){return{out:!1,moving:!1,sliding:!1,x:0,y:-50,z:0,vx:0,vy:0,vz:0,wx:0,wy:0,wz:0,bounces:0,age:0,restYaw:0}}function KD(e,t,n,r){if(!e.out||!e.moving)return!1;e.age+=n;let i=r+.006;if(!e.sliding)e.vy-=BD*n,e.x+=e.vx*n,e.y+=e.vy*n,e.z+=e.vz*n,ZD(t,e.wx*n,e.wy*n,e.wz*n),e.y<=i&&(e.y=i,e.bounces.6?(e.bounces+=1,e.vy=-e.vy*VD,e.vx*=.72,e.vz*=.72,e.wx*=.55,e.wy*=.7,e.wz*=.55):(e.vy=0,e.sliding=!0));else{let r=Math.hypot(e.vx,e.vz),a=Math.max(0,r-HD*n);a<=UD?(e.vx=0,e.vz=0):(e.vx*=a/r,e.vz*=a/r),e.x+=e.vx*n,e.z+=e.vz*n,e.y=i,e.wy*=Math.exp(-4*n),e.restYaw+=e.wy*n,JD.setFromAxisAngle(qD,e.restYaw),t.slerp(JD,1-Math.exp(-12*n)),e.vx===0&&e.vz===0&&t.angleTo(JD)<.02&&(t.copy(JD),e.moving=!1)}return e.moving}var qD=new J(0,1,0),JD=new cm,YD=new cm,XD=new Hm;function ZD(e,t,n,r){YD.setFromEuler(XD.set(t,n,r)),e.multiply(YD).normalize()}var QD=900,$D=90,eO=96,tO=24,nO=18,rO=14,iO=5,aO=2,oO=4,sO=24,cO=16,lO=.12,uO=.35,dO=.04,fO=4,pO=9.81,mO=5,hO=2,gO=.09,_O=.22,vO=.7,yO=.45,bO=.6,xO=.9,SO=8,CO=.015,wO=.068,TO=.085,EO=.05,DO=1/60,OO={flesh:1,tooth:3,brain:1,shard:2},kO=.3,AO=6.5,jO=7.5,MO=5,NO=4,PO=1.7,FO=.14,IO=110,LO=.12,RO=4,zO=6,BO=.35,VO=new J,HO=new J,UO=new J,WO=new J;function GO(e,t){let n=e.mesh.geometry===e.flesh?e.flesh.getAttribute(`normal`):void 0;return n!==void 0&&n.count>0?t.fromBufferAttribute(n,0):t.set(0,1,0).applyQuaternion(e.mesh.quaternion),t.lengthSq()>1e-8?t.normalize():t.set(0,1,0)}var KO=e=>()=>(e=Math.imul(e^e>>>15,1|e),e^=e+Math.imul(e^e>>>7,61|e),((e^e>>>14)>>>0)/4294967296),Z=(e,t=0)=>Number.isFinite(e)?e:t,qO=()=>({alive:!1,x:0,y:-50,z:0,vx:0,vy:0,vz:0,rx:0,ry:0,rz:0,vrx:0,vry:0,vrz:0,scale:0,stretch:1,bounces:0,stained:!1,kind:`flesh`,resting:!1,born:0,owner:-1,fading:-1}),JO=new Y(16054267),YO=.012,XO=.055,ZO=.985,QO=new q(.097,.132),$O=.012,ek={r:.82,g:.86,b:.9};function tk(e){if(typeof e==`number`)return{x:e<0?-1:1,z:0};let t=Math.hypot(e.x,e.z);return Number.isFinite(t)&&t>1e-6?{x:e.x/t,z:e.z/t}:{x:1,z:0}}var nk=new J(0,1,0),rk=new J(0,0,1),ik=e=>Number.isFinite(e)?K.clamp(e,0,EO):0,ak=new Set([`hit`,`counter_hit`,`block`,`perfect_block`,`guard_break`,`knockdown`,`bleed`]);function ok(e,t,n,r){let i=Math.abs(e)-r;if(i<=0)return{position:e,velocity:t};let a=Math.sign(e);return Math.abs(n)<=r?{position:a*r,velocity:t*-.42}:{position:e,velocity:-a*Math.max(-a*t,i*fO)}}function sk(e){if(e.kind===`bleed`)return`ooze`;if(e.kind===`knockdown`)return`impact`;let t=e.detail.split(`:`);if(t.at(-1)===`body`)return`body_burst`;let n=t.find(e=>[`jab`,`straight`,`hook`,`uppercut`].includes(e));return n===`hook`?`fan`:n===`uppercut`?`plume`:`jet`}function ck(e,t){e.set(Z(t.x),Z(t.y),Z(t.z),Z(t.w,1)),e.lengthSq()<1e-6?e.identity():e.normalize()}function lk(){return new Fv({color:9063730,roughness:.58,metalness:.02,clearcoat:.25,clearcoatRoughness:.6})}function uk(e,t,n){n.identity();let r=e.getAttribute(`position`),i=e.getAttribute(`bindPosition`),a=e.getIndex();if(i===void 0||i.count!==r.count)return n;let o=new Uint8Array(r.count);if(a===null)o.fill(1);else for(let e=0;eObject.assign(new Ig(e.geometry,e.material),{castShadow:e.castShadow}));if(e.push(Object.assign(new Ig(this.stumpGeometry,this.jawMaterial),{castShadow:this.stumps[0].mesh.castShadow})),this.bakedStandIns===null){let e=new Em,t=lk();t.map=e,new oD(t,UE,{core:ZE,lids:$E,baked:!0}),new MD(t,!0);let n=new Pv({color:1920728,roughness:.38,metalness:.03,map:e});mE(n,!0),this.bakedStandIns={map:e,materials:[t,n]}}for(let t of this.bakedStandIns.materials)e.push(Object.assign(new Ig(this.headGeometry,t),{castShadow:!0}));return e}setBloodLevel(e){if(e===this.bloodLevel)return;let t=this.bloodLevel;this.bloodLevel=e,(e===`off`||t===`full`&&e===`reduced`)&&this.clearArcadeGore()}get shakeAmount(){return this.shake}get liveParticles(){return this.droplets.filter(e=>e.alive).length}get liveBloodParticles(){return this.droplets.filter(e=>e.alive&&e.blood).length}get liveMist(){return this.mists.filter(e=>e.alive).length}get canvasStains(){return this.canvasBlood.stains}useUploader(e){this.canvasBlood.useUploader(e)}setLowTier(e){this.canvasBlood.setLowTier(e)}get liveGibs(){return this.gibs.filter(e=>e.alive).length}get liveShards(){return this.shards.filter(e=>e.alive).length}debrisCount(e){return[...this.gibs,...this.shards].filter(t=>t.alive&&t.kind===e).length}get activeHeads(){return this.heads.filter(e=>e.active).length}headBurst(e){let t=this.stumps[Math.trunc(e)];return t!==void 0&&t.active&&t.mesh.material===this.jawMaterial}severedHeadPosition(e,t){let n=this.heads[Math.trunc(e)];return n===void 0||!n.active?!1:(t.copy(n.mesh.position),!0)}severedHeadFacing(e,t){let n=this.heads[Math.trunc(e)];return n===void 0||!n.active?!1:(t.set(0,0,1).applyQuaternion(n.mesh.quaternion),!0)}mouthpieceOut(e){return this.shields[Math.trunc(e)]?.out===!0}mouthpiecePosition(e,t){let n=this.shields[Math.trunc(e)];return n===void 0||!n.out?!1:(t.set(n.x,n.y,n.z),!0)}ejectMouthpiece(e,t,n,r,i,a,o=!1){if(e<0||e>=aO)return!1;let s=Math.trunc(e),c=this.shields[s],l=Number.isSafeInteger(i)?i:0;if(c.out&&!o||this.lastShieldEvent[s]===l)return!1;this.lastShieldEvent[s]=l;let u=KO(l*48271+s*7907+5),d=tk(r),f=.9+u()*.9,p=.9+u()*.9,m=(u()-.5)*1.1;Object.assign(c,GD(),{out:!0,moving:!0,x:Z(t.x),y:Z(t.y,1.45),z:Z(t.z),vx:f*d.x-m*d.z,vy:p,vz:f*d.z+m*d.x,wx:(u()-.5)*26,wy:(u()-.5)*18,wz:(u()-.5)*26,restYaw:u()*Math.PI*2});let h=this.shieldMeshes[s];h.position.set(c.x,c.y,c.z),ck(h.quaternion,n),h.visible=!0,this.shieldMaterials[s].color.setHex(a).lerp(JO,.55);let g=this.bloodLevel===`full`;for(let e=0;e<(g?22:12);e+=1){let t=u()*Math.PI*2,n=g?e%3!=0:this.bloodLevel===`reduced`&&e%3==0;this.spawnDroplet(c.x+(u()-.5)*.03,c.y+(u()-.5)*.03,c.z+(u()-.5)*.03,c.vx*(.5+u()*.6)+Math.sin(t)*.4,c.vy*(.4+u()*.6),c.vz*.5+Math.cos(t)*.4,n?EE(u()):ek,.5+u()*.5,n)}return!0}gougeEye(e,t,n,r,i){if(this.bloodLevel!==`full`||e<0||e>=aO)return;let a=Math.trunc(e),o=this.eyes[a],s=Number.isSafeInteger(i)?i:0;if(o.active||o.eventId===s)return;o.active=!0,o.eventId=s,o.socket.set(Z(t.x),Z(t.y,1.6),Z(t.z)),o.facing.copy(n),o.hasSkull=!1,o.position.copy(o.socket).addScaledVector(n,YO*1.5),this.eyeDirection.copy(n).multiplyScalar(1.3);let c=tk(r);this.eyeDirection.x+=c.x*.5,this.eyeDirection.z+=c.z*.5,o.previous.copy(o.position).addScaledVector(this.eyeDirection,-.016666666666666666),o.bleeding=2,o.accumulator=0,this.eyeMeshes[a].visible=!0,this.nerveMeshes[a].visible=!0;let l=KO(s*52711+a*3571+13);for(let e=0;e<36;e+=1){let e=l()*Math.PI*2;this.spawnDroplet(o.socket.x,o.socket.y,o.socket.z,n.x*(.8+l())+Math.sin(e)*.6,.4+l()*1.2,n.z*(.8+l())+Math.cos(e)*.6,EE(l()),.6+l()*.6,!0)}for(let e=0;e<4;e+=1)this.spawnMist(o.socket.x+(l()-.5)*.1,o.socket.y+(l()-.5)*.08,o.socket.z+(l()-.5)*.1,.7+l()*.6,.5+l()*.4);this.writeEye(a)}anchorEye(e,t,n,r,i){let a=this.eyes[Math.trunc(e)];a===void 0||!a.active||(a.socket.set(Z(t.x),Z(t.y,1.6),Z(t.z)),n!==void 0&&n.lengthSq()>1e-8&&a.facing.copy(n).normalize(),r!==void 0&&i!==void 0&&(a.skull.set(Z(r.x),Z(r.y,1.6),Z(r.z)),ck(a.skullTurn,i),a.hasSkull=!0))}eyeOut(e){return this.eyes[Math.trunc(e)]?.active===!0}eyePosition(e,t){let n=this.eyes[Math.trunc(e)];return n===void 0||!n.active?!1:(t.copy(n.position),!0)}updateEyes(e){for(let[t,n]of this.eyes.entries()){if(!n.active)continue;this.eyeDirection.copy(n.position).sub(n.previous).multiplyScalar(ZO),n.previous.copy(n.position),n.position.add(this.eyeDirection),n.position.y-=pO*e*e,this.eyeDirection.copy(n.position).sub(n.socket);let r=this.eyeDirection.length();if(r>.055&&n.position.copy(n.socket).addScaledVector(this.eyeDirection,XO/r),n.hasSkull&&this.restOnFace(n)){n.previous.lerp(n.position,.6),this.eyeDirection.copy(n.position).sub(n.socket);let e=this.eyeDirection.length();e>.055&&n.position.copy(n.socket).addScaledVector(this.eyeDirection,XO/e)}if(n.position.y<0+YO&&(n.position.y=0+YO),this.bloodLevel===`full`&&n.bleeding>0)for(n.bleeding=Math.max(0,n.bleeding-e),n.accumulator+=e*14;n.accumulator>=1;)--n.accumulator,this.spawnDroplet(n.socket.x,n.socket.y-.005,n.socket.z,(this.ambientRandom()-.5)*.2,-.2-this.ambientRandom()*.3,(this.ambientRandom()-.5)*.2,EE(this.ambientRandom()),.9,!0);this.writeEye(t)}}restOnFace(e){this.eyeTurn.copy(e.skullTurn).invert();let t=this.eyeDirection.copy(e.position).sub(e.skull).applyQuaternion(this.eyeTurn),n=this.eyeSocketLocal.copy(e.socket).sub(e.skull).applyQuaternion(this.eyeTurn).z+YO+$O;if(t.z>=n)return!1;let r=Math.hypot(t.x/QO.x,t.y/QO.y);return r>=1?!1:(n-t.z<=(1-r)*Math.min(QO.x,QO.y)||r<1e-6?t.z=n:(t.x/=r,t.y/=r),e.position.copy(e.skull).add(t.applyQuaternion(e.skullTurn)),!0)}writeEye(e){let t=this.eyes[e],n=this.eyeMeshes[e],r=this.nerveMeshes[e];n.position.copy(t.position),this.eyeDirection.copy(t.position).sub(t.socket);let i=this.eyeDirection.length();i>1e-5&&(this.eyeDirection.multiplyScalar(1/i),r.quaternion.setFromUnitVectors(nk,this.eyeDirection),this.eyeDirection.addScaledVector(t.facing,.9).normalize(),n.quaternion.setFromUnitVectors(rk,this.eyeDirection)),r.position.copy(t.socket),r.scale.set(1,Math.max(.001,i-YO*.8),1)}returnMouthpiece(e){let t=Math.trunc(e),n=this.shields[t];n!==void 0&&(Object.assign(n,GD()),this.shieldMeshes[t].visible=!1,this.lastShieldEvent[t]=null)}clearMouthpieces(){for(let[e,t]of this.shields.entries())Object.assign(t,GD()),this.shieldMeshes[e].visible=!1,this.lastShieldEvent[e]=null}updateShields(e){for(let[t,n]of this.shields.entries()){if(!n.out||!n.moving)continue;let r=this.shieldMeshes[t],i=n.age<.3&&!n.sliding,a=n.x,o=n.z;KD(n,r.quaternion,e,0);let s=ww-LD-dO,c=ok(n.x,n.vx,a,s),l=ok(n.z,n.vz,o,s);if(n.x=c.position,n.vx=c.velocity,n.z=l.position,n.vz=l.velocity,r.position.set(n.x,n.y,n.z),i&&this.ambientRandom()e.active).length}get activeStumps(){return[...this.stumps,...this.handStumps].filter(e=>e.active).length}spawnDroplet(e,t,n,r,i,a,o,s,c,l=c?.0035+this.ambientRandom()**3*.0125:.0025+this.ambientRandom()**2*.004){let u=this.liveDroplets1e-4?(this.dropletVelocity.set(t.vx/n,t.vy/n,t.vz/n),this.dropletQuaternion.setFromUnitVectors(nk,this.dropletVelocity)):this.dropletQuaternion.identity();let r=Math.min(1,t.life/(t.maxLife*.3))*this.dropletCloseness,i=wE(t.radius*r,n,this.dropletShape);this.dropletScale.set(i.width,i.length,i.width),this.dropletMatrix.compose(this.dropletVelocity.set(t.x,t.y,t.z),this.dropletQuaternion,this.dropletScale),this.dropletMesh.setMatrixAt(e,this.dropletMatrix)}spawnMist(e,t,n,r,i){let a=this.mistIndex%$D;this.mistIndex+=1;let o=this.mists[a];o.alive=!0,o.x=Z(e),o.y=Z(t,1),o.z=Z(n),o.maxLife=Math.max(.05,Z(i,.5)),o.life=o.maxLife,o.scale=Math.max(.1,Z(r,1)),this.mistPositions[a*3]=o.x,this.mistPositions[a*3+1]=o.y,this.mistPositions[a*3+2]=o.z,this.mistColors[a*4+3]=Math.min(1,o.scale),this.mistGeometry.attributes.position.needsUpdate=!0,this.mistGeometry.attributes.color.needsUpdate=!0}placeDecal(e,t,n,r,i,a,o){this.canvasBlood.stain(Z(e),Z(t),Math.max(.01,Z(n,.2))*.28,Math.max(.01,Z(r,.15))*.28,Z(i),K.clamp(Z(a,.4),0,.95),o)}ambientRandom(){return this.ambientSeed=Math.imul(this.ambientSeed^this.ambientSeed>>>15,1|this.ambientSeed),this.ambientSeed^=this.ambientSeed+Math.imul(this.ambientSeed^this.ambientSeed>>>7,61|this.ambientSeed),((this.ambientSeed^this.ambientSeed>>>14)>>>0)/4294967296}splatter(e,t,n,r=()=>this.ambientRandom(),i=12,a=xO){if(this.bloodLevel===`off`)return;let o=this.bloodLevel===`reduced`,s=o?Math.ceil(i/4):i,c=o?.35:1;for(let i=0;i0&&t<=hO){let e=1-gO*t;this.spawnDroplet(f.x,f.y,f.z,f.vx*e,f.vy*e,f.vz*e,f.color,f.life,!0,f.radius*(1-_O*t));continue}let n=t===0,r=(i()-.5)*Math.PI,a=n?2.2+i()*1.8:.9+i()*1.5,c=a,u=.5+i()*1.25,d=(i()-.5)*.5;if(o===`fan`)c=a*(.45+Math.cos(r)*.35),u=.55+i()*1.15,d=Math.sin(r)*a*.9;else if(o===`plume`)c=a*(.25+i()*.25),u=1.8+i()*2.2,d=(i()-.5)*a*.65;else if(o===`body_burst`)c=a*(.55+i()*.45),u=-.15+i()*1.05,d=Math.sin(r)*a*.75;else if(o===`ooze`)c=.08+i()*.22,u=-.3-i()*.55,d=(i()-.5)*.22;else if(o===`impact`)c=a*(.45+i()*.65),u=.9+i()*2.1,d=Math.sin(r)*a;else{let e=n?.12:.34;c=a*(.8+i()*.35),u=n?1.3+i()*1.6:.5+i()*1.25,d=(i()-.5)*a*e}let p=c*s.x-d*s.z,m=c*s.z+d*s.x,h=EE(i()),g=l.x+(i()-.5)*.12,_=l.y+(i()-.5)*.14,v=l.z+(i()-.5)*.12,y=.65+i()*.85;if(!n){this.spawnDroplet(g,_,v,p,u,m,h,y,!0);continue}f.x=g,f.y=_,f.z=v,f.vx=p,f.vy=u,f.vz=m,f.color=h,f.life=y,f.radius=.008+i()*.008,this.spawnDroplet(g,_,v,p,u,m,h,y,!0,f.radius)}let g=e.kind===`hit`&&e.amount>=70||e.kind===`knockdown`||e.kind===`counter_hit`||m>=SO*2;if(!n&&e.blood>0&&this.bloodLevel!==`off`&&g){let t=this.bloodLevel===`reduced`?e.kind===`knockdown`?3:2:e.kind===`knockdown`?14:10+Math.min(8,Math.floor(m/4)),n=this.bloodLevel===`reduced`?.35:1;for(let e=0;e0&&this.bloodLevel!==`off`){let t=e.kind===`knockdown`;this.splatter(l.x+(i()-.5)*.6,l.z+(i()-.5)*.6,t?1+Math.min(2,e.blood/55):.8+Math.min(1.2,m/30),i,_,t?bO:yO)}let v=e.amount>=70||e.kind===`counter_hit`&&e.amount>=gE,y=p&&(v&&m>=SO||e.kind===`counter_hit`&&e.amount>=95);if(this.bloodLevel===`full`&&!n&&y&&!e.detail.endsWith(`:body`)){let t=Math.min(8,Math.max(2,Math.round((e.amount-30)/10)));for(let e=0;e0){e.sideLow=1/0,e.sideHigh=-1/0;for(let t=0;t=aO)return;let s=Math.trunc(e),c=Number.isSafeInteger(i)?i:0,l=this.heads[s];if(l.active||this.lastDecapitationEvent[s]===c)return;this.applyBakedPart(l,o,a);let u=KO(c*104729+s*8191+23),d=tk(r);this.lastDecapitationEvent[s]=c,l.active=!0,l.moving=!0,l.eventId=c;let f=1.8+u()*1.1;l.vy=2.3+u()*1.1;let p=(u()-.5)*2.2;l.vx=f*d.x-p*d.z,l.vz=f*d.z+p*d.x,l.vrx=(u()-.5)*12,l.vry=(u()-.5)*12,l.vrz=(u()-.5)*12,l.bounces=0,l.stained=!1,l.settle=-1,l.mesh.position.set(Z(t.x),Z(t.y,1.55),Z(t.z)),ck(l.mesh.quaternion,n),l.mesh.visible=!0;let m=this.stumps[s];m.mesh.material=this.stumpMaterial,m.active=!0,m.fountainLife=AO,m.accumulator=0,m.seed=c|1,m.direction=d,m.mesh.position.copy(l.mesh.position),m.mesh.quaternion.copy(l.mesh.quaternion),m.mesh.visible=!0;for(let e=0;e=aO)return;let i=Math.trunc(e),a=Number.isSafeInteger(r)?r:0,o=this.stumps[i];if(o.active||this.lastBurstEvent[i]===a)return;this.lastBurstEvent[i]=a;let s=KO(a*91813+i*6151+41),c=tk(n),l=Z(t.x),u=Z(t.y,1.6),d=Z(t.z),f=(e,t,n,r,a)=>{for(let o=0;o=aO)return;let c=Math.trunc(e)*2+(t===`left`?0:1),l=Number.isSafeInteger(a)?a:0,u=this.hands[c];if(u.active||this.lastDismembermentEvent[c]===l)return;this.applyBakedPart(u,s,o);let d=KO(l*130363+c*12289+37),f=tk(i);this.lastDismembermentEvent[c]=l,u.active=!0,u.moving=!0,u.eventId=l;let p=1.3+d()*1.1;u.vy=1.5+d()*1.2;let m=(d()-.5)*1.8;u.vx=p*f.x-m*f.z,u.vz=p*f.z+m*f.x,u.vrx=(d()-.5)*16,u.vry=(d()-.5)*16,u.vrz=(d()-.5)*16,u.bounces=0,u.stained=!1,u.mesh.position.set(Z(n.x),Z(n.y,1.25),Z(n.z)),ck(u.mesh.quaternion,r),u.mesh.visible=!0;let h=this.handStumps[c];h.active=!0,h.fountainLife=MO,h.accumulator=0,h.seed=l|1,h.direction=f,h.mesh.position.copy(u.mesh.position),h.mesh.quaternion.copy(u.mesh.quaternion),h.mesh.visible=!0;for(let t=0;t=aO)return;let i=Math.trunc(e)*2+(t===`left`?0:1),a=this.handStumps[i];a.active&&(a.mesh.position.set(Z(n.x),Z(n.y,1.2),Z(n.z)),ck(a.mesh.quaternion,r))}anchorStump(e,t,n,r,i){if(this.bloodLevel!==`full`||e<0||e>=aO)return;let a=this.stumps[Math.trunc(e)];if(a.active){if(a.mesh.position.set(Z(t.x),Z(t.y,1.5),Z(t.z)),r===void 0||r.length<18){a.mesh.geometry=this.stumpGeometry,ck(a.mesh.quaternion,n);return}IE(a.flesh,r,a.mesh.position,this.stumpOutward.set(0,1,0).applyQuaternion(n),.01,i),a.mesh.geometry=a.flesh,a.mesh.quaternion.identity()}}restoreFighter(e){if(e<0||e>=aO)return;let t=Math.trunc(e),n=this.heads[t];n.active=!1,n.moving=!1,n.mesh.visible=!1,n.mesh.position.y=-50,this.applyBakedPart(n,void 0,9063730);let r=this.stumps[t];r.active=!1,r.fountainLife=0,r.accumulator=0,r.mesh.visible=!1,r.mesh.position.y=-50,r.mesh.material=this.stumpMaterial,this.clearGibs(t);let i=this.eyes[t];i.active=!1,i.bleeding=0,this.eyeMeshes[t].visible=!1,this.nerveMeshes[t].visible=!1;for(let e of[t*2,t*2+1]){let t=this.hands[e];t.active=!1,t.moving=!1,t.mesh.visible=!1,t.mesh.position.y=-50,this.applyBakedPart(t,void 0,1920728);let n=this.handStumps[e];n.active=!1,n.fountainLife=0,n.accumulator=0,n.mesh.visible=!1,n.mesh.position.y=-50}}drip(e,t,n,r=0){let i=+(r===1);if(this.bloodLevel===`off`||n||t<=.05){this.stopDrip(i);return}let a=this.dripEmitters[i];a.position.set(Z(e.x),Z(e.y,1.5),Z(e.z)),a.rate=this.bloodLevel===`reduced`?Math.min(3,t*3):Math.min(18,t*14),a.active=!0}stopDrip(e){let t=this.dripEmitters[+(e===1)];t.active=!1,t.rate=0,t.accumulator=0}updateDripEmitters(e){for(let t of this.dripEmitters)if(t.active)for(t.accumulator+=e*t.rate;t.accumulator>=1;)--t.accumulator,this.spawnDroplet(t.position.x+(this.ambientRandom()-.5)*.14,t.position.y-.04,t.position.z+(this.ambientRandom()-.5)*.1,(this.ambientRandom()-.5)*.08,-.25-this.ambientRandom()*.4,(this.ambientRandom()-.5)*.08,EE(this.ambientRandom()),1.1,!0)}spawnTeeth(e,t,n,r){if(this.bloodLevel===`off`)return;let i=KO((Number.isSafeInteger(r)?r:0)*7331+91),a=tk(t),o=Math.min(4,Math.max(1,Math.round(n*(this.bloodLevel===`reduced`?.5:1))));for(let t=0;t>3)&&n!==null&&(n.fading=kO)}sprayGib(e,t,n,r,i,a,o,s,c=`flesh`,l=-1){this.spawnGib(t,n,r,i*e.x-o*e.z,a,i*e.z+o*e.x,s,c,l)}writeDebrisMatrix(e,t,n){if(!n.alive)this.gibPosition.set(0,-50,0),this.gibQuaternion.identity(),this.gibScale.setScalar(0);else{this.gibPosition.set(n.x,n.y,n.z),this.gibEuler.set(n.rx,n.ry,n.rz),this.gibQuaternion.setFromEuler(this.gibEuler);let e=n.fading>=0?n.scale*Math.max(0,n.fading)/kO:n.scale;this.gibScale.set(e*n.stretch,e,e/Math.sqrt(n.stretch))}this.gibMatrix.compose(this.gibPosition,this.gibQuaternion,this.gibScale),e.setMatrixAt(t,this.gibMatrix)}updateDebris(e,t,n){let r=!1;for(let[i,a]of e.entries())if(!(!a.alive||a.resting&&a.fading<0)){if(r=!0,a.fading>=0){a.fading-=n,a.fading<=0&&(a.alive=!1),this.writeDebrisMatrix(t,i,a);continue}a.vy-=6.4*n,a.x+=a.vx*n,a.y+=a.vy*n,a.z+=a.vz*n,a.rx+=a.vrx*n,a.ry+=a.vry*n,a.rz+=a.vrz*n,this.confineGib(a),a.y<=.015&&(a.y=.015,!a.stained&&this.bloodLevel===`full`&&a.kind!==`tooth`&&(a.stained=!0,this.placeDecal(a.x,a.z,.24+a.scale*.16,.14+a.scale*.1,a.rz,.55,6425104)),a.bounces.35?(a.bounces+=1,a.vy=Math.abs(a.vy)*.3,a.vx*=.68,a.vz*=.68,a.vrx*=.7,a.vry*=.7,a.vrz*=.7):(a.vx=0,a.vy=0,a.vz=0,a.vrx=a.vry=a.vrz=0,a.kind===`shard`&&(a.rx=Math.round(a.rx/Math.PI)*Math.PI),a.resting=!0)),this.writeDebrisMatrix(t,i,a)}r&&(t.instanceMatrix.needsUpdate=!0)}confineGib(e){Math.abs(e.x)>3.05&&(e.x=Math.sign(e.x)*bw,e.vx*=-.45),Math.abs(e.z)>3.05&&(e.z=Math.sign(e.z)*bw,e.vz*=-.45)}updateDetachedParts(e,t){for(let n of e){if(n.active&&n.settle>=0&&n.settle<1){n.settle=Math.min(1,n.settle+t/uO);let e=n.settle*n.settle*(3-2*n.settle);n.mesh.quaternion.slerpQuaternions(n.settleFrom,n.settleTo,e),n.mesh.position.y=n.settleFromY+(n.settleToY-n.settleFromY)*e}if(!n.active||!n.moving)continue;let e=n.mesh.position.x,r=n.mesh.position.z;n.vy-=5.8*t,n.mesh.position.x+=n.vx*t,n.mesh.position.y+=n.vy*t,n.mesh.position.z+=n.vz*t,n.mesh.rotation.x+=n.vrx*t,n.mesh.rotation.y+=n.vry*t,n.mesh.rotation.z+=n.vrz*t;let i=ww-n.radius-dO,a=ok(n.mesh.position.x,n.vx,e,i),o=ok(n.mesh.position.z,n.vz,r,i);if(n.mesh.position.x=a.position,n.mesh.position.z=o.position,n.vx=a.velocity,n.vz=o.velocity,n.mesh.position.y<=0+n.radius){if(n.mesh.position.y=0+n.radius,!n.stained&&this.bloodLevel===`full`){n.stained=!0;let e=n.radius===lO?1:.55;this.placeDecal(n.mesh.position.x,n.mesh.position.z,1.3*e,.9*e,n.mesh.rotation.z,.85,4392199)}n.bounces<2&&Math.abs(n.vy)>.42?(n.bounces+=1,n.vy=Math.abs(n.vy)*.32,n.vx*=.62,n.vz*=.62,n.vrx*=.65,n.vry*=.65,n.vrz*=.65):(n.moving=!1,n.vx=0,n.vy=0,n.vz=0,n.vrx=0,n.vry=0,n.vrz=0,n.look!==null&&this.rollOntoEar(n))}(!Number.isFinite(n.mesh.position.x)||!Number.isFinite(n.mesh.position.y)||!Number.isFinite(n.mesh.position.z)||!Number.isFinite(n.mesh.quaternion.x)||!Number.isFinite(n.mesh.quaternion.y)||!Number.isFinite(n.mesh.quaternion.z)||!Number.isFinite(n.mesh.quaternion.w))&&(n.mesh.position.set(0,0+n.radius,0),n.mesh.quaternion.identity(),n.moving=!1,n.vx=0,n.vy=0,n.vz=0)}}rollOntoEar(e){let t=this.settleAxis.set(1,0,0).applyQuaternion(e.mesh.quaternion),n=t.y>=0?1:-1;e.settleFrom.copy(e.mesh.quaternion),e.settleTo.setFromUnitVectors(t,this.settleUp.set(0,n,0)).multiply(e.mesh.quaternion),e.settleFromY=e.mesh.position.y,e.settleToY=0+(n>0?-e.sideLow:e.sideHigh),e.settle=0}stumpRandom(e){let t=e.seed|0;return t=Math.imul(t^t>>>15,1|t),t^=t+Math.imul(t^t>>>7,61|t),e.seed=t|0,((t^t>>>14)>>>0)/4294967296}updateStumpFountains(e){if(this.bloodLevel===`full`)for(let t of[...this.stumps,...this.handStumps]){if(!t.active||t.fountainLife<=0)continue;t.fountainLife=Math.max(0,t.fountainLife-e);let n=Math.min(1,t.fountainLife/NO),r=t.fountainLife*PO%1,i=Math.exp(-(((r-.5)/FO)**2));if(t.accumulator+=e*IO*n*(LO+(1-LO)*i),t.accumulator<1)continue;let a=GO(t,VO);for(HO.set(Math.abs(a.y)<.9?0:1,+(Math.abs(a.y)<.9),0).cross(a).normalize(),UO.crossVectors(a,HO);t.accumulator>=1;){--t.accumulator;let e=this.stumpRandom(t),r=this.stumpRandom(t),o=this.stumpRandom(t),s=Math.sqrt(e)*BO,c=r*Math.PI*2;WO.copy(a).multiplyScalar(Math.cos(s)).addScaledVector(HO,Math.sin(s)*Math.cos(c)).addScaledVector(UO,Math.sin(s)*Math.sin(c)).multiplyScalar(K.lerp(RO,zO,o)*(.7+.3*i)*(.55+.45*n)),this.spawnDroplet(t.mesh.position.x+a.x*.015+(e-.5)*.03,t.mesh.position.y+a.y*.015,t.mesh.position.z+a.z*.015+(r-.5)*.03,WO.x,WO.y,WO.z,EE(e),1.2+o*.6,!0,.004+o*o*.01)}}}update(e){for(this.canvasBlood.update(ik(e)),this.simulationRemainder+=ik(e);this.simulationRemainder+1e-9>=DO;)this.simulationRemainder=Math.max(0,this.simulationRemainder-DO),this.simulate(DO)}simulate(e){this.shake*=.02**e,this.updateDripEmitters(e),this.updateStumpFountains(e),this.updateDetachedParts(this.heads,e),this.updateDetachedParts(this.hands,e),this.updateShields(e),this.updateEyes(e),this.updateDebris(this.gibs,this.gibMesh,e),this.updateDebris(this.shards,this.shardMesh,e);let t=this.liveDroplets,n=!1;for(let t=0;t0&&(this.dropletBuffers.position.needsUpdate=!0,this.dropletBuffers.color.needsUpdate=!0,this.uploadDroplets(0,this.liveDroplets,n));let r=!1;for(let[t,n]of this.mists.entries()){if(!n.alive)continue;if(r=!0,n.life-=e,n.life<=0){n.alive=!1,this.mistPositions[t*3+1]=-50;continue}let i=1-n.life/n.maxLife;this.mistPositions[t*3]=n.x,this.mistPositions[t*3+1]=n.y+i*.14,this.mistPositions[t*3+2]=n.z,this.mistColors[t*4+3]=Math.max(0,1-i)*Math.min(1,n.scale)}r&&(this.mistGeometry.attributes.position.needsUpdate=!0,this.mistGeometry.attributes.color.needsUpdate=!0)}clearDroplets(e){let t=this.liveDroplets;for(let t=0;t=2.0 are supported.`));return}let c=new fA(i,{path:t||this.resourcePath||``,crossOrigin:this.crossOrigin,requestHeader:this.requestHeader,manager:this.manager,ktx2Loader:this.ktx2Loader,meshoptDecoder:this.meshoptDecoder});c.fileLoader.setRequestHeader(this.requestHeader);for(let e=0;e=0&&o[t]===void 0&&console.warn(`THREE.GLTFLoader: Unknown extension "`+t+`".`)}}c.setExtensions(a),c.setPlugins(o),c.parse(n,r)}parseAsync(e,t){let n=this;return new Promise(function(r,i){n.parse(e,t,r,i)})}};function gk(){let e={};return{get:function(t){return e[t]},add:function(t,n){e[t]=n},remove:function(t){delete e[t]},removeAll:function(){e={}}}}function _k(e,t,n){let r=e.json.materials[t];return r.extensions&&r.extensions[n]?r.extensions[n]:null}var vk={KHR_BINARY_GLTF:`KHR_binary_glTF`,KHR_DRACO_MESH_COMPRESSION:`KHR_draco_mesh_compression`,KHR_LIGHTS_PUNCTUAL:`KHR_lights_punctual`,KHR_MATERIALS_CLEARCOAT:`KHR_materials_clearcoat`,KHR_MATERIALS_DISPERSION:`KHR_materials_dispersion`,KHR_MATERIALS_IOR:`KHR_materials_ior`,KHR_MATERIALS_SHEEN:`KHR_materials_sheen`,KHR_MATERIALS_SPECULAR:`KHR_materials_specular`,KHR_MATERIALS_TRANSMISSION:`KHR_materials_transmission`,KHR_MATERIALS_IRIDESCENCE:`KHR_materials_iridescence`,KHR_MATERIALS_ANISOTROPY:`KHR_materials_anisotropy`,KHR_MATERIALS_UNLIT:`KHR_materials_unlit`,KHR_MATERIALS_VOLUME:`KHR_materials_volume`,KHR_TEXTURE_BASISU:`KHR_texture_basisu`,KHR_TEXTURE_TRANSFORM:`KHR_texture_transform`,KHR_MESH_QUANTIZATION:`KHR_mesh_quantization`,KHR_MATERIALS_EMISSIVE_STRENGTH:`KHR_materials_emissive_strength`,EXT_MATERIALS_BUMP:`EXT_materials_bump`,EXT_TEXTURE_WEBP:`EXT_texture_webp`,EXT_TEXTURE_AVIF:`EXT_texture_avif`,EXT_MESHOPT_COMPRESSION:`EXT_meshopt_compression`,KHR_MESHOPT_COMPRESSION:`KHR_meshopt_compression`,EXT_MESH_GPU_INSTANCING:`EXT_mesh_gpu_instancing`},yk=class{constructor(e){this.parser=e,this.name=vk.KHR_LIGHTS_PUNCTUAL,this.cache={refs:{},uses:{}}}_markDefs(){let e=this.parser,t=this.parser.json.nodes||[];for(let n=0,r=t.length;n=0)throw Error(`THREE.GLTFLoader: setKTX2Loader must be called before loading KTX2 textures`);return null}return t.loadTextureImage(e,i.source,a)}},Nk=class{constructor(e){this.parser=e,this.name=vk.EXT_TEXTURE_WEBP}loadTexture(e){let t=this.name,n=this.parser,r=n.json,i=r.textures[e];if(!i.extensions||!i.extensions[t])return null;let a=i.extensions[t],o=r.images[a.source],s=n.textureLoader;if(o.uri){let e=n.options.manager.getHandler(o.uri);e!==null&&(s=e)}return n.loadTextureImage(e,a.source,s)}},Pk=class{constructor(e){this.parser=e,this.name=vk.EXT_TEXTURE_AVIF}loadTexture(e){let t=this.name,n=this.parser,r=n.json,i=r.textures[e];if(!i.extensions||!i.extensions[t])return null;let a=i.extensions[t],o=r.images[a.source],s=n.textureLoader;if(o.uri){let e=n.options.manager.getHandler(o.uri);e!==null&&(s=e)}return n.loadTextureImage(e,a.source,s)}},Fk=class{constructor(e,t){this.name=t,this.parser=e}loadBufferView(e){let t=this.parser.json,n=t.bufferViews[e];if(n.extensions&&n.extensions[this.name]){let e=n.extensions[this.name],r=this.parser.getDependency(`buffer`,e.buffer),i=this.parser.options.meshoptDecoder;if(!i||!i.supported){if(t.extensionsRequired&&t.extensionsRequired.indexOf(this.name)>=0)throw Error(`THREE.GLTFLoader: setMeshoptDecoder must be called before loading compressed files`);return null}return r.then(function(t){let n=e.byteOffset||0,r=e.byteLength||0,a=e.count,o=e.byteStride,s=new Uint8Array(t,n,r);return i.decodeGltfBufferAsync?i.decodeGltfBufferAsync(a,o,s,e.mode,e.filter).then(function(e){return e.buffer}):i.ready.then(function(){let t=new ArrayBuffer(a*o);return i.decodeGltfBuffer(new Uint8Array(t),a,o,s,e.mode,e.filter),t})})}else return null}},Ik=class{constructor(e){this.name=vk.EXT_MESH_GPU_INSTANCING,this.parser=e}createNodeMesh(e){let t=this.parser.json,n=t.nodes[e];if(!n.extensions||!n.extensions[this.name]||n.mesh===void 0)return null;let r=t.meshes[n.mesh];for(let e of r.primitives)if(e.mode!==qk.TRIANGLES&&e.mode!==qk.TRIANGLE_STRIP&&e.mode!==qk.TRIANGLE_FAN&&e.mode!==void 0)return null;let i=n.extensions[this.name].attributes,a=[],o={};for(let e in i)a.push(this.parser.getDependency(`accessor`,i[e]).then(t=>(o[e]=t,o[e])));return a.length<1?null:(a.push(this.parser.createNodeMesh(e)),Promise.all(a).then(e=>{let t=e.pop(),n=t.isGroup?t.children:[t],r=e[0].count,i=[];for(let e of n){let t=new Mm,n=new J,a=new cm,s=new J(1,1,1),c=new c_(e.geometry,e.material,r);for(let e=0;e0||e.search(/^data\:image\/jpeg/)===0?`image/jpeg`:e.search(/\.webp($|\?)/i)>0||e.search(/^data\:image\/webp/)===0?`image/webp`:e.search(/\.ktx2($|\?)/i)>0||e.search(/^data\:image\/ktx2/)===0?`image/ktx2`:`image/png`}var dA=new Mm,fA=class{constructor(e={},t={}){this.json=e,this.extensions={},this.plugins={},this.options=t,this.cache=new gk,this.associations=new Map,this.primitiveCache={},this.nodeCache={},this.meshCache={refs:{},uses:{}},this.cameraCache={refs:{},uses:{}},this.lightCache={refs:{},uses:{}},this.sourceCache={},this.textureCache={},this.nodeNamesUsed={};let n=!1,r=-1,i=!1,a=-1;if(typeof navigator<`u`&&navigator.userAgent!==void 0){let e=navigator.userAgent;n=/^((?!chrome|android).)*safari/i.test(e)===!0;let t=e.match(/Version\/(\d+)/);r=n&&t?parseInt(t[1],10):-1,i=e.indexOf(`Firefox`)>-1,a=i?e.match(/Firefox\/([0-9]+)\./)[1]:-1}typeof createImageBitmap>`u`||n&&r<17||i&&a<98?this.textureLoader=new vy(this.options.manager):this.textureLoader=new Hy(this.options.manager),this.textureLoader.setCrossOrigin(this.options.crossOrigin),this.textureLoader.setRequestHeader(this.options.requestHeader),this.fileLoader=new hy(this.options.manager),this.fileLoader.setResponseType(`arraybuffer`),this.options.crossOrigin===`use-credentials`&&this.fileLoader.setWithCredentials(!0)}setExtensions(e){this.extensions=e}setPlugins(e){this.plugins=e}parse(e,t){let n=this,r=this.json,i=this.extensions;this.cache.removeAll(),this.nodeCache={},this._invokeAll(function(e){return e._markDefs&&e._markDefs()}),Promise.all(this._invokeAll(function(e){return e.beforeRoot&&e.beforeRoot()})).then(function(){return Promise.all([n.getDependencies(`scene`),n.getDependencies(`animation`),n.getDependencies(`camera`)])}).then(function(t){let a={scene:t[0][r.scene||0],scenes:t[0],animations:t[1],cameras:t[2],asset:r.asset,parser:n,userData:{}};return rA(i,a,r),iA(a,r),Promise.all(n._invokeAll(function(e){return e.afterRoot&&e.afterRoot(a)})).then(function(){for(let e of a.scenes)e.updateMatrixWorld();e(a)})}).catch(t)}_markDefs(){let e=this.json.nodes||[],t=this.json.skins||[],n=this.json.meshes||[];for(let n=0,r=t.length;n{let n=this.associations.get(e);n!=null&&this.associations.set(t,n);for(let[n,r]of e.children.entries())i(r,t.children[n])};return i(n,r),r.name+=`_instance_`+e.uses[t]++,r}_invokeOne(e){let t=Object.values(this.plugins);t.push(this);for(let n=0;n=2&&p.setY(t,u[e*a+1]),a>=3&&p.setZ(t,u[e*a+2]),a>=4&&p.setW(t,u[e*a+3]),a>=5)throw Error(`THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.`)}p.normalized=d}return p})}loadTexture(e){let t=this.json,n=this.options,r=t.textures[e].source,i=t.images[r],a=this.textureLoader;if(i.uri){let e=n.manager.getHandler(i.uri);e!==null&&(a=e)}return this.loadTextureImage(e,r,a)}loadTextureImage(e,t,n){let r=this,i=this.json,a=i.textures[e],o=i.images[t],s=(o.uri||o.bufferView)+`:`+a.sampler;if(this.textureCache[s])return this.textureCache[s];let c=this.loadImageSource(t,n).then(function(t){t.flipY=!1,t.name=a.name||o.name||``,t.name===``&&typeof o.uri==`string`&&o.uri.startsWith(`data:image/`)===!1&&(t.name=o.uri);let n=(i.samplers||{})[a.sampler]||{};return t.magFilter=Yk[n.magFilter]||1006,t.minFilter=Yk[n.minFilter]||1008,t.wrapS=Xk[n.wrapS]||1e3,t.wrapT=Xk[n.wrapT]||1e3,t.generateMipmaps=!t.isCompressedTexture&&t.minFilter!==1003&&t.minFilter!==1006,r.associations.set(t,{textures:e}),t}).catch(function(){return null});return this.textureCache[s]=c,c}loadImageSource(e,t){let n=this,r=this.json,i=this.options;if(this.sourceCache[e]!==void 0)return this.sourceCache[e].then(e=>e.clone());let a=r.images[e],o=self.URL||self.webkitURL,s=a.uri||``,c=!1;if(a.bufferView!==void 0)s=n.getDependency(`bufferView`,a.bufferView).then(function(e){c=!0;let t=new Blob([e],{type:a.mimeType});return s=o.createObjectURL(t),s});else if(a.uri===void 0)throw Error(`THREE.GLTFLoader: Image `+e+` is missing URI and bufferView`);let l=Promise.resolve(s).then(function(e){return new Promise(function(n,r){let a=n;t.isImageBitmapLoader===!0&&(a=function(e){let t=new Em(e);t.needsUpdate=!0,n(t)}),t.load(By.resolveURL(e,i.path),a,void 0,r)})}).then(function(e){return c===!0&&o.revokeObjectURL(s),iA(e,a),e.userData.mimeType=a.mimeType||uA(a.uri),e}).catch(function(e){throw console.error(`THREE.GLTFLoader: Couldn't load texture`,s),e});return this.sourceCache[e]=l,l}assignTexture(e,t,n,r){let i=this;return this.getDependency(`texture`,n.index).then(function(a){if(!a)return null;if(n.texCoord!==void 0&&n.texCoord>0&&(a=a.clone(),a.channel=n.texCoord),i.extensions[vk.KHR_TEXTURE_TRANSFORM]){let e=n.extensions===void 0?void 0:n.extensions[vk.KHR_TEXTURE_TRANSFORM];if(e){let t=i.associations.get(a);a=i.extensions[vk.KHR_TEXTURE_TRANSFORM].extendTexture(a,e),i.associations.set(a,t)}}return r!==void 0&&(a.colorSpace=r),e[t]=a,a})}assignFinalMaterial(e){let t=e.geometry,n=e.material,r=t.attributes.tangent===void 0,i=t.attributes.color!==void 0,a=t.attributes.normal===void 0;if(e.isPoints){let e=`PointsMaterial:`+n.uuid,t=this.cache.get(e);t||(t=new D_,vg.prototype.copy.call(t,n),t.color.copy(n.color),t.map=n.map,t.sizeAttenuation=!1,this.cache.add(e,t)),n=t}else if(e.isLine){let e=`LineBasicMaterial:`+n.uuid,t=this.cache.get(e);t||(t=new p_,vg.prototype.copy.call(t,n),t.color.copy(n.color),t.map=n.map,this.cache.add(e,t)),n=t}if(r||i||a){let e=`ClonedMaterial:`+n.uuid+`:`;r&&(e+=`derivative-tangents:`),i&&(e+=`vertex-colors:`),a&&(e+=`flat-shading:`);let t=this.cache.get(e);t||(t=n.clone(),i&&(t.vertexColors=!0),a&&(t.flatShading=!0),r&&(t.normalScale&&(t.normalScale.y*=-1),t.clearcoatNormalScale&&(t.clearcoatNormalScale.y*=-1)),this.cache.add(e,t),this.associations.set(t,this.associations.get(n))),n=t}e.material=n}getMaterialType(){return Pv}loadMaterial(e){let t=this,n=this.json,r=this.extensions,i=n.materials[e],a,o={},s=i.extensions||{},c=[];if(s[vk.KHR_MATERIALS_UNLIT]){let e=r[vk.KHR_MATERIALS_UNLIT];a=e.getMaterialType(),c.push(e.extendParams(o,i,t))}else{let n=i.pbrMetallicRoughness||{};if(o.color=new Y(1,1,1),o.opacity=1,Array.isArray(n.baseColorFactor)){let e=n.baseColorFactor;o.color.setRGB(e[0],e[1],e[2],gp),o.opacity=e[3]}n.baseColorTexture!==void 0&&c.push(t.assignTexture(o,`map`,n.baseColorTexture,hp)),o.metalness=n.metallicFactor===void 0?1:n.metallicFactor,o.roughness=n.roughnessFactor===void 0?1:n.roughnessFactor,n.metallicRoughnessTexture!==void 0&&(c.push(t.assignTexture(o,`metalnessMap`,n.metallicRoughnessTexture)),c.push(t.assignTexture(o,`roughnessMap`,n.metallicRoughnessTexture))),a=this._invokeOne(function(t){return t.getMaterialType&&t.getMaterialType(e)}),c.push(Promise.all(this._invokeAll(function(t){return t.extendMaterialParams&&t.extendMaterialParams(e,o)})))}i.doubleSided===!0&&(o.side=2);let l=i.alphaMode||tA.OPAQUE;if(l===tA.BLEND?(o.transparent=!0,o.depthWrite=!1):(o.transparent=!1,l===tA.MASK&&(o.alphaTest=i.alphaCutoff===void 0?.5:i.alphaCutoff)),i.normalTexture!==void 0&&a!==wg&&(c.push(t.assignTexture(o,`normalMap`,i.normalTexture)),o.normalScale=new q(1,1),i.normalTexture.scale!==void 0)){let e=i.normalTexture.scale;o.normalScale.set(e,e)}if(i.occlusionTexture!==void 0&&a!==wg&&(c.push(t.assignTexture(o,`aoMap`,i.occlusionTexture)),i.occlusionTexture.strength!==void 0&&(o.aoMapIntensity=i.occlusionTexture.strength)),i.emissiveFactor!==void 0&&a!==wg){let e=i.emissiveFactor;o.emissive=new Y().setRGB(e[0],e[1],e[2],gp)}return i.emissiveTexture!==void 0&&a!==wg&&c.push(t.assignTexture(o,`emissiveMap`,i.emissiveTexture,hp)),Promise.all(c).then(function(){let n=new a(o);return i.name&&(n.name=i.name),iA(n,i),t.associations.set(n,{materials:e}),i.extensions&&rA(r,n,i),n})}createUniqueName(e){let t=ob.sanitizeNodeName(e||``);return t in this.nodeNamesUsed?t+`_`+ ++this.nodeNamesUsed[t]:(this.nodeNamesUsed[t]=0,t)}loadGeometries(e){let t=this,n=this.extensions,r=this.primitiveCache;function i(e){return n[vk.KHR_DRACO_MESH_COMPRESSION].decodePrimitive(e,t).then(function(n){return mA(n,e,t)})}let a=[];for(let n=0,o=e.length;nyw(e,1)):o.mode===qk.TRIANGLE_FAN&&(e=e.then(e=>yw(e,2))),r[s]={primitive:o,promise:e},a.push(e)}}return Promise.all(a)}loadMesh(e){let t=this,n=this.json,r=this.extensions,i=n.meshes[e],a=i.primitives,o=[];for(let e=0,t=a.length;e0&&oA(d,i),d.name=t.createUniqueName(i.name||`mesh_`+e),iA(d,i),u.extensions&&rA(r,d,u),t.assignFinalMaterial(d),c.push(d)}for(let n=0,r=c.length;n0){let e=t.userData.pivot,r=n[0];t.pivot=new J().fromArray(e),t.position.x-=e[0],t.position.y-=e[1],t.position.z-=e[2],r.position.set(0,0,0),delete t.userData.pivot}return t})}_loadNodeShallow(e){let t=this.json,n=this.extensions,r=this;if(this.nodeCache[e]!==void 0)return this.nodeCache[e];let i=t.nodes[e],a=i.name?r.createUniqueName(i.name):``,o=[],s=r._invokeOne(function(t){return t.createNodeMesh&&t.createNodeMesh(e)});return s&&o.push(s),i.camera!==void 0&&o.push(r.getDependency(`camera`,i.camera).then(function(e){return r._getNodeRef(r.cameraCache,i.camera,e)})),r._invokeAll(function(t){return t.createNodeAttachment&&t.createNodeAttachment(e)}).forEach(function(e){o.push(e)}),this.nodeCache[e]=Promise.all(o).then(function(t){let o;if(o=i.isBone===!0?new Yg:t.length>1?new oh:t.length===1?t[0]:new ah,o!==t[0])for(let e=0,n=t.length;e1){let e=r.associations.get(o);r.associations.set(o,{...e})}return r.associations.get(o).nodes=e,o}),this.nodeCache[e]}loadScene(e){let t=this.extensions,n=this.json.scenes[e],r=this,i=new oh;n.name&&(i.name=r.createUniqueName(n.name)),iA(i,n),n.extensions&&rA(t,i,n);let a=n.nodes||[],o=[];for(let e=0,t=a.length;e{let t=new Map;for(let[e,n]of r.associations)(e instanceof vg||e instanceof Em)&&t.set(e,n);return e.traverse(e=>{let n=r.associations.get(e);n!=null&&t.set(e,n)}),t})(i),i})}_createAnimationTracks(e,t,n,r,i){let a=[],o=e.name?e.name:e.uuid,s=[];function c(e){e.morphTargetInfluences&&s.push(e.name?e.name:e.uuid)}$k[i.path]===$k.weights?(c(e),e.isGroup&&e.children.forEach(c)):s.push(o);let l;switch($k[i.path]){case $k.weights:l=ty;break;case $k.rotation:l=ry;break;case $k.translation:case $k.scale:l=ay;break;default:switch(n.itemSize){case 1:l=ty;break;default:l=ay;break}break}let u=r.interpolation===void 0?sp:eA[r.interpolation],d=this._getArrayFromAccessor(n);for(let e=0,n=s.length;e` +${OA} +varying vec3 vOutfitPos; +vec3 outfitBindNormal = vec3(0.0, 1.0, 0.0); +float outfitClothAt(vec3 p) { return ${kA[e]}; } +vec3 outfitRounded(vec3 p, vec3 n) { + if (p.y < 60.0) { + vec2 radial = normalize(p.xz - outfitLegAxis(p.y, sign(p.x)) + vec2(1e-4)); + return normalize(vec3(radial.x, n.y * 0.3, radial.y)); + } + vec3 shape = outfitTorso(p.y); + float planar = length(n.xz); + vec2 ring = vec2(p.x, p.z - shape.x) / shape.yz; + ring = normalize(ring * ring * ring / shape.yz + vec2(0.0, 1e-5)); + float lift = smoothstep(99.0, 106.0, p.y); + vec3 torso = normalize(vec3(ring.x * mix(1.0, planar, lift), n.y * lift, ring.y * mix(1.0, planar, lift))); + vec2 centre = outfitArmAxis(abs(p.x)); + vec2 around = normalize(vec2(p.y - centre.x, p.z - centre.y) + vec2(1e-4)); + vec3 arm = normalize(vec3(n.x * 0.25, around.x, around.y)); + return normalize(mix(torso, arm, smoothstep(11.0, 17.0, abs(p.x)))); +} +/** Cloth hangs from the high points: hollows in the torso and arms are filled out to a smooth drape. */ +vec3 outfitDraped(vec3 p, vec3 at, float cloth) { + vec3 shape = outfitTorso(p.y); + vec2 offset = vec2(p.x, p.z - shape.x); + float reach = max(length(offset), 1e-3); + vec2 toward = offset / reach; + vec2 squared = toward * toward / (shape.yz * shape.yz); + float rim = 1.0 / sqrt(sqrt(dot(squared, squared))); + float torso = (1.0 - smoothstep(10.5, 15.0, abs(p.x))) * (1.0 - smoothstep(104.0, 108.0, p.y)) * cloth; + at.xz += toward * (max(reach, rim) - reach) * torso; + float along = abs(p.x); + vec2 centre = outfitArmAxis(along); + vec2 around = vec2(p.y - centre.x, p.z - centre.y); + float span = max(length(around), 1e-3); + float sleeve = mix(5.1, 3.3, smoothstep(30.0, 43.0, along)); + float arm = smoothstep(15.0, 19.0, along) * cloth; + at.yz += around / span * (max(span, sleeve) - span) * arm; + return at; +} +`,jA=` +{ + vec3 p = position; + float cloth = outfitClothAt(p); + objectNormal = normalize(mix(objectNormal, outfitRounded(p, objectNormal), 0.97 * cloth)); + outfitBindNormal = objectNormal; +} +`,MA=` + float side = sign(p.x); + vec2 axis = outfitLegAxis(p.y, side); + vec2 offset = transformed.xz - axis; + float reach = max(length(offset), 1e-3); + float tube = mix(5.2, 6.0, smoothstep(12.0, 40.0, p.y)); +`,NA={head:` +{ + vec3 p = position; + vOutfitPos = p; + float cloth = outfitClothAt(p); + transformed = outfitDraped(p, transformed, cloth) + outfitBindNormal * uOutfitShape.z * cloth; +} +`,body:` +{ + vec3 p = position; + vOutfitPos = p; + if (p.y < 60.0) { + ${MA} + float weight = 1.0 - smoothstep(40.5, 45.0, p.y); + transformed.xz = axis + offset / reach * mix(reach, max(reach, tube), weight); + } else { + float cloth = outfitClothAt(p); + transformed = outfitDraped(p, transformed, cloth) + outfitBindNormal * uOutfitShape.z * cloth; + } +} +`,shoes:` +{ + vec3 p = position; + vOutfitPos = p; + ${MA} + float weight = smoothstep(9.0, 11.5, p.y); + transformed.xz = axis + offset / reach * mix(reach, max(reach, tube), weight); +} +`,pants:` +{ + vec3 p = position; + vOutfitPos = p; + vec2 axis = vec2(sign(p.x) * 8.4, -4.9); + float slim = smoothstep(38.0, 56.0, p.y); + vec2 narrow = vec2(mix(0.76, 1.0, slim), mix(0.62, 1.0, slim)); + transformed.xz = mix(transformed.xz, axis + (transformed.xz - axis) * narrow, smoothstep(0.5, 4.0, abs(p.x))); +} +`},PA=` +${OA} +uniform vec3 uOutfitShirt; +uniform vec3 uOutfitTrousers; +uniform vec3 uOutfitAccent; +varying vec3 vOutfitPos; +float outfitCloth = 0.0; +float outfitGloss = 0.0; +float outfitBowTie(vec3 p) { + if (uOutfitShape.y < 0.5 || p.z < -2.5) return 0.0; + float dx = abs(p.x); + float dy = abs(p.y - 106.6); + float wing = step(dx, 3.7) * step(dy, 0.4 + 1.2 * dx / 3.7); + float knot = step(dx, 0.8) * step(dy, 0.95); + return max(wing, knot); +} +/** The shirt above the waist, the same on the neck mesh and the body mesh so the pattern has no seam. */ +vec3 outfitShirtTone(vec3 p) { + float along = abs(p.x); + float fold = 0.5 + 0.5 * sin(p.y * 0.9 + sin(p.x * 0.7 + p.z * 0.8) * 2.4); + fold *= 0.6 + 0.4 * sin(p.x * 1.9 - p.y * 0.35 + p.z * 1.3); + float reach = outfitNeckReach(p); + float band = (1.0 - smoothstep(7.0, 7.2, reach)) * step(104.0, p.y); + vec3 tone = uOutfitShirt * mix(mix(0.9, 1.0, fold), 1.07, band); + tone *= 1.0 - 0.4 * (1.0 - smoothstep(0.0, 0.3, abs(reach - 7.25))) * step(104.0, p.y); + float hem = min(uOutfitShape.x, ${TA.toFixed(1)}) - 2.4; + tone *= mix(1.0, 0.86, smoothstep(hem - 0.1, hem + 0.1, along)); + if (along < 1.0 && p.z > 0.5 && p.y < 106.0) { + tone *= 0.92; + float button = 1.0 - smoothstep(0.32, 0.46, length(vec2(p.x, mod(p.y - 81.0, 4.6) - 2.3))); + tone = mix(tone, uOutfitAccent, button * 0.85); + } + float tie = outfitBowTie(p); + outfitGloss = tie * 0.4; + return mix(tone, uOutfitAccent, tie); +} +`,FA={head:` +{ + vec3 p = vOutfitPos; + float collar = outfitCollar(p); + outfitCloth = 1.0 - smoothstep(collar - 0.12, collar + 0.12, p.y); + diffuseColor.rgb = mix(diffuseColor.rgb, outfitShirtTone(p), outfitCloth); + outfitGloss *= outfitCloth; +} +`,body:` +{ + vec3 p = vOutfitPos; + if (p.y < 60.0) { + diffuseColor.rgb = uOutfitTrousers; + outfitCloth = 1.0; + } else { + outfitCloth = 1.0 - smoothstep(uOutfitShape.x - 0.12, uOutfitShape.x + 0.12, abs(p.x)); + diffuseColor.rgb = mix(diffuseColor.rgb, outfitShirtTone(p), outfitCloth); + outfitGloss *= outfitCloth; + } +} +`,shoes:` +{ + float cloth = smoothstep(9.3, 9.7, vOutfitPos.y); + diffuseColor.rgb = mix(diffuseColor.rgb, uOutfitTrousers, cloth); + outfitCloth = cloth; + outfitGloss = 1.0 - cloth; +} +`,pants:` +{ + vec3 p = vOutfitPos; + float belt = smoothstep(76.3, 76.5, p.y); + float buckle = belt * step(abs(p.x), 1.8) * step(6.0, p.z) * step(76.9, p.y) * step(p.y, 79.2); + diffuseColor.rgb = mix(mix(uOutfitTrousers, uOutfitAccent, belt), vec3(0.72, 0.71, 0.66), buckle); + outfitCloth = 1.0 - belt; + outfitGloss = belt; +} +`},IA=` +roughnessFactor = mix(mix(roughnessFactor, 0.88, outfitCloth), 0.32, outfitGloss); +`,LA=` +#ifdef USE_CLEARCOAT +material.clearcoat *= 1.0 - outfitCloth; +#endif +`;function RA(e,t,n){let r={uOutfitShirt:{value:new Y(n.shirt)},uOutfitTrousers:{value:new Y(n.trousers)},uOutfitAccent:{value:new Y(n.accent)},uOutfitShape:{value:new Dm(n.sleeves===`long`?63.4:EA,+!!n.bowTie,DA,0)}},i=e.onBeforeCompile,a=Object.hasOwn(e,`customProgramCacheKey`)?e.customProgramCacheKey():`plain`;e.onBeforeCompile=(n,a)=>{i.call(e,n,a),Object.assign(n.uniforms,r),n.vertexShader=n.vertexShader.replace(`#include `,`#include \n${AA(t)}`).replace(`#include `,`#include \n${jA}`).replace(`#include `,`#include \n${NA[t]}`),n.fragmentShader=n.fragmentShader.replace(`#include `,`#include \n${PA}`).replace(`#include `,`#include \n${FA[t]}`).replace(`#include `,`#include \n${IA}`).replace(`#include `,`#include \n${LA}`)},e.customProgramCacheKey=()=>`${a}-outfit-${t}`,e.needsUpdate=!0}var zA=new J(0,1,0);function BA(e,t,n,r){let i=new B_(r,n,1,6);return i.applyQuaternion(new cm().setFromUnitVectors(zA,t)),i.translate(e.x+t.x*n*.5,e.y+t.y*n*.5,e.z+t.z*n*.5),i}function VA(e){let t=e===`left`?-1:1,n=[],r=new bA(8.4,9.2,2.8,1,1.25);r.translate(0,5.2,0),n.push(r);let i=new B_(2.2,2.2,1,6);i.scale(1.3,1,.9),i.translate(0,.4,0),n.push(i);let a=[{x:3.2*t,y:9.1,scale:.95,curl:.22,splay:.12*t},{x:1.08*t,y:9.5,scale:1.05,curl:.34,splay:.03*t},{x:-1.05*t,y:9.3,scale:.98,curl:.46,splay:-.05*t},{x:-3.05*t,y:8.7,scale:.8,curl:.6,splay:-.16*t}];for(let e of a){let t=new J(e.x,e.y,0),r=e.curl;for(let[i,a,o]of[[4.3,1.04,.55],[2.7,.95,.5],[2.2,.86,0]]){let s=new J(Math.sin(e.splay)*Math.cos(r),Math.cos(e.splay)*Math.cos(r),Math.sin(r)),c=i*e.scale;n.push(BA(t,s,c,a*(.88+.12*e.scale))),t.addScaledVector(s,c),r+=o}}let o=new Sv(1.9,6,4);o.scale(1.15,1.5,.95),o.translate(2.7*t,3.3,.9),n.push(o);let s=new J(3.7*t,3.4,.9),c=new J(.62*t,.64,.45).normalize();n.push(BA(s,c,3.9,1.2)),s.addScaledVector(c,3.9),n.push(BA(s,new J(.3*t,.82,.48).normalize(),3,1.05));let l=n.map(e=>{let t=e.index===null?e:e.toNonIndexed();return t.deleteAttribute(`uv`),t}),u=gw(l,!1);for(let e of new Set([...n,...l]))e.dispose();if(u===null)throw Error(`hand geometry could not be merged`);return u}function HA(){let e=new H_(3.9,6,9.2,20,1,!1);return e.scale(1,1,.96),e.translate(0,21.1,0),e}var UA={hips:gA.hips,spine:gA.spine,chest:`Spine1_024`,upperChest:gA.chest,neck:`Neck_012`,head:gA.head,clavicleL:`LeftShoulder_09`,shoulderL:gA.shoulderL,elbowL:gA.elbowL,gloveL:gA.gloveL,clavicleR:`RightShoulder_020`,shoulderR:gA.shoulderR,elbowR:gA.elbowR,gloveR:gA.gloveR,hipL:gA.hipL,kneeL:gA.kneeL,ankleL:gA.ankleL,toeL:`LeftToeBase_011`,hipR:gA.hipR,kneeR:gA.kneeR,ankleR:gA.ankleR,toeR:`RightToeBase_022`},WA=new J,GA=new J,KA=new J,qA=new Mm,JA=new cm,YA=new cm,XA=new J,ZA=new J,QA=new J,$A=new J,ej=new J,tj=new J,nj=new J,rj=new J,ij=new J,aj=new J;function oj(e,t){return t.setFromMatrixPosition(e.matrixWorld)}function sj(e,t){return e.matrixWorld.decompose(XA,t,ZA),t}function cj(e,t,n,r){return GA.copy(e).normalize(),n===`z`?(KA.copy(t).addScaledVector(GA,-t.dot(GA)),KA.lengthSq()<1e-8&&KA.set(0,0,1).addScaledVector(GA,-GA.z),KA.normalize(),WA.crossVectors(GA,KA).normalize()):(WA.copy(t).addScaledVector(GA,-t.dot(GA)),WA.lengthSq()<1e-8&&WA.set(1,0,0).addScaledVector(GA,-GA.x),WA.normalize(),KA.crossVectors(WA,GA).normalize()),qA.makeBasis(WA,GA,KA),r.setFromRotationMatrix(qA)}var lj=class{bones;metrics;restLocal=new Map;restPositions=new Map;limb={reached:!1,joint:new J};constructor(e){let t=new Map;e.traverse(e=>{e instanceof Yg&&t.set(e.name,e)});let n={};for(let[e,r]of Object.entries(UA)){let i=t.get(r);if(i===void 0)throw Error(`fighter rig missing bone ${r} (${e})`);n[e]=i}this.bones=n,e.updateMatrixWorld(!0);for(let e of t.values())this.restLocal.set(e,e.quaternion.clone()),this.restPositions.set(e,e.position.clone());let r=(e,t)=>oj(n[e],QA).distanceTo(oj(n[t],$A)),i=oj(n.hips,QA).clone(),a=oj(n.hips.parent??e,$A).clone(),o=n.hips.position.length(),s=i.distanceTo(a);this.metrics={hipsHeight:i.y,headHeight:oj(n.head,QA).y,chestHeight:oj(n.upperChest,QA).y,ankleHeight:oj(n.ankleL,QA).y,armL:{upper:r(`shoulderL`,`elbowL`),lower:r(`elbowL`,`gloveL`)},armR:{upper:r(`shoulderR`,`elbowR`),lower:r(`elbowR`,`gloveR`)},legL:{upper:r(`hipL`,`kneeL`),lower:r(`kneeL`,`ankleL`)},legR:{upper:r(`hipR`,`kneeR`),lower:r(`kneeR`,`ankleR`)},footLength:r(`ankleL`,`toeL`),unitsPerMeter:s>1e-6?o/s:1}}resetToRest(){for(let[e,t]of this.restLocal){e.quaternion.copy(t);let n=this.restPositions.get(e);n!==void 0&&e.position.copy(n)}}restLocalQuaternion(e){return this.restLocal.get(e)??e.quaternion}restLocalPosition(e){return this.restPositions.get(e)??e.position}position(e,t){return oj(this.bones[e],t)}setWorldRotation(e,t){let n=e.parent;n===null?e.quaternion.copy(t):(sj(n,YA),e.quaternion.copy(YA.invert().multiply(t)).normalize()),e.updateWorldMatrix(!1,!1)}setWorldFrame(e,t,n,r){this.setWorldRotation(e,cj(t,n,r,JA))}setWorldPosition(e,t){let n=e.parent;n===null?e.position.copy(t):(e.position.copy(t),n.worldToLocal(e.position)),e.updateWorldMatrix(!1,!1)}aimAt(e,t,n){if(e.updateWorldMatrix(!1,!1),oj(e,QA),ej.subVectors(t,QA),ej.lengthSq()<1e-10)return;sj(e,JA);let r=n===`z`?aj.set(0,0,1):aj.set(1,0,0);this.setWorldFrame(e,ej,r.applyQuaternion(JA),n)}solveLimb(e,t,n,r,i,a,o,s=.01){e.updateWorldMatrix(!1,!1);let c=oj(e,QA);ej.subVectors(r,c);let l=n.upper+n.lower-s,u=K.clamp(ej.length(),Math.abs(n.upper-n.lower)+.005,l),d=ej.length()<=l;ej.lengthSq()<1e-10&&ej.set(0,-1,0),ej.normalize(),tj.copy(i).addScaledVector(ej,-i.dot(ej)),tj.lengthSq()<1e-8&&(tj.set(0,0,1).addScaledVector(ej,-ej.z),tj.lengthSq()<1e-8&&tj.set(1,0,0)),tj.normalize();let f=K.clamp((n.upper*n.upper+u*u-n.lower*n.lower)/(2*n.upper*u),-1,1),p=n.upper*f,m=n.upper*Math.sqrt(Math.max(0,1-f*f));nj.copy(c).addScaledVector(ej,p).addScaledVector(tj,m),rj.crossVectors(tj,ej).multiplyScalar(o).normalize(),ij.copy(nj).sub(c),this.setWorldFrame(e,ij,rj,a),t.updateWorldMatrix(!1,!1);let h=oj(t,$A);return ij.copy(c).addScaledVector(ej,u).sub(h),this.setWorldFrame(t,ij,rj,a),this.limb.reached=d,this.limb.joint.copy(h),this.limb}},uj=new J(.2018,.081,-.027),dj=new J(-.2018,.081,-.027),fj=(e,t,n)=>new J(e,t,n),pj=(e,t,n)=>{let r=K.clamp((n-e)/Math.max(1e-6,t-e),0,1);return r*r*(3-2*r)},mj=(e,t=1.7)=>K.clamp(e,0,1)**+t,hj=(e,t=2)=>1-(1-K.clamp(e,0,1))**t,gj=new cm,_j=new J,vj=new J,yj=new J,bj=new J,xj=new J,Sj=new cm,Cj=new cm,wj=new Hm,Tj=new J(0,1,0),Ej=[[`spine`,.25,.2],[`chest`,.55,.5],[`upperChest`,1,1]],Dj=class{rig;chestLocal=new J;constructor(e){this.rig=e}toWorld(e,t){return t.copy(e).applyQuaternion(gj).add(_j)}dirToWorld(e,t){return t.copy(e).applyQuaternion(gj)}setYawPitchRoll(e,t,n,r){wj.set(n,t,r,`YXZ`),Sj.setFromEuler(wj),Cj.copy(gj).multiply(Sj),this.rig.setWorldRotation(e,Cj)}apply(e,t){let n=this.rig,r=n.bones;n.resetToRest(),e.updateMatrixWorld(!0),sj(e,gj),oj(e,_j);let i=t.torso;this.toWorld(i.hips,vj),n.setWorldPosition(r.hips,vj),this.setYawPitchRoll(r.hips,i.hipsYaw,i.hipsPitch,i.hipsRoll);let a=i.shouldersYaw-i.hipsYaw;for(let e=0;e0&&(oj(r.head,vj),this.dirToWorld(i.headOffset,yj),vj.add(yj),n.setWorldPosition(r.head,vj)),this.solveArm(`L`,t.handL,t.shrugL),this.solveArm(`R`,t.handR,t.shrugR),this.solveLeg(`L`,t.footL),this.solveLeg(`R`,t.footR)}solveArm(e,t,n){let r=this.rig,i=r.bones,a=e===`L`?i.clavicleL:i.clavicleR,o=e===`L`?i.shoulderL:i.shoulderR,s=e===`L`?i.elbowL:i.elbowR,c=e===`L`?i.gloveL:i.gloveR,l=e===`L`?r.metrics.armL:r.metrics.armR,u=e===`L`?uj:dj;this.chestLocal.copy(u),this.chestLocal.z+=n*.06,this.chestLocal.y+=n*.035,this.chestLocal.x*=1-n*.08,bj.copy(this.chestLocal).multiplyScalar(r.metrics.unitsPerMeter),i.upperChest.localToWorld(bj),r.aimAt(a,bj,`z`),o.updateWorldMatrix(!1,!1),this.toWorld(t.position,vj),this.dirToWorld(t.pole,yj),r.solveLimb(o,s,l,vj,yj,`z`,e===`L`?1:-1,.012),c.updateWorldMatrix(!1,!1),this.dirToWorld(t.knuckles,bj),this.dirToWorld(t.palm,xj),r.setWorldFrame(c,bj,xj,`z`)}solveLeg(e,t){let n=this.rig,r=n.bones,i=e===`L`?r.hipL:r.hipR,a=e===`L`?r.kneeL:r.kneeR,o=e===`L`?r.ankleL:r.ankleR,s=e===`L`?r.toeL:r.toeR,c=e===`L`?n.metrics.legL:n.metrics.legR;this.toWorld(t.position,vj),this.dirToWorld(t.toe,bj).normalize();let l=n.metrics.footLength;xj.copy(bj).multiplyScalar(l).addScaledVector(Tj,-n.metrics.ankleHeight).normalize(),t.heel!==0&&(vj.addScaledVector(xj,l),Sj.setFromAxisAngle(yj.crossVectors(bj,Tj).normalize(),-t.heel),xj.applyQuaternion(Sj),vj.addScaledVector(xj,-l)),this.dirToWorld(t.pole,yj),n.solveLimb(i,a,c,vj,yj,`x`,-1,.02),o.updateWorldMatrix(!1,!1),n.setWorldFrame(o,xj,Tj,`z`),s.updateWorldMatrix(!1,!1),n.setWorldFrame(s,bj,Tj,`z`)}},Oj={bladeYaw:-1.05,hipsHeight:.8,leadFoot:fj(.12,0,.24),rearFoot:fj(-.1,0,-.34),leadToe:fj(-.34,0,.94),rearToe:fj(-.77,0,.64),relaxedLead:fj(.11,-.22,.36),relaxedRear:fj(-.11,-.16,.1),guardHighLead:fj(.14,-.02,.17),guardHighRear:fj(-.1,-.02,.11),guardLowLead:fj(.15,-.36,.22),guardLowRear:fj(-.13,-.34,.12),leadPole:fj(.45,-1,.2),rearPole:fj(-.5,-1,-.05),leadKnuckles:fj(.1,.55,.8),leadPalm:fj(-.9,.15,-.35),rearKnuckles:fj(-.05,.7,.7),rearPalm:fj(.95,.1,-.25)};function kj(e,t,n=new J){return n.set(e.x*t,e.y,e.z)}var Q={pelvis:0,hipL:1,hipR:2,belly:3,chest:4,upper:5,neck:6,shoulderL:7,shoulderR:8,head:9,crown:10,face:11,elbowL:12,wristL:13,fistL:14,elbowR:15,wristR:16,fistR:17,kneeL:18,ankleL:19,toeL:20,heelL:21,kneeR:22,ankleR:23,toeR:24,heelR:25},Aj=[9,4,4,2,10,7,2,3.5,3.5,2.5,1.5,1,1.6,1,1,1.6,1,1,3.6,1.6,.4,.6,3.6,1.6,.4,.6],jj=[.12,.09,.09,.11,.14,.13,.06,.07,.07,.09,.085,.055,.05,.065,.08,.05,.065,.08,.06,.05,.035,.04,.06,.05,.035,.04],Mj=.12,Nj=.16,Pj=.09,Fj=.06,Ij=.11,Lj=[[Q.pelvis,Q.hipL],[Q.pelvis,Q.hipR],[Q.hipL,Q.hipR],[Q.belly,Q.hipL],[Q.belly,Q.hipR],[Q.belly,Q.pelvis],[Q.pelvis,Q.chest],[Q.chest,Q.upper],[Q.upper,Q.neck],[Q.upper,Q.shoulderL],[Q.upper,Q.shoulderR],[Q.neck,Q.shoulderL],[Q.neck,Q.shoulderR],[Q.shoulderL,Q.shoulderR],[Q.neck,Q.head],[Q.head,Q.crown],[Q.head,Q.face],[Q.crown,Q.face],[Q.shoulderL,Q.elbowL],[Q.elbowL,Q.wristL],[Q.wristL,Q.fistL],[Q.shoulderR,Q.elbowR],[Q.elbowR,Q.wristR],[Q.wristR,Q.fistR],[Q.hipL,Q.kneeL],[Q.kneeL,Q.ankleL],[Q.ankleL,Q.toeL],[Q.ankleL,Q.heelL],[Q.toeL,Q.heelL],[Q.hipR,Q.kneeR],[Q.kneeR,Q.ankleR],[Q.ankleR,Q.toeR],[Q.ankleR,Q.heelR],[Q.toeR,Q.heelR]],Rj=[[Q.hipL,Q.shoulderL,.8,1.05],[Q.hipR,Q.shoulderR,.8,1.05],[Q.hipL,Q.shoulderR,.84,1.08],[Q.hipR,Q.shoulderL,.84,1.08],[Q.belly,Q.upper,.7,1.06],[Q.pelvis,Q.upper,.86,1],[Q.hipL,Q.chest,.84,1.08],[Q.hipR,Q.chest,.84,1.08],[Q.shoulderL,Q.face,.76,1.2],[Q.shoulderR,Q.face,.76,1.2],[Q.kneeL,Q.hipR,.8,1.3],[Q.kneeR,Q.hipL,.8,1.3],[Q.kneeL,Q.belly,.5,1.12],[Q.kneeR,Q.belly,.5,1.12]],zj=8,Bj=[[Q.kneeL,Q.kneeR,.13],[Q.ankleL,Q.ankleR,.11],[Q.kneeL,Q.ankleR,.1],[Q.kneeR,Q.ankleL,.1],[Q.hipL,Q.ankleL,.2],[Q.hipR,Q.ankleR,.2],[Q.shoulderL,Q.wristL,.14],[Q.shoulderR,Q.wristR,.14],[Q.face,Q.chest,.2],[Q.crown,Q.chest,.24],[Q.kneeL,Q.upper,.3],[Q.kneeR,Q.upper,.3]],Vj=[Q.elbowL,Q.wristL,Q.fistL,Q.elbowR,Q.wristR,Q.fistR],Hj=[.7,.6,.6,.7,.9,1,1,1,1,1,1,1,.9,.8,.8,.9,.8,.8,.35,.1,0,0,.35,.1,0,0],Uj=[[Q.pelvis,Q.chest,.12],[Q.chest,Q.upper,.13]],Wj=Int32Array.from(Lj.flat()),Gj=Int32Array.from(Rj.flatMap(([e,t])=>[e,t])),Kj=Int32Array.from(Bj.flatMap(([e,t])=>[e,t])),qj=Float64Array.from(Bj.map(([,,e])=>e)),Jj=Int32Array.from(Uj.flatMap(([e,t])=>[e,t])),Yj=Float64Array.from(Uj.map(([,,e])=>e)),Xj=Float64Array.from([Q.head,1,Q.crown,.9,Q.face,1,Q.neck,.45]),Zj=Float64Array.from([Q.belly,1,Q.chest,.6,Q.pelvis,.4,Q.hipL,.3,Q.hipR,.3]),Qj=[Q.head,Q.crown,Q.face],$j=[Q.crown,Q.face],eM=[[Q.toeL,Q.heelL],[Q.toeR,Q.heelR]],tM=[[Q.heelL],[Q.heelR]],nM=[Q.head,Q.crown,Q.face],rM=[Q.fistL],iM=[Q.fistR],aM=1/120,oM=12,sM=9.81,cM=.998,lM=.5,uM=.03,dM=.35,fM=.12,pM=[Q.pelvis,Q.hipL,Q.hipR,Q.belly,Q.chest,Q.upper],mM=[Q.upper,Q.neck,Q.shoulderL,Q.shoulderR,Q.head,Q.crown,Q.face,Q.elbowL,Q.wristL,Q.fistL,Q.elbowR,Q.wristR,Q.fistR],hM=[Q.pelvis,Q.hipL,Q.hipR,Q.belly,Q.kneeL,Q.ankleL,Q.toeL,Q.heelL,Q.kneeR,Q.ankleR,Q.toeR,Q.heelR],gM=40*Math.PI/180,_M=[[Q.kneeL,Q.ankleL,Q.toeL,Q.heelL],[Q.kneeR,Q.ankleR,Q.toeR,Q.heelR]],vM=[[Q.ankleL,Q.toeL,Q.heelL],[Q.ankleR,Q.toeR,Q.heelR]],yM=.08,bM=.35,xM=new Set(pM),SM=bw+.45,CM=.24,wM=1.75,TM=.012,EM=1,DM=.35,OM=.15,kM=1.1,AM=.975,jM=.4,MM=1.2,NM=1200,PM=3,FM=7,IM=.08,LM=.97,RM=2.55,zM=.5,BM=30*Math.PI/180,VM=32*Math.PI/180,HM=28*Math.PI/180,UM=35*Math.PI/180,WM=20*Math.PI/180,GM=.5,KM=.01,qM=.04,JM=.4,YM=.02,XM=.25,ZM={timber:{knee:.12,spine:.2,seconds:.55},crumple:{knee:RM,spine:1,seconds:0},sag:{knee:.55,spine:.6,seconds:.3},fold:{knee:.9,spine:1,seconds:.5}},QM=2.5,$M=35*Math.PI/180,eN=50*Math.PI/180,tN=135*Math.PI/180,nN=[[Q.kneeL,Q.ankleL,Q.toeL,eN,tN],[Q.kneeR,Q.ankleR,Q.toeR,eN,tN],[Q.elbowL,Q.wristL,Q.fistL,0,$M],[Q.elbowR,Q.wristR,Q.fistR,0,$M]],rN=Int32Array.from(nN.flatMap(([e,,t])=>[e,t]));function iN(e,t,n,r){if(t===`body`)return`fold`;if(n<90)return`sag`;let i=Math.abs(Math.floor(r))%3;return e===`hook`?i===0?`timber`:`crumple`:i===0?`crumple`:`timber`}var aN=(e,t,n)=>n.set(e[t*3],e[t*3+1],e[t*3+2]),oN=e=>e-Math.round(e/(Math.PI*2))*Math.PI*2,sN=(e,t,n)=>{e[t*3]=n.x,e[t*3+1]=n.y,e[t*3+2]=n.z},cN=class{position=new Float64Array(78);previous=new Float64Array(78);before=new Float64Array(78);invMass=new Float64Array(26);rigidLength=new Float64Array(Lj.length);rangeMin=new Float64Array(Rj.length);rangeMax=new Float64Array(Rj.length);restRange=new Float64Array(Rj.length);rangeStart=new Float64Array(Rj.length);kneeStart=new Float64Array(2);footSign=new Float64Array(2);turnAxis=new J;turnQuaternion=new cm;turnPoint=new J;angleRanges=new Float64Array(nN.length*2);elbowAxes=new Float64Array(6);kneeAxes=new Float64Array(6);hingeStart=new Float64Array(24);hingeNow=new Float64Array(24);swing=new cm;local=new J;localAxis=new J;ropeLines=new Float64Array(4);style=`crumple`;steps=0;stillSeconds=0;torsoStillSeconds=0;fallSeconds=0;landedSeconds=-1;asleep=!1;obstacleActive=!1;obstacleX=0;obstacleZ=0;obstacleTrack=null;obstacleTrackLive=!1;pull=null;pullShare=0;trackObstacle(e,t){this.obstacleTrack=e,this.obstacleTrackLive=e!==null&&!t}a=new J;b=new J;c=new J;d=new J;e=new J;fx=new J;fy=new J;fz=new J;gx=new J;gy=new J;gz=new J;pivot=new J;segmentA=new J;segmentB=new J;constructor(){for(let e=0;e<26;e+=1)this.invMass[e]=1/Aj[e]}calibrate(e){for(let[t,[n,r]]of Rj.entries())this.restRange[t]=this.distance(e,n,r)}start(e,t,n){this.position.set(e),this.before.set(e);for(let n=0;n<78;n+=1)this.previous[n]=e[n]-t[n]*aM;for(let[t,[n,r]]of Lj.entries())this.rigidLength[t]=this.distance(e,n,r);for(let[t,[n,r,i,a,o]]of nN.entries()){let s=this.distance(e,n,r),c=this.distance(e,r,i),l=this.distance(e,n,i),u=e=>Math.sqrt(s*s+c*c+2*s*c*Math.cos(e));this.angleRanges[t*2]=Math.min(u(o),l),this.angleRanges[t*2+1]=Math.max(u(a),l)}for(let[t,[n,r,i,a]]of Rj.entries()){let o=this.distance(e,n,r),s=this.restRange[t]>0?this.restRange[t]:o;this.rangeMin[t]=Math.min(s*i,o*.98),this.rangeMax[t]=Math.max(s*a,o*1.02),this.rangeStart[t]=o}this.style=n,this.steps=0,this.stillSeconds=0,this.torsoStillSeconds=0,this.fallSeconds=0,this.landedSeconds=-1,this.asleep=!1,this.upperFrame(this.position),this.pelvisFrame(this.position);for(let e of[0,1]){this.startHinge(this.elbowAxes,e,e===0?Q.shoulderL:Q.shoulderR,e===0?Q.elbowL:Q.elbowR,e===0?Q.wristL:Q.wristR,this.gx,-1),this.startHinge(this.kneeAxes,e,e===0?Q.hipL:Q.hipR,e===0?Q.kneeL:Q.kneeR,e===0?Q.ankleL:Q.ankleR,this.fx,1),this.storeHingeStart(e,e===0?Q.hipL:Q.hipR,e===0?Q.kneeL:Q.kneeR,this.kneeAxes,this.fx,this.fy,this.fz),this.storeHingeStart(2+e,e===0?Q.shoulderL:Q.shoulderR,e===0?Q.elbowL:Q.elbowR,this.elbowAxes,this.gx,this.gy,this.gz);let t=this.dir(e===0?Q.hipL:Q.hipR,e===0?Q.kneeL:Q.kneeR,this.a),n=this.dir(e===0?Q.kneeL:Q.kneeR,e===0?Q.ankleL:Q.ankleR,this.b);this.kneeStart[e]=Math.acos(Math.min(1,Math.max(-1,t.dot(n))));let r=this.c.set(this.kneeAxes[e*3],this.kneeAxes[e*3+1],this.kneeAxes[e*3+2]);this.footSign[e]=this.footNormal(e,this.d).dot(r)>=0?1:-1}for(let t=0;t<4;t+=1){let n=t<2?0:2,r=t%2==0?1:-1,i=0;for(let t of pM)i=Math.max(i,r*e[t*3+n]+jj[t]);this.ropeLines[t]=Math.max(ww,i+.04)}}startHinge(e,t,n,r,i,a,o){let s=this.dir(n,r,this.a),c=this.dir(r,i,this.b),l=this.c.crossVectors(s,c);l.length()<.2&&l.copy(a).multiplyScalar(o),l.addScaledVector(s,-l.dot(s)),l.lengthSq()<1e-8&&l.copy(a).multiplyScalar(o).addScaledVector(s,-o*a.dot(s)),l.normalize(),e[t*3]=l.x,e[t*3+1]=l.y,e[t*3+2]=l.z}storeHingeStart(e,t,n,r,i,a,o){let s=e%2,c=this.dir(t,n,this.a),l=this.b.set(r[s*3],r[s*3+1],r[s*3+2]),u=e*6;this.hingeStart[u]=c.dot(i),this.hingeStart[u+1]=c.dot(a),this.hingeStart[u+2]=c.dot(o),this.hingeStart[u+3]=l.dot(i),this.hingeStart[u+4]=l.dot(a),this.hingeStart[u+5]=l.dot(o);for(let e=0;e<6;e+=1)this.hingeNow[u+e]=this.hingeStart[u+e]}hingeAxis(e,t,n,r,i,a){let o=e*6,s=this.hingeNow;return this.local.set(t.dot(n),t.dot(r),t.dot(i)).normalize(),this.localAxis.set(s[o],s[o+1],s[o+2]),this.swing.setFromUnitVectors(this.localAxis,this.local),this.localAxis.set(s[o+3],s[o+4],s[o+5]).applyQuaternion(this.swing),a.copy(n).multiplyScalar(this.localAxis.x).addScaledVector(r,this.localAxis.y).addScaledVector(i,this.localAxis.z),a.addScaledVector(t,-a.dot(t)).normalize()}carryHinges(){this.pelvisFrame(this.position),this.upperFrame(this.position);for(let e=0;e<4;e+=1){let t=e%2,n=e<2,r=n?t===0?Q.hipL:Q.hipR:t===0?Q.shoulderL:Q.shoulderR,i=n?t===0?Q.kneeL:Q.kneeR:t===0?Q.elbowL:Q.elbowR,a=n?this.fx:this.gx,o=n?this.fy:this.gy,s=n?this.fz:this.gz,c=this.dir(r,i,this.a),l=this.hingeAxis(e,c,a,o,s,this.b),u=e*6;this.hingeNow[u]=c.dot(a),this.hingeNow[u+1]=c.dot(o),this.hingeNow[u+2]=c.dot(s),this.hingeNow[u+3]=l.dot(a),this.hingeNow[u+4]=l.dot(o),this.hingeNow[u+5]=l.dot(s)}}get holding(){return this.steps*aM0){let e=Math.min(this.steps,t.steps-1)*3;this.obstacleActive=t.data[e]===1,this.obstacleX=t.data[e+1],this.obstacleZ=t.data[e+2]}let n=aM,r=this.position,i=this.previous,a=this.landedSeconds<0?cM:cM+(AM-cM)*Math.min(1,this.landedSeconds/jM);this.before.set(r);for(let e=0;e<26;e+=1)for(let t=0;t<3;t+=1){let o=e*3+t,s=r[o],c=(s-i[o])*a;i[o]=s,r[o]=s+c-(t===1?sM*n*n:0)}let o=this.pull;if(o!==null)for(let e=0;e<26;e+=1){let t=(o[e*3]-r[e*3])*this.pullShare,n=(o[e*3+1]-r[e*3+1])*this.pullShare,a=(o[e*3+2]-r[e*3+2])*this.pullShare,s=Math.sqrt(t*t+n*n+a*a),c=this.pullShare<1&&s>bN?bN/s:1;for(let o=0;o<3;o+=1){let s=(o===0?t:o===1?n:a)*c;r[e*3+o]=r[e*3+o]+s,i[e*3+o]=i[e*3+o]+s}}this.carryHinges();for(let e=0;e=0&&o===null&&this.toppleLegs(),this.applyFriction(),this.limitSpeed(),this.steps+=1;let s=0,c=0;for(let e=0;e<26;e+=1){let t=r[e*3]-i[e*3],a=r[e*3+1]-i[e*3+1],o=r[e*3+2]-i[e*3+2],l=Math.sqrt(t*t+a*a+o*o)/n;s=Math.max(s,l),xM.has(e)&&(c=Math.max(c,l))}this.stillSeconds=s=0?this.landedSeconds+=n:(this.torsoStillSeconds=c=OM||this.fallSeconds>=kM)&&(this.landedSeconds=0)),o===null&&(this.stillSeconds>=EM||this.landedSeconds>=MM)&&(this.asleep=!0)}solveRigid(){for(let e=0;er?r:c;if(l===c)return;let u=this.invMass[e],d=this.invMass[t],f=u+d;if(f<=0)return;let p=(c-l)/(c*f);i[e*3]=i[e*3]+a*p*u,i[e*3+1]=i[e*3+1]+o*p*u,i[e*3+2]=i[e*3+2]+s*p*u,i[t*3]=i[t*3]-a*p*d,i[t*3+1]=i[t*3+1]-o*p*d,i[t*3+2]=i[t*3+2]-s*p*d}solveKnees(){this.pelvisFrame(this.position);for(let e=0;e<2;e+=1){let t=e===0?Q.hipL:Q.hipR,n=e===0?Q.kneeL:Q.kneeR,r=this.dir(t,n,this.a),i=this.hingeAxis(e,r,this.fx,this.fy,this.fz,this.b),a=this.holding?Math.min(RM,this.kneeStart[e]+ZM[this.style].knee):RM;this.hinge(n,e===0?Q.ankleL:Q.ankleR,r,i,-.12,a)}}solveElbows(){this.upperFrame(this.position);for(let e=0;e<2;e+=1){let t=e===0?Q.shoulderL:Q.shoulderR,n=e===0?Q.elbowL:Q.elbowR,r=this.dir(t,n,this.a),i=this.hingeAxis(2+e,r,this.gx,this.gy,this.gz,this.b);this.hinge(n,e===0?Q.wristL:Q.wristR,r,i,-.12,QM)}}toppleLegs(){let e=this.position;for(let t=0;t<2;t+=1){let n=t===0?Q.hipL:Q.hipR,r=t===0?Q.kneeL:Q.kneeR,i=t===0?Q.ankleL:Q.ankleR,a=this.dir(n,r,this.a);if(a.y>bM){let i=this.dir(Q.hipR,Q.hipL,this.b),o=this.c.crossVectors(i,this.d.set(e[r*3]-e[n*3],e[r*3+1]-e[n*3+1],e[r*3+2]-e[n*3+2])).y>0?-1:1;this.rotateAbout(_M[t],n,i,o*yM*(a.y-bM))}let o=this.dir(r,i,this.a);if(o.y>bM){let n=this.kneeAxis(t===0?0:1,e,this.b),a=this.c.crossVectors(n,this.d.set(e[i*3]-e[r*3],e[i*3+1]-e[r*3+1],e[i*3+2]-e[r*3+2])).y>0?-1:1;this.rotateAbout(vM[t],r,n,a*yM*(o.y-bM))}}}solveTwist(){let e=this.dir(Q.pelvis,Q.upper,this.e),t=this.dir(Q.hipR,Q.hipL,this.a);t.addScaledVector(e,-t.dot(e));let n=this.dir(Q.shoulderR,Q.shoulderL,this.c);if(n.addScaledVector(e,-n.dot(e)),t.lengthSq()<1e-8||n.lengthSq()<1e-8)return;let r=Math.atan2(this.d.crossVectors(t,n).dot(e),t.dot(n)),i=Math.abs(r)-gM;if(i<=0)return;let a=this.spinInertia(mM,e),o=this.spinInertia(hM,e),s=Math.sign(r)*i*JM;this.rotateAbout(mM,Q.chest,e,-s*(o/(a+o))),this.rotateAbout(hM,Q.chest,e,s*(a/(a+o)))}spinInertia(e,t){let n=this.position,r=1e-6;for(let i=0;iMath.abs(p)+1e-6){let n=Math.atan2(r,u),a=Math.acos(p/i),c=oN(n-a),l=oN(n+a),d=(Math.abs(c)<=Math.abs(l)?c:l)*s;this.rotateAbout(tM[t],o,e,Math.min(qM,Math.max(-.04,d)))}}let m=this.dir(i,a,this.a),h=this.hingeAxis(t,m,this.fx,this.fy,this.fz,this.b),g=this.footNormal(t===0?0:1,this.d).multiplyScalar(this.footSign[t]);if(h.addScaledVector(c,-h.dot(c)),g.addScaledVector(c,-g.dot(c)),h.lengthSq()<.09||g.lengthSq()<.09)continue;h.normalize(),g.normalize();let _=Math.atan2(this.e.crossVectors(h,g).dot(c),h.dot(g)),v=Math.min(HM,Math.max(-HM,_));v!==_&&this.rotateAbout(eM[t],o,c,Math.min(KM,Math.max(-.01,(v-_)*s)))}}footNormal(e,t){let n=this.position,r=e===0?Q.ankleL:Q.ankleR,i=e===0?Q.toeL:Q.toeR,a=e===0?Q.heelL:Q.heelR;return t.set(n[i*3]-n[a*3],n[i*3+1]-n[a*3+1],n[i*3+2]-n[a*3+2]),this.turnPoint.set(n[r*3]-n[a*3],n[r*3+1]-n[a*3+1],n[r*3+2]-n[a*3+2]),t.cross(this.turnPoint).normalize()}cone(e,t,n,r,i,a){let o=this.dir(t,n,this.turnPoint),s=Math.acos(Math.min(1,Math.max(-1,o.dot(e))));if(s<=r)return;let c=this.turnAxis.crossVectors(o,e);if(c.lengthSq()<1e-10)return;c.normalize();let l=this.pressed(a,i,c)?YM*XM:YM;this.rotateAbout(a,i,c,Math.min(l,(s-r)*JM))}pressed(e,t,n){let r=this.position;for(let i=0;ijj[a]+.005)&&n.z*(r[a*3]-r[t*3])-n.x*(r[a*3+2]-r[t*3+2])<-1e-9)return!0}return!1}rotateAbout(e,t,n,r){let i=this.position;this.turnQuaternion.setFromAxisAngle(n,r);let a=i[t*3],o=i[t*3+1],s=i[t*3+2];for(let t=0;t=u||l<1e-6||(o.addScaledVector(c,(u-l)/l),sN(e,n,o))}}}closestOnSegment(e,t,n,r){let i=this.segmentA.subVectors(t,e),a=i.lengthSq(),o=a<1e-12?0:Math.min(1,Math.max(0,this.segmentB.subVectors(n,e).dot(i)/a));return r.copy(e).addScaledVector(i,o)}solveEnvironment(){let e=this.position;for(let t=0;t<26;t+=1){let n=jj[t],r=e[t*3],i=e[t*3+1],a=e[t*3+2];i=p||f<1e-9)continue;let m=(p-f)/f*dM,h=u*m,g=d*m;t?r+=s*h:a+=s*h,i+=g}let u=o?c-n*.6:SM;s*(t?r:a)>u&&(t?r=s*u:a=s*u)}for(let e=0;e<4;e+=1){let t=e<2?-ww:ww,o=e%2==0?-ww:ww,s=r-t,c=a-o,l=Math.sqrt(s*s+c*c),u=fM+n;l>=u||l<1e-9||i>1.45||(r=t+s/l*u,a=o+c/l*u)}if(this.obstacleActive&&i1e-9&&(r=this.obstacleX+e/i*o,a=this.obstacleZ+t/i*o)}e[t*3]=r,e[t*3+1]=i,e[t*3+2]=a}}limitSpeed(){let e=this.position,t=this.previous,n=FM*aM,r=IM*aM;for(let i=0;i<26;i+=1){let a=e[i*3]-t[i*3],o=e[i*3+1]-t[i*3+1],s=e[i*3+2]-t[i*3+2],c=Math.sqrt(a*a+o*o+s*s);if(c>n||cn?n/c:LM;t[i*3]=e[i*3]-a*r,t[i*3+1]=e[i*3+1]-o*r,t[i*3+2]=e[i*3+2]-s*r}}}applyFriction(){let e=this.position,t=this.previous;for(let n=0;n<26;n+=1)e[n*3+1]>jj[n]+.004||(t[n*3]=t[n*3]+(e[n*3]-t[n*3])*lM,t[n*3+2]=t[n*3+2]+(e[n*3+2]-t[n*3+2])*lM,t[n*3+1]>e[n*3+1]&&(t[n*3+1]=e[n*3+1]))}settle(e=4,t){let n=this.steps+Math.ceil(e/aM);for(;!this.asleep&&this.steps1e-9?n.divideScalar(i):n.set(0,1,0)}pelvisFrame(e){this.fx.set(e[Q.hipL*3]-e[Q.hipR*3],e[Q.hipL*3+1]-e[Q.hipR*3+1],e[Q.hipL*3+2]-e[Q.hipR*3+2]).normalize(),this.fz.set(e[Q.belly*3]-(e[Q.hipL*3]+e[Q.hipR*3])/2,e[Q.belly*3+1]-(e[Q.hipL*3+1]+e[Q.hipR*3+1])/2,e[Q.belly*3+2]-(e[Q.hipL*3+2]+e[Q.hipR*3+2])/2),this.fz.addScaledVector(this.fx,-this.fz.dot(this.fx)).normalize(),this.fy.crossVectors(this.fz,this.fx).normalize()}upperFrame(e){this.gx.set(e[Q.shoulderL*3]-e[Q.shoulderR*3],e[Q.shoulderL*3+1]-e[Q.shoulderR*3+1],e[Q.shoulderL*3+2]-e[Q.shoulderR*3+2]).normalize(),this.gy.set(e[Q.neck*3]-e[Q.upper*3],e[Q.neck*3+1]-e[Q.upper*3+1],e[Q.neck*3+2]-e[Q.upper*3+2]),this.gy.addScaledVector(this.gx,-this.gy.dot(this.gx)).normalize(),this.gz.crossVectors(this.gx,this.gy).normalize()}pelvisForward(e){return this.pelvisFrame(this.position),e.copy(this.fz)}};function lN(e,t){e.setLimp(nM,(t&1)!=0),e.setLimp(rM,(t&2)!=0),e.setLimp(iM,(t&4)!=0)}var uN=[`hips`,`spine`,`chest`,`upperChest`,`neck`,`head`,`shoulderL`,`elbowL`,`gloveL`,`shoulderR`,`elbowR`,`gloveR`,`hipL`,`kneeL`,`ankleL`,`hipR`,`kneeR`,`ankleR`],dN=uN.map(e=>({shoulderL:[Q.shoulderL,Q.elbowL],elbowL:[Q.elbowL,Q.wristL],gloveL:[Q.wristL,Q.fistL],shoulderR:[Q.shoulderR,Q.elbowR],elbowR:[Q.elbowR,Q.wristR],gloveR:[Q.wristR,Q.fistR],hipL:[Q.hipL,Q.kneeL],kneeL:[Q.kneeL,Q.ankleL],hipR:[Q.hipR,Q.kneeR],kneeR:[Q.kneeR,Q.ankleR]})[e]??null),fN=uN.map(e=>({chest:Q.chest,shoulderL:Q.shoulderL,shoulderR:Q.shoulderR,hipL:Q.hipL,hipR:Q.hipR})[e]??-1),pN=.1,mN=.35,hN=.6,gN=10,_N=.35,vN=6,yN=.25,bN=.05,xN=.3,SN=6;function CN(e){let t=e.lateral;if(e.target===`body`){let n=Math.min(2.6,Math.max(1,.9+e.amount/90));return{x:.3*t*n,y:-.05*n,z:-n,driveX:.1*t*n,driveY:-.2*n,driveZ:.45*n,twist:0}}let n=Math.min(3.2,Math.max(1.4,1.2+e.amount/70)),r=n/3.2;switch(e.punchClass){case`hook`:return{x:t*n,y:.05*n,z:-.25*n,driveX:.55*t*r,driveY:-.1*r,driveZ:.85*r,twist:.6};case`uppercut`:return{x:0,y:.8*n,z:-.65*n,driveX:0,driveY:.25*r,driveZ:-1.1*r,twist:.4};case`straight`:return{x:.12*t*n,y:.1*n,z:-n,driveX:.1*t*r,driveY:0,driveZ:-1.25*r,twist:.3};default:return{x:.12*t*n,y:.1*n,z:-.85*n,driveX:.08*t*r,driveY:0,driveZ:-1*r,twist:.2}}}var wN=class{rig;root;body=new cN;ahead=new cN;aheadOf=null;aheadBlows=0;ragdolling=!1;age=0;accumulator=0;sampleNow=new Float64Array(78);sampleBefore=new Float64Array(78);sampleRoot=new J;sampleSeconds=0;samples=0;drawn=new Float64Array(78);velocities=new Float64Array(78);offsets=uN.map(()=>new cm);fromLocal=uN.map(()=>new cm);fromPositions=uN.map(()=>new J);fromHips=new J;riseLocal=uN.map(()=>new cm);riseHips=new J;riseTurns=uN.map(()=>new cm);solvedTurns=uN.map(()=>new cm);rootTurn=new cm;turn=new cm;riseCaptured=!1;lastFall=null;recordPositions=new Float64Array(78);recordVelocities=new Float64Array(78);recordOffsets=uN.map(()=>new cm);recordImpulses=[];recordObstacles={data:new Float64Array(NM*3),steps:0};recordLosses=[];recordRest={position:new Float64Array(78),previous:new Float64Array(78),steps:-1};replaying=null;followTargets=new Float64Array(78);followOffsets=uN.map(()=>new cm);blendedOffsets=uN.map(()=>new cm);following=!1;followAge=0;primed=!1;pending=null;lost=0;clock=0;bones;frame=new cm;world=new cm;axisX=new J;axisY=new J;axisZ=new J;hint=new J;point=new J;other=new J;pelvisX=new J;pelvisY=new J;upperX=new J;headX=new J;kneeAxes=[new J(1,0,0),new J(1,0,0)];elbowAxes=[new J,new J];spineLength;tiltJoint=new J;tiltChest=new J;tiltAcross=new J;tiltTurn=new cm;constructor(e,t){this.rig=e,this.root=t,this.bones=uN.map(t=>e.bones[t]);let n=new Map;t.traverse(e=>{e instanceof Yg&&n.set(e,[e.quaternion.clone(),e.position.clone()])}),e.resetToRest(),t.updateMatrixWorld(!0);let r=new Float64Array(78);this.read(r),this.body.calibrate(r),this.ahead.calibrate(r),this.spineLength=this.tiltJoint.setFromMatrixPosition(e.bones.spine.matrixWorld).distanceTo(this.tiltChest.setFromMatrixPosition(e.bones.chest.matrixWorld));for(let[e,[t,r]]of n)e.quaternion.copy(t),e.position.copy(r);t.updateMatrixWorld(!0)}get active(){return this.ragdolling}get record(){return this.lastFall}sample(e){this.ragdolling||e<=0||(this.sampleBefore.set(this.sampleNow),this.read(this.sampleNow),this.sampleRoot.setFromMatrixPosition(this.root.matrixWorld),this.sampleSeconds=e,this.samples=Math.min(2,this.samples+1))}tick(e){this.clock+=e}takeBlow(e,t,n,r,i){let a={step:0,x:e.x,y:e.y,z:e.z,driveX:t.x,driveY:t.y,driveZ:t.z,target:n,twist:r,style:i};if(this.ragdolling){if(this.age>mN)return;if(this.replaying!==null){this.replaying.impulses.length===0&&this.replaying.impulses.push({...a,step:this.body.steps});return}this.lastFall?.impulses.push(this.body.impulse(a));return}this.pending={...a,at:this.clock}}prime(){this.primed=this.lastFall!==null}forget(){this.lastFall=null,this.primed=!1}start(e){let t=this.primed?this.lastFall:null;if(this.primed=!1,this.root.updateMatrixWorld(!0),this.captureLocals(this.fromLocal,this.fromHips),t!==null){this.replaying=t,this.body.start(t.positions,t.velocities,t.style),this.body.trackObstacle(t.obstacles,!0),this.replayLosses(this.body,t,0);for(let[e,n]of t.offsets.entries())this.offsets[e].copy(n);this.aheadOf=null,t.rest.steps<0&&this.runAhead(t,0)}else{this.replaying=null;let t=this.point.setFromMatrixPosition(this.root.matrixWorld).sub(this.sampleRoot),n=this.samples>0&&t.lengthSq()<=_N*_N;if(!n)this.read(this.sampleNow);else for(let e=0;e<26;e+=1)for(let n of[this.sampleNow,this.sampleBefore])n[e*3]=n[e*3]+t.x,n[e*3+1]=n[e*3+1]+t.y,n[e*3+2]=n[e*3+2]+t.z;let r=n&&this.samples>=2&&this.sampleSeconds>0?this.sampleSeconds:0;this.samples=0;for(let e=0;e<78;e+=1){let t=r>0?(this.sampleNow[e]-this.sampleBefore[e])/r:0;this.velocities[e]=Math.max(-3,Math.min(PM,t))}let i=this.rig.bones.neck,a=this.rig.bones.head;this.point.copy(this.rig.restLocalPosition(a)).applyMatrix4(i.matrixWorld).sub(this.other.setFromMatrixPosition(a.matrixWorld));for(let e of Qj)this.sampleNow[e*3]=this.sampleNow[e*3]+this.point.x,this.sampleNow[e*3+1]=this.sampleNow[e*3+1]+this.point.y,this.sampleNow[e*3+2]=this.sampleNow[e*3+2]+this.point.z;let o=this.pending!==null&&this.clock-this.pending.at<=hN?this.pending:null;this.body.start(this.sampleNow,this.velocities,o?.style??e),this.computeOffsets();let s=this.recordImpulses;s.length=0,o!==null&&s.push(this.body.impulse({step:0,x:o.x,y:o.y,z:o.z,driveX:o.driveX,driveY:o.driveY,driveZ:o.driveZ,target:o.target,twist:o.twist}));let c=this.recordObstacles;c.steps=0,this.body.trackObstacle(c,!1),this.recordPositions.set(this.sampleNow),this.recordVelocities.set(this.velocities);for(let e=0;e=.008333333333333333&&i=.008333333333333333&&n=0)this.body.restore(t);else{let t=this.body.steps+Math.ceil(SN/aM);for(;!this.body.asleep&&this.body.steps=1)return;let t=Math.min(1,Math.max(0,e));this.bodyTurns(this.solvedTurns),this.rig.bones.hips.position.lerpVectors(this.riseHips,this.rig.bones.hips.position,t);for(let e=0;esN(e,t,n),r=e=>this.point.setFromMatrixPosition(e.matrixWorld),i=(e,t,n)=>n.setFromMatrixColumn(e.matrixWorld,t).normalize();n(Q.pelvis,r(t.hips)),n(Q.hipL,r(t.hipL)),n(Q.hipR,r(t.hipR)),n(Q.belly,r(t.hips).addScaledVector(i(t.hips,2,this.axisZ),Mj)),n(Q.chest,r(t.chest)),n(Q.upper,r(t.upperChest)),n(Q.neck,r(t.neck)),n(Q.shoulderL,r(t.shoulderL)),n(Q.shoulderR,r(t.shoulderR)),n(Q.head,r(t.head)),n(Q.crown,r(t.head).addScaledVector(i(t.head,1,this.axisY),Nj)),n(Q.face,r(t.head).addScaledVector(i(t.head,2,this.axisZ),Pj).addScaledVector(i(t.head,1,this.axisY),Fj)),n(Q.elbowL,r(t.elbowL)),n(Q.wristL,r(t.gloveL)),n(Q.fistL,r(t.gloveL).addScaledVector(i(t.gloveL,1,this.axisY),Ij)),n(Q.elbowR,r(t.elbowR)),n(Q.wristR,r(t.gloveR)),n(Q.fistR,r(t.gloveR).addScaledVector(i(t.gloveR,1,this.axisY),Ij)),n(Q.kneeL,r(t.kneeL)),n(Q.ankleL,r(t.ankleL)),n(Q.toeL,r(t.toeL)),n(Q.heelL,r(t.ankleL).addScaledVector(i(t.toeL,1,this.axisY),-.07).addScaledVector(i(t.toeL,2,this.axisZ),-.09)),n(Q.kneeR,r(t.kneeR)),n(Q.ankleR,r(t.ankleR)),n(Q.toeR,r(t.toeR)),n(Q.heelR,r(t.ankleR).addScaledVector(i(t.toeR,1,this.axisY),-.07).addScaledVector(i(t.toeR,2,this.axisZ),-.09))}computeOffsets(){this.prepareFrames(this.body.position);for(let e=0;e=0&&this.rig.setWorldPosition(i,aN(e,a,this.point)),uN[r]===`upperChest`&&(this.rig.bones.clavicleL.updateWorldMatrix(!1,!1),this.rig.bones.clavicleR.updateWorldMatrix(!1,!1))}this.root.updateMatrixWorld(!0)}tiltHips(e){let t=this.rig.bones.hips,n=this.point.setFromMatrixPosition(t.matrixWorld),r=this.tiltJoint.copy(this.rig.restLocalPosition(this.rig.bones.spine)).applyMatrix4(t.matrixWorld).sub(n),i=aN(e,Q.chest,this.tiltChest).sub(n),a=this.pelvisX,o=a.dot(r),s=this.tiltAcross.copy(r).addScaledVector(a,-o),c=(r.lengthSq()+i.lengthSq()-this.spineLength*this.spineLength)/2-o*a.dot(i),l=s.dot(i),u=this.other.crossVectors(a,s).dot(i),d=Math.hypot(l,u);if(d<1e-9)return;let f=Math.atan2(u,l),p=Math.acos(Math.min(1,Math.max(-1,c/d))),m=oN(f-p),h=oN(f+p),g=Math.abs(m)<=Math.abs(h)?m:h;Math.abs(g)<1e-5||(t.getWorldQuaternion(this.world).premultiply(this.tiltTurn.setFromAxisAngle(a,g)),this.rig.setWorldRotation(t,this.world))}prepareFrames(e){this.segment(e,Q.hipR,Q.hipL,this.pelvisX),this.axisZ.set(e[Q.belly*3]-(e[Q.hipL*3]+e[Q.hipR*3])/2,e[Q.belly*3+1]-(e[Q.hipL*3+1]+e[Q.hipR*3+1])/2,e[Q.belly*3+2]-(e[Q.hipL*3+2]+e[Q.hipR*3+2])/2),this.axisZ.addScaledVector(this.pelvisX,-this.axisZ.dot(this.pelvisX)).normalize(),this.pelvisY.crossVectors(this.axisZ,this.pelvisX).normalize(),this.segment(e,Q.shoulderR,Q.shoulderL,this.upperX),this.segment(e,Q.head,Q.crown,this.axisX),this.segment(e,Q.head,Q.face,this.other),this.headX.crossVectors(this.axisX,this.other).normalize()}segment(e,t,n,r){return r.set(e[n*3]-e[t*3],e[n*3+1]-e[t*3+1],e[n*3+2]-e[t*3+2]),r.lengthSq()>1e-12?r.normalize():r.set(0,1,0)}frameFor(e,t,n){let r=uN[e];switch(r){case`hips`:return cj(this.pelvisY,this.pelvisX,`x`,n);case`spine`:{let r=this.bones[e];return r.updateWorldMatrix(!1,!1),this.axisY.setFromMatrixPosition(r.matrixWorld),this.axisY.set(t[Q.chest*3]-this.axisY.x,t[Q.chest*3+1]-this.axisY.y,t[Q.chest*3+2]-this.axisY.z),this.axisY.lengthSq()>1e-12?this.axisY.normalize():this.segment(t,Q.pelvis,Q.chest,this.axisY),cj(this.axisY,this.hint.copy(this.pelvisX).multiplyScalar(.75).addScaledVector(this.upperX,.25),`x`,n)}case`chest`:return this.segment(t,Q.chest,Q.upper,this.axisY),cj(this.axisY,this.hint.copy(this.pelvisX).multiplyScalar(.4).addScaledVector(this.upperX,.6),`x`,n);case`upperChest`:return this.segment(t,Q.upper,Q.neck,this.axisY),cj(this.axisY,this.upperX,`x`,n);case`neck`:return this.segment(t,Q.neck,Q.head,this.axisY),cj(this.axisY,this.hint.copy(this.upperX).add(this.headX),`x`,n);case`head`:return this.segment(t,Q.head,Q.crown,this.axisY),cj(this.axisY,this.segment(t,Q.head,Q.face,this.hint),`z`,n);case`shoulderL`:case`elbowL`:case`gloveL`:case`shoulderR`:case`elbowR`:case`gloveR`:{let i=+!r.endsWith(`L`),a=dN[e],o=this.body.elbowAxis(i,t,this.elbowAxes[i]);return this.segment(t,a[0],a[1],this.axisY),cj(this.axisY,o,`x`,n)}case`hipL`:case`kneeL`:case`hipR`:case`kneeR`:{let i=+!r.endsWith(`L`),a=dN[e],o=this.body.kneeAxis(i,t,this.kneeAxes[i]);return this.segment(t,a[0],a[1],this.axisY),cj(this.axisY,o,`x`,n)}default:{let e=r===`ankleL`,i=e?Q.ankleL:Q.ankleR,a=e?Q.toeL:Q.toeR,o=e?Q.heelL:Q.heelR;return this.segment(t,i,a,this.axisY),this.segment(t,o,a,this.hint),this.other.set(t[i*3]-t[o*3],t[i*3+1]-t[o*3+1],t[i*3+2]-t[o*3+2]),this.hint.cross(this.other),cj(this.axisY,this.hint,`x`,n)}}}};function TN(e,t,n){let r=Math.min(1,Math.max(0,(n-e)/(t-e)));return r*r*(3-2*r)}var EN=3,DN=5;function ON(e){return 1-.74*TN(-.6,1.4,e)}var kN=.23,AN=.22,jN=.22,MN=1.15,NN=.5,PN=.9,FN=.045,IN=[.33,.66],LN=FN*2*.33*.67,RN=.04;function zN(e,t){return{pressX:Math.min(AN,Math.max(0,Math.abs(e)+kN-ww)),pressZ:Math.min(AN,Math.max(0,Math.abs(t)+kN-ww))}}function BN(e,t,n){return n.x=Math.sign(e)*Math.max(0,Math.abs(e)+kN-ww-AN),n.z=Math.sign(t)*Math.max(0,Math.abs(t)+kN-ww-AN),n}var VN=` +float ropeGive(float along, float position, float height) { + float near = 1.0 - smoothstep(${jN.toFixed(2)}, ${MN.toFixed(2)}, abs(along)); + float tied = smoothstep(0.0, ${NN.toFixed(2)}, ${ww.toFixed(4)} - abs(position)); + return near * tied * (${PN.toFixed(2)} + ${(1-PN).toFixed(2)} * smoothstep(0.5, 0.88, height)); +} +`,HN=` vec3 ropeWorld = (modelMatrix * vec4(transformed, 1.0)).xyz; bool ropeSideX = abs(ropeWorld.x) > abs(ropeWorld.z); vec3 ropeOut = ropeSideX ? vec3(sign(ropeWorld.x), 0.0, 0.0) : vec3(0.0, 0.0, sign(ropeWorld.z)); @@ -4546,14 +4944,15 @@ for (int ropeIndex = 0; ropeIndex < 2; ropeIndex += 1) { float along = ropeSideX ? contact.y : contact.x; float press = ropeSideX ? contact.z : contact.w; float sameSide = ropeSideX ? step(0.0, ropeOut.x * contact.x) : step(0.0, ropeOut.z * contact.y); - ropeFlex += press * sameSide * (1.0 - smoothstep(0.0, 0.8, abs(ropeAlong - along))); + ropeFlex = max(ropeFlex, press * sameSide * ropeGive(ropeAlong - along, ropeAlong, ropeWorld.y)); } -float ropeAnchor = smoothstep(0.0, 0.7, ${Cx.toFixed(2)} - abs(ropeAlong)); -float ropeHeightWeight = 0.45 + 0.55 * smoothstep(0.4, 1.0, ropeWorld.y); -transformed += ropeOut * min(1.0, ropeFlex) * 0.22 * ropeHeightWeight * ropeAnchor; -`;function iE(e,t){let n=document.createElement(`canvas`);n.width=e,n.height=e;let r=n.getContext(`2d`);r!==null&&t(r,e);let i=new lm(n);return i.anisotropy=8,i.colorSpace=Vl,i}function aE(){return iE(1024,(e,t)=>{e.fillStyle=`#9d968a`,e.fillRect(0,0,t,t);let n=e.createLinearGradient(0,0,t,t);n.addColorStop(0,`rgba(255,255,255,0.08)`),n.addColorStop(.5,`rgba(60,50,40,0.06)`),n.addColorStop(1,`rgba(255,255,255,0.04)`),e.fillStyle=n,e.fillRect(0,0,t,t);for(let n=0;n<2600;n+=1){let r=Math.sin(n*12.9898)*43758.5453%1,i=Math.sin(n*78.233)*12543.1234%1;e.fillStyle=`rgba(${n%3==0?`255,255,255`:`70,55,45`},${.02+n%5*.006})`,e.fillRect(Math.abs(r)*t,Math.abs(i)*t,2+n%4,1+n%3)}for(let n=0;n<40;n+=1){let r=Math.abs(Math.sin(n*91.7)*7919.3%1)*t,i=Math.abs(Math.sin(n*47.1)*4271.9%1)*t;e.fillStyle=`rgba(80,60,50,${.05+n%4*.02})`,e.beginPath(),e.ellipse(r,i,18+n%7*6,6+n%5*3,n*.7%Math.PI,0,Math.PI*2),e.fill()}e.strokeStyle=`rgba(30,60,140,0.85)`,e.lineWidth=12,e.beginPath(),e.arc(t/2,t/2,t*.2,0,Math.PI*2),e.stroke(),e.fillStyle=`rgba(30,60,140,0.9)`,e.font=`800 ${Math.round(t*.062)}px Inter, system-ui, sans-serif`,e.textAlign=`center`,e.textBaseline=`middle`,e.fillText(`H A N D S`,t/2,t/2-t*.012),e.font=`600 ${Math.round(t*.024)}px Inter, system-ui, sans-serif`,e.fillText(`AUTHORITATIVE BOXING`,t/2,t/2+t*.052),e.strokeStyle=`rgba(30,60,140,0.6)`,e.lineWidth=6,e.strokeRect(t*.035,t*.035,t*.93,t*.93)})}function oE(){let e=[],t=[],n=[],r=new Nd;r.name=`ring`;let i=aE();n.push(i);let a=new sh({map:i,roughness:.88,metalness:0});t.push(a);let o=new qm(Cx*2,Cx*2);e.push(o);let s=new up(o,a);s.rotation.x=-Math.PI/2,s.position.y=.002,s.receiveShadow=!0,r.add(s);let c=new sh({color:`#1c2b52`,roughness:.85});t.push(c);let l=new Jm(Cx*.98,wx,4,1);e.push(l);let u=new up(l,c);u.rotation.x=-Math.PI/2,u.rotation.z=Math.PI/4,u.position.y=.001,u.receiveShadow=!0,r.add(u);let d=new sh({color:`#0c1424`,roughness:.9});t.push(d);let f=new pm(Tx*2,1,Tx*2);e.push(f);let p=new up(f,d);p.position.y=-.5,p.receiveShadow=!0,r.add(p);let m=new sh({color:`#101b33`,roughness:.95});t.push(m);for(let t=0;t<4;t+=1){let n=new qm(Tx*2,.95);e.push(n);let i=new up(n,m),a=t*Math.PI/2;i.position.set(Math.sin(a)*(Tx-.001),-.48,Math.cos(a)*(Tx-.001)),i.rotation.y=a,r.add(i)}let h=new sh({color:`#9aa7b5`,roughness:.35,metalness:.75});t.push(h);let g=[kx.blue,kx.neutral,kx.red,kx.neutral],_=new gm(.055,.055,1.55,12);e.push(_);let v=new pm(.34,.52,.13);e.push(v);let y=new pm(.16,.07,.1);e.push(y);let b=[];for(let e=0;e<4;e+=1){let n=Math.PI/4+e*Math.PI/2,i=Math.sin(n)*Ex*Math.SQRT2*.72,a=Math.cos(n)*Ex*Math.SQRT2*.72;b.push(new Z(i,0,a));let o=new up(_,h);o.position.set(i,.78,a),o.castShadow=!0,r.add(o);let s=new sh({color:g[e],roughness:.55});t.push(s);for(let e of[.62,1.12]){let t=new up(v,s);t.position.set(i*.985,e,a*.985),t.lookAt(0,e,0),t.castShadow=!0,r.add(t)}}let x=[new ed(0,0,0,0),new ed(0,0,0,0)],S={uRopeContactA:{value:x[0]},uRopeContactB:{value:x[1]}},C=[12131356,15067115,1920728].map(e=>{let n=new sh({color:e,roughness:.42,metalness:.05});return n.onBeforeCompile=e=>{e.uniforms.uRopeContactA=S.uRopeContactA,e.uniforms.uRopeContactB=S.uRopeContactB,e.vertexShader=e.vertexShader.replace(`#include `,`#include -uniform vec4 uRopeContactA; -uniform vec4 uRopeContactB;`).replace(`#include `,`#include ${rE}`)},t.push(n),n}),w=(e,t)=>{for(let[n,r]of[e,t].entries()){let e=x[n];if(r===null){e.set(0,0,0,0);continue}let t=nE(r.x,r.z);e.set(r.x,r.z,t.pressX,t.pressZ)}};for(let n=0;n<4;n+=1){let i=b[n],a=b[(n+1)%4];for(let[n,o]of Dx.entries()){let s=i.clone().add(a).multiplyScalar(.5);s.y=o-.045;let c=new Wm(new Z(i.x,o,i.z),s,new Z(a.x,o,a.z)),l=new Xm(c,24,.028,8,!1);e.push(l);let u=new up(l,C[n]);u.castShadow=!0,r.add(u);for(let n of[.33,.66]){let i=c.getPoint(n),a=new pm(.035,.82,.012);e.push(a);let s=new sh({color:14212840,roughness:.6});t.push(s);let l=new up(a,s);l.position.set(i.x,o-.36,i.z),l.lookAt(0,o-.36,0),r.add(l)}}}return{group:r,materials:t,geometries:e,textures:n,setRopeContacts:w,ropeContacts:x}}function sE(e){for(let t of e.geometries)t.dispose();for(let t of e.materials)t.dispose();for(let t of e.textures)t.dispose()}function cE(e,t=2){let n=e.getBoundingClientRect(),r=Math.max(1,Math.round(n.width)),i=Math.max(1,Math.round(n.height)),a=Math.min(t,Math.max(1,window.devicePixelRatio||1));(e.width!==Math.round(r*a)||e.height!==Math.round(i*a))&&(e.width=Math.round(r*a),e.height=Math.round(i*a));let o=e.getContext(`2d`);return o===null?null:(o.setTransform(a,0,0,a,0,0),{context:o,viewport:{width:r,height:i},dpr:a})}function lE(e,t,n){let r=e.detail.endsWith(`:head`)||e.detail.endsWith(`:body`);return t===void 0||![`hit`,`counter_hit`].includes(e.kind)||!r?!1:t.is_downed||n?.finish_method===`flash_ko`&&n.winner_id!==null&&n.winner_id===e.actor_id}function uE(e){for(let t of e.passes)t.dispose?.();e.dispose()}function dE(e,t,n,r,i){e.setRenderTarget(t.readBuffer);let a=e.compileAsync(n,r,i);return e.setRenderTarget(null),a}var fE=-4e-4,pE=10;function mE(e,t,n){e.autoUpdate=!t,e.intensity=+!t,e.bias=t?pE:fE,(t&&e.map!==null||e.mapSize.x!==n)&&(e.mapSize.set(n,n),e.map?.dispose(),e.map=null)}function hE(e,t){let n=t.fighters.findIndex(t=>t.player_id===e.target_id),r=t.fighters.findIndex(t=>t.player_id===e.actor_id),i=e.kind===`block`||e.kind===`perfect_block`;return{recipientIndex:i?r:n,puncherIndex:i?n:r}}function gE(e,t,n){if(e===void 0||t===void 0)return;let r=n.x(t.x)-n.x(e.x),i=n.z(t.y)-n.z(e.y),a=Math.hypot(r,i);return Number.isFinite(a)&&a>.001?{x:r/a,z:i/a}:void 0}var _E=e=>e.kind===`hit`||e.kind===`counter_hit`,vE=e=>e.kind===`block`||e.kind===`perfect_block`,yE=480,bE=.56,xE=1.8,SE=1.7,CE=.5,wE=3.42,TE=2.2,EE=2.95,DE=2.4,OE=1.6,kE={uniforms:{tDiffuse:{value:null},uTime:{value:0},uVignette:{value:.32},uGrain:{value:.035}},vertexShader:` +transformed += ropeOut * ropeFlex; +`;function UN(e,t){let n=document.createElement(`canvas`);n.width=e,n.height=e;let r=n.getContext(`2d`);r!==null&&t(r,e);let i=new F_(n);return i.anisotropy=8,i.colorSpace=hp,i}function WN(){return UN(1024,(e,t)=>{e.fillStyle=`#9d968a`,e.fillRect(0,0,t,t);let n=e.createLinearGradient(0,0,t,t);n.addColorStop(0,`rgba(255,255,255,0.08)`),n.addColorStop(.5,`rgba(60,50,40,0.06)`),n.addColorStop(1,`rgba(255,255,255,0.04)`),e.fillStyle=n,e.fillRect(0,0,t,t);for(let n=0;n<2600;n+=1){let r=Math.sin(n*12.9898)*43758.5453%1,i=Math.sin(n*78.233)*12543.1234%1;e.fillStyle=`rgba(${n%3==0?`255,255,255`:`70,55,45`},${.02+n%5*.006})`,e.fillRect(Math.abs(r)*t,Math.abs(i)*t,2+n%4,1+n%3)}for(let n=0;n<40;n+=1){let r=Math.abs(Math.sin(n*91.7)*7919.3%1)*t,i=Math.abs(Math.sin(n*47.1)*4271.9%1)*t;e.fillStyle=`rgba(80,60,50,${.05+n%4*.02})`,e.beginPath(),e.ellipse(r,i,18+n%7*6,6+n%5*3,n*.7%Math.PI,0,Math.PI*2),e.fill()}e.strokeStyle=`rgba(30,60,140,0.85)`,e.lineWidth=12,e.beginPath(),e.arc(t/2,t/2,t*.2,0,Math.PI*2),e.stroke(),e.fillStyle=`rgba(30,60,140,0.9)`,e.font=`800 ${Math.round(t*.062)}px Inter, system-ui, sans-serif`,e.textAlign=`center`,e.textBaseline=`middle`,e.fillText(`H A N D S`,t/2,t/2-t*.012),e.font=`600 ${Math.round(t*.024)}px Inter, system-ui, sans-serif`,e.fillText(`CHAMPIONSHIP BOXING`,t/2,t/2+t*.052),e.strokeStyle=`rgba(30,60,140,0.6)`,e.lineWidth=6,e.strokeRect(t*.035,t*.035,t*.93,t*.93)})}function GN(){let e=[],t=[],n=[],r=new oh;r.name=`ring`;let i=WN();n.push(i);let a=new Pv({map:i,roughness:.88,metalness:0});t.push(a);let o=new bv(bw*2,bw*2);e.push(o);let s=new Ig(o,a);s.rotation.x=-Math.PI/2,s.position.y=.002,s.receiveShadow=!0,r.add(s);let c=new Pv({color:`#1c2b52`,roughness:.85});t.push(c);let l=new xv(bw*.98,xw,4,1);e.push(l);let u=new Ig(l,c);u.rotation.x=-Math.PI/2,u.rotation.z=Math.PI/4,u.position.y=.001,u.receiveShadow=!0,r.add(u);let d=new Pv({color:`#0c1424`,roughness:.9});t.push(d);let f=new z_(Sw*2,1,Sw*2);e.push(f);let p=new Ig(f,d);p.position.y=-.5,p.receiveShadow=!0,r.add(p);let m=new Pv({color:`#101b33`,roughness:.95});t.push(m);for(let t=0;t<4;t+=1){let n=new bv(Sw*2,.95);e.push(n);let i=new Ig(n,m),a=t*Math.PI/2;i.position.set(Math.sin(a)*(Sw-.001),-.48,Math.cos(a)*(Sw-.001)),i.rotation.y=a,r.add(i)}let h=new Pv({color:`#9aa7b5`,roughness:.35,metalness:.75});t.push(h);let g=[Dw.neutral,Dw.red,Dw.neutral,Dw.blue],_=new H_(.055,.055,1.55,12);e.push(_);let v=new z_(.34,.52,.13);e.push(v);let y=new z_(.16,.07,.1);e.push(y);let b=[];for(let e=0;e<4;e+=1){let n=Math.PI/4+e*Math.PI/2,i=Math.sin(n)*Cw*Math.SQRT2*.72,a=Math.cos(n)*Cw*Math.SQRT2*.72;b.push(new J(i,0,a));let o=new Ig(_,h);o.position.set(i,.78,a),o.castShadow=!0,r.add(o);let s=new Pv({color:g[e],roughness:.55});t.push(s);for(let e of[.62,1.12]){let t=new Ig(v,s);t.position.set(i*.985,e,a*.985),t.lookAt(0,e,0),t.castShadow=!0,r.add(t)}}let x=[new Dm(0,0,0,0),new Dm(0,0,0,0)],S={uRopeContactA:{value:x[0]},uRopeContactB:{value:x[1]}},C=[12131356,15067115,1920728],w=e=>{e.uniforms.uRopeContactA=S.uRopeContactA,e.uniforms.uRopeContactB=S.uRopeContactB,e.vertexShader=e.vertexShader.replace(`#include `,`#include \nuniform vec4 uRopeContactA;\nuniform vec4 uRopeContactB;\n${VN}`).replace(`#include `,`#include ${HN}`)},T=e=>{let n=new Pv(e);return n.onBeforeCompile=w,t.push(n),n},E=new Iv;E.onBeforeCompile=w,E.customProgramCacheKey=()=>`hands-rope-depth`,t.push(E);let D=e=>T({color:e,roughness:.42,metalness:.05}),O=C.map(D),ee=C.map(D),te=T({color:14212840,roughness:.6}),k=T({color:14212840,roughness:.6});ee.push(k);let ne=Tw[0]-LN-RN,A=Tw[Tw.length-1]-LN+RN,re=new z_(.035,A-ne,.012,1,6,1);e.push(re);let ie=(e,t)=>{for(let[n,r]of[e,t].entries()){let e=x[n];if(r===null){e.set(0,0,0,0);continue}let t=zN(r.x,r.z);e.set(r.x,r.z,t.pressX,t.pressZ)}};for(let t=0;t<4;t+=1){let n=b[t],i=b[(t+1)%4];for(let[a,o]of Tw.entries()){let s=n.clone().add(i).multiplyScalar(.5);s.y=o-FN;let c=new Cv(new gv(new J(n.x,o,n.z),s,new J(i.x,o,i.z)),72,.028,8,!1);e.push(c);let l=new Ig(c,(t===EN?ee:O)[a]);l.castShadow=!0,l.customDepthMaterial=E,t===EN&&(l.renderOrder=DN),r.add(l)}for(let e of IN){let a=n.x+(i.x-n.x)*e,o=n.z+(i.z-n.z)*e,s=new Ig(re,t===EN?k:te);s.position.set(a,(ne+A)/2,o),s.lookAt(0,s.position.y,0),t===EN&&(s.renderOrder=DN),r.add(s)}}for(let e of ee)e.transparent=!0;return{group:r,materials:t,geometries:e,textures:n,setRopeContacts:ie,ropeContacts:x,setNearRopeOpacity:e=>{for(let t of ee)t.opacity=K.clamp(e,0,1),t.visible=t.opacity>.03,t.depthWrite=t.opacity>=1},nearRopeOpacity:()=>ee[0].opacity}}function KN(e){for(let t of e.geometries)t.dispose();for(let t of e.materials)t.dispose();for(let t of e.textures)t.dispose()}var qN={MHeadMat0:`head`,GlovesMat0:`gloves`,MBodyMat0:`body`,ShoesMat0:`shoes`,PantsMat0:`pants`},JN=new Map,YN=new Map,XN=null,ZN=null;function QN(e){return e.colorSpace=hp,e.flipY=!1,e}function $N(e){let t=JN.get(e);if(t!==void 0)return Promise.resolve(t);let n=YN.get(e);if(n!==void 0)return n;let r=new vy().loadAsync(_A[e]).then(t=>(JN.set(e,QN(t)),t));return YN.set(e,r),r}function eP(){return Promise.all(Object.keys(_A).map($N))}async function tP(){let e=Uint8Array.from(atob(hA),e=>e.charCodeAt(0)),t=new Response(e).body;if(t===null)throw Error(`fighter_glb_stream_unavailable`);return new Response(t.pipeThrough(new DecompressionStream(`gzip`))).arrayBuffer()}function nP(){if(XN===null){let e=tP().then(e=>new Promise((t,n)=>{new hk().parse(e,``,t,e=>n(e instanceof Error?e:Error(String(e))))}));XN=Promise.all([e,eP()]).then(([e])=>(ZN=e.scene,e))}return XN}function rP(){iP(ZN,JN.values())}function iP(e,t){e?.traverse(e=>{e instanceof Ig&&e.geometry.dispose()});for(let e of t)e.dispose()}function aP(e){e.traverse(e=>{e instanceof Jg&&e.skeleton.dispose()})}function oP(e){let t=JN.get(e);if(t!==void 0)return t;let n=QN(new vy().load(_A[e]));return JN.set(e,n),n}var sP=`{ + vec4 pressedOntoCanvas = modelMatrix * vec4( transformed, 1.0 ); + if ( pressedOntoCanvas.y < ${0 .toFixed(4)} && max( abs( pressedOntoCanvas.x ), abs( pressedOntoCanvas.z ) ) < ${Sw.toFixed(4)} ) { + transformed += inverse( mat3( modelMatrix ) ) * vec3( 0.0, ${0 .toFixed(4)} - pressedOntoCanvas.y, 0.0 ); + } +}`;function cP(e){let t=e.onBeforeCompile,n=Object.hasOwn(e,`customProgramCacheKey`)?e.customProgramCacheKey():`plain`;e.onBeforeCompile=(n,r)=>{t.call(e,n,r),n.vertexShader=n.vertexShader.replace(`#include `,`${sP}\n#include `)},e.customProgramCacheKey=()=>`${n}-on-canvas`,e.needsUpdate=!0}function lP(e,t){let n=t.outfit!==void 0,r=[],i=[],a=new Map,o=null,s={value:0},c=null,l=null;if(e.traverse(e=>{if(!(e instanceof Jg)||Array.isArray(e.material))return;let u=e.material.name,d=qN[u];if(d===void 0)throw Error(`fighter GLB has unsupported material ${u}`);let f=a.get(u);if(f===void 0){let e=u===`MHeadMat0`||u===`MBodyMat0`,p=t.outfit,m=u===`GlovesMat0`?t.gear:u===`PantsMat0`?p?.trousers??t.gear:u===`ShoesMat0`&&p!==void 0?p.shoes:16777215,h=oP(d);f=e&&!n?new Fv({map:h,color:m,roughness:.58,metalness:.02,clearcoat:.25,clearcoatRoughness:.6}):new Pv({map:h,color:m,roughness:e?.58:.4,metalness:e?.02:.03}),f.name=u,a.set(u,f),i.push(f),e&&r.push(f),u===`MHeadMat0`&&(c=new oD(n?null:f,UE,{core:ZE,lids:$E})),u===`MBodyMat0`&&(l=new oD(n?null:f,WE,{core:QE,wash:!0}));let g=uP[u];p!==void 0&&g!==void 0?RA(f,g,p):u===`PantsMat0`?s=mE(f,!1,`trunks`):u===`GlovesMat0`&&(o=mE(f)),cP(f)}e.material=f,e.castShadow=!n,e.receiveShadow=!0,e.frustumCulled=!1}),r.length!==2||o===null||i.length!==5||c===null||l===null)throw Error(`fighter GLB material contract failed: ${r.length} skin, ${i.length} total`);return{skin:r,gloveBlood:o,trunksBlood:s,owned:i,headInjury:c,bodyInjury:l}}var uP={MHeadMat0:`head`,MBodyMat0:`body`,ShoesMat0:`shoes`,PantsMat0:`pants`},dP=new Y().setRGB(.6,.32,.19,gp),fP=(e,t,n,r)=>e+(t-e)*(1-Math.exp(-n*r)),pP=(e,t,n,r)=>e+(((t-e+Math.PI)%(Math.PI*2)+Math.PI*2)%(Math.PI*2)-Math.PI)*(1-Math.exp(-n*r)),mP=K.clamp,hP=class{root=new oh;bones=new Map;rig;metrics;skinMaterials;ownedMaterials;gloveBlood;trunksBlood;headMeshes=[];handMeshes={left:[],right:[]};decapitated=!1;burst=!1;dismemberedHands={left:!1,right:!1};dressed;ownedGeometries=[];gearBaseColor;skinBaseColor;headInjury;bodyInjury;headLook;bareHand;constructor(e,t){let n=pk(e.scene);n.scale.setScalar(1),this.root.add(n);let r=lP(n,t);this.skinMaterials=r.skin,this.ownedMaterials=[...r.owned],this.gloveBlood=r.gloveBlood,this.trunksBlood=r.trunksBlood,this.headInjury=r.headInjury,this.bodyInjury=r.bodyInjury,this.gearBaseColor=new Y(t.gear),this.skinBaseColor=new Y(t.skin),this.dressed=t.outfit!==void 0,n.traverse(e=>{e instanceof Jg&&e.name===`BoxerHead`&&this.headMeshes.push(e),e instanceof Jg&&e.name===`BoxerGloveLeft`&&this.handMeshes.left.push(e),e instanceof Jg&&e.name===`BoxerGloveRight`&&this.handMeshes.right.push(e),e instanceof Yg&&this.bones.set(e.name,e)});let i=Object.values(gA).filter(e=>!this.bones.has(e));if(i.length>0)throw Error(`fighter GLB missing required bones: ${i.join(`, `)}`);if(this.headMeshes.length!==1)throw Error(`fighter GLB requires one BoxerHead mesh, found ${this.headMeshes.length}`);let a=this.headInjury.shadowMaterial();this.headMeshes[0].customDepthMaterial=a;let o=this.headMeshes[0].material;o.side=0,o.shadowSide=1,this.ownedMaterials.push(a);for(let e of[`left`,`right`])if(this.handMeshes[e].length!==1)throw Error(`fighter GLB requires one ${e} glove mesh, found ${this.handMeshes[e].length}`);this.root.updateMatrixWorld(!0),this.rig=new lj(this.root),this.headLook=new MD(this.headMeshes[0].material),this.bareHand=t.outfit===void 0?null:this.addHands(t.outfit),this.setLook(t.look??gD);let s=this.rig.metrics;this.metrics={armUpper:s.armL.upper,armFore:s.armL.lower,legThigh:s.legL.upper,legShin:s.legL.lower,headRestY:s.headHeight,chestRestY:s.chestHeight,ankleRestY:s.ankleHeight}}bone(e){return this.bones.get(gA[e]??e)??null}setGloveBlood(e){this.gloveBlood.value=mP(e,0,1)}get gloveBloodLevel(){return this.gloveBlood.value}setTrunksBlood(e){this.trunksBlood.value=mP(e,0,1)}get trunksBloodLevel(){return this.trunksBlood.value}get headMesh(){return this.headMeshes[0]}gloveMesh(e){return this.handMeshes[e][0]}get skin(){return this.skinMaterials[0]}get isDecapitated(){return this.decapitated}setDecapitated(e){this.decapitated=e,this.burst=!1,this.headInjury.setSevered(e),this.bone(`head`).visible=!e}get isHeadBurst(){return this.burst}setHeadBurst(e){this.burst=e,this.decapitated=!1,this.headInjury.setBurst(e),this.bone(`head`).visible=!e}isHandDismembered(e){return this.dismemberedHands[e]}setHandDismembered(e,t){this.dismemberedHands[e]=t;for(let n of this.handMeshes[e])n.visible=!t&&!this.dressed}setLook(e){this.headLook.set(e);for(let t of this.skinMaterials)t.color.setHex(e.tint);this.bareHand?.color.copy(dP).multiply(this.skinMaterials[0].color)}get look(){return this.headLook.look}addHands(e){let t=e.gloves!==null,n=new Pv({color:t?e.gloves:dP,roughness:t?.38:.6,metalness:.02}),r=t?n:new Pv({color:new Y(e.shirt).multiplyScalar(.9),roughness:.88,metalness:0});this.ownedMaterials.push(n),r!==n&&this.ownedMaterials.push(r);let i=HA();this.ownedGeometries.push(i);for(let e of[`left`,`right`]){for(let t of this.handMeshes[e])t.visible=!1;let t=VA(e);this.ownedGeometries.push(t);let a=e===`left`?`L`:`R`;for(let[o,s,c,l]of[[this.rig.bones[`glove${a}`],t,n,`hand-${e}`],[this.rig.bones[`elbow${a}`],i,r,`cuff-${e}`]]){let e=new Ig(s,c);e.name=l,e.castShadow=!1,e.receiveShadow=!0,o.add(e)}}return t?null:n}setSkinClearcoat(e){for(let t of this.skinMaterials)t instanceof Fv&&(t.clearcoat=e)}dispose(){for(let e of this.ownedMaterials)e.dispose();for(let e of this.ownedGeometries)e.dispose();aP(this.root)}},gP=(e,t,n,r,i)=>{let a=e.value,o=e.velocity;o.addScaledVector(a,-n*t).multiplyScalar(Math.exp(-r*t)),a.addScaledVector(o,t),a.length()>i&&a.setLength(i)},_P=.06,vP=150,yP=.58,bP=.25,xP=.32,SP=9,CP=1,wP=.08,TP=.3,EP=1,DP=1.6,OP=.25,kP=.12,AP=.3,jP=.3,MP=8;function NP(e,t,n,r){BN(e,t,n);let i=Math.hypot(n.x,n.z);if(i<=0)return r.x=0,r.z=0,0;let a=pj(0,OP,i),o=1+kP*a/i;return r.x=n.x*o,r.z=n.z*o,a}var PP=(e,t)=>e===0||Math.sign(t)===Math.sign(e)&&Math.abs(t)>Math.abs(e)?t:e,FP=.75,IP=.38,LP=.72,RP=.5,zP=.2,BP=.55,VP=.7,HP=6,UP=.4,WP=.2,GP=.3,KP=.5,qP=class{mapping;boxer;solver;yaw=0;yawInitialized=!1;rootX=null;rootZ=0;punchActive=!1;punchAgeTicks=0;punchTotalTicks=1;punchClass=`jab`;punchHand=`left`;punchTarget=`head`;punchPower=`normal`;punchTiming=A(`jab`,`head`,`normal`);actionId=null;completedActionId=null;ownActionId=null;ownLeadTicks=0;ownAuthoritativeAge=null;ownWaitedTicks=0;ownExpectedTicks=0;ownPulled=!1;ownSequence=null;ownStarted=!1;ownPressedAt=0;ownRefused=!1;ownQueued=null;contactId=null;contactParried=!1;shownFighter=null;shownOpponent=null;retiredOwnIds=[];anticipatedId=null;hitstop=0;hitstopScale=1;downState=`up`;fallAge=0;riseProgress=0;riseFrom=0;fallSide=0;fallProne=!1;fallKneel=!1;windedSide=-1;windedTime=0;winded=0;landProne=!1;landSide=0;landShift=new J;landSettled=!1;fallen=wF();fallenFromPhysics=!1;riseRoot=new J;riseRootFrom=new J;downPoles=[new J,new J,new J,new J];downPolesSet=!1;poleSettle=0;characterInverse=new Mm;place=new J;riseRootVelocity=new J;liveOpponentHead=new J;hasLiveHead=!1;guardOpponentHead=new J;opponentHeadOffset=new J;opponentSteadySeconds=0;followHead=0;aimActionId=null;headKick={value:new J,velocity:new J};torsoKick={value:new J,velocity:new J};rootKick={value:new J,velocity:new J};coverKick={value:new J,velocity:new J};foldDelay=0;pendingFold=0;guardKick=0;guardHigh=0;guardLow=0;guardSpent=0;slip=0;weave=0;weaveTime=0;pull=0;crouchExtra=0;bouncePhase=0;weightPhase=0;stunPhase=0;feintTimer=2;feint=0;stunAmount=0;tired=0;clinchWeight=0;foulWeight=0;tauntWeight=0;crowding=0;punchCommit=0;headSpace=new q;ownHead=new J;ropeHold=new J;ropeExcess={x:0,z:0};ropeOwn={x:0,z:0};ropeTheirs={x:0,z:0};ropeLeanTarget=0;ropeLean=0;ropeOutward=new J(1,0,0);ropeLocal=new J;tauntEnd=null;lastSpeed=0;stool;stoolRoot=new J;stoolYaw=0;enswell;bottle;treatProp=`enswell`;treatTarget=new J;treatScratch=new J;treatFacing=new J(0,0,-1);treatFacingLocal=new J;treatSide=1;treating=!1;treatWeight=0;resting=!1;seated=0;stillTime=0;celebrateTime=0;celebration=0;waveTime=0;wave=0;breakTime=0;breakWeight=0;breakFighters=[new J,new J];breakAimed=!1;attending=!1;verdictSide=0;verdict=null;squareWeight=0;raisedWeight=0;bowedWeight=0;wrists=[null,null];wristTargets=[new J,new J];wristWeights=[0,0];attendWeight=0;countdownTicks=null;countdownEnd=null;touchWeight=0;touchOffset=new J;touchVelocity=new J;debugHoldImpact=!1;feet=[{planted:new J,from:new J,to:new J,progress:1,stepping:!1,duration:.2},{planted:new J,from:new J,to:new J,progress:1,stepping:!1,duration:.2}];feetInitialized=!1;dislocation=null;scratch=new J;scratchB=new J;scratchC=new J;scratchD=new J;scratchE=new J;clinchCentre=new J;clinchForward=new J;clinchTemp=new J;scratchQ=new cm;headWorld=new J;hand={L:this.makeHand(),R:this.makeHand()};punchGuard=this.makeHand();punchDir=new J;punchContact=new J;punchStart=new J;punchSweepFrom=new J;punchSweepTo=new J;punchMid=new J;foot={L:this.makeFoot(),R:this.makeFoot()};torso={hips:new J,hipsYaw:0,hipsPitch:0,hipsRoll:0,shouldersYaw:0,spinePitch:0,spineRoll:0,headYaw:0,headPitch:0,headRoll:0,headOffset:new J};pose;downStanding=wF();downFrom=wF();downTo=wF();downResult=wF();downStand=1;fallStart=wF();fallStartStand=1;fallFromGetUp=!1;referee;refereeCount=0;refereeCounting=!1;countChangedTick=null;ragdoll;forcedDown=!1;heldDown=!1;authoredNextFall=!1;obstacle=null;blows=0;riseBlendAge=0;constructor(e,t,n={}){this.mapping=t,this.boxer=e,this.referee=n.referee===!0,this.solver=new Dj(e.rig),this.pose={torso:this.torso,handL:this.hand.L,handR:this.hand.R,footL:this.foot.L,footR:this.foot.R,shrugL:0,shrugR:0},this.stool=ZF(),this.stool.group.position.set(0,0,.02),e.root.add(this.stool.group),this.enswell=YF(),e.rig.bones.gloveL.add(this.enswell.group),this.bottle=XF(),e.rig.bones.gloveL.add(this.bottle.group),this.ragdoll=this.referee?null:new wN(e.rig,e.root)}primeReplayFall(){this.ragdoll?.prime()}useAuthoredFall(){this.authoredNextFall=!0}knockOut(){this.forcedDown=!0}stayDown(e){this.heldDown=e}setObstacle(e,t){this.obstacle??={x:e,z:t},this.obstacle.x=e,this.obstacle.z=t}get fallBody(){return this.ragdoll?.active===!0?this.ragdoll:null}setResting(e){this.resting=e}get stoolVisible(){return this.stool.group.visible}celebrate(e=4.4){this.celebrateTime=e}resetTransient(e=!1){this.forcedDown=!1;let t=this.ragdoll;t!==null&&(e?t.active?t.settle():t.restoreSettled():(t.stop(),t.clearRise())),this.downState=e?`down`:`up`,this.landSettled=!1,this.fallenFromPhysics=!1,this.riseRoot.set(0,0,0),this.riseRootFrom.set(0,0,0),this.riseRootVelocity.set(0,0,0),this.fallAge=e?FP:0,this.riseProgress=0,this.hitstop=0,this.hitstopScale=1;for(let e of[this.headKick,this.torsoKick,this.rootKick,this.coverKick])e.value.set(0,0,0),e.velocity.set(0,0,0);this.foldDelay=0,this.pendingFold=0,this.guardKick=0,this.stunAmount=0,this.windedTime=0,this.winded=0,this.celebrateTime=0,this.celebration=0,this.waveTime=0,this.wave=0,this.verdictSide=0,this.verdict=null,this.squareWeight=0,this.raisedWeight=0,this.bowedWeight=0,this.wrists.fill(null),this.wristWeights.fill(0),this.seated=0,this.stillTime=0,this.rootX=null,this.yawInitialized=!1,this.feetInitialized=!1,this.retirePunch(),this.ownQueued=null,this.completedActionId=null,this.retiredOwnIds.length=0,this.anticipatedId=null}waveOff(e=2.6){this.waveTime=e}setCountdown(e){this.countdownTicks=e}attend(e){this.attending=e}treat(e,t,n=1,r=`enswell`){this.treating=e!==null,e!==null&&(this.treatTarget.copy(e),t!==void 0&&this.treatFacing.copy(t),this.treatSide=n,this.treatProp=r)}awaitVerdict(e){this.verdictSide=e??0,e===null&&(this.verdict=null)}announce(e){this.verdict=e}raise(e,t){this.wrists[0]=e===null?null:this.wristTargets[0].copy(e),this.wrists[1]=t===null?null:this.wristTargets[1].copy(t)}breakClinch(e=1.3){this.breakTime=e}get breaking(){return this.breakTime>0}aimBreak(e,t){this.breakFighters[0].copy(e),this.breakFighters[1].copy(t),this.breakAimed=!0}makeHand(){return{position:new J,knuckles:new J(0,1,0),palm:new J(0,0,1),pole:new J(0,-1,0)}}makeFoot(){return{position:new J,toe:new J(0,0,1),heel:0,pole:new J(0,0,1)}}get currentRoot(){return{x:this.rootX??0,z:this.rootZ}}get isDown(){return this.downState!==`up`}setRefereeCount(e,t){(e!==this.refereeCounting||t!==this.refereeCount)&&(this.countChangedTick=null),this.refereeCounting=e,this.refereeCount=t}predict(e,t,n,r=0,i){if(e.kind!==`punch`||e.id===void 0)return;let a={action:{...e,id:e.id},timing:i??A(e.class,e.target,e.power),leadTicks:r,pressedAt:t,sequence:null,started:!1,waited:0};if(this.punchActive&&this.ownActionId!==null&&this.ownAuthoritativeAge===null&&!this.ownPulled&&!this.ownStarted&&(t-this.ownPressedAt)*n0||e.clinch_ticks>0||e.clinch_startup_ticks>0,a=this.ownQueued;if(a!==null&&(a.sequence??=r?.(a.action.id)??null,e.action_id===a.action.id?a.started=!0:!a.started&&(i||a.sequence!==null&&e.last_input_sequence>=a.sequence&&e.queued_actions===0)&&(this.ownQueued=null)),!this.punchActive||this.ownActionId===null||this.ownPulled||this.ownRefused)return;this.ownStarted||(this.ownSequence??=r?.(this.ownActionId)??null);let o=e.action_id===this.ownActionId;o&&(this.ownStarted=!0);let s=this.ownQueued;if(s!==null&&s.sequence!==null&&r!==null&&!this.ownStarted&&this.ownSequence===null&&this.ownAuthoritativeAge===null){this.ownActionId=null,this.ownQueued=null,this.retirePunch(),this.startOwnPunch(s,Math.max(0,s.leadTicks-s.waited));return}(o?i&&e.action_contact_tick===null:!this.ownStarted&&this.ownAuthoritativeAge===null&&(i||this.ownSequence!==null&&e.last_input_sequence>=this.ownSequence&&e.queued_actions===0))&&(this.ownRefused=!0,this.punchAgeTickse.action_startup_ticks*(1/nI-1)||this.punchActive&&this.punchAgeTicks/this.punchTotalTicks<=.55)return;this.anticipatedId=n;let r=e.action_target??`head`,i=e.action_power??`normal`,a={...A(e.action,r,i),startup:e.action_startup_ticks,active:e.action_active_ticks,recovery:e.action_recovery_ticks},o=e.action_hand??(e.stance===`orthodox`?`left`:`right`);this.startEarly(n,e.action,o,r,i,a,t,t)}startOwnPunch(e,t){let{action:n,timing:r}=e;this.startEarly(n.id,n.class,n.hand,n.target,n.power,r,t,Math.max(0,t)),this.ownSequence=e.sequence,this.ownStarted=e.started,this.ownPressedAt=e.pressedAt}startEarly(e,t,n,r,i,a,o,s){this.retirePunch(),this.punchClass=t,this.punchHand=n,this.punchTarget=r,this.punchPower=i,this.punchTiming=a,this.punchTotalTicks=Math.max(1,se(a)),this.punchAgeTicks=0,this.punchActive=!0,this.ownActionId=e,this.ownLeadTicks=mP(o,0,$F),this.ownExpectedTicks=s}followUpStartAge(e){let t=this.shownFighter,n=this.shownOpponent,r=this.actionId??this.ownActionId,i=r!==null&&r===this.contactId&&!this.contactParried;return t!==null&&n!==null&&y(this.punchClass,i,e.action,t.stamina,n.stunned_ticks>0,t.style)?v(this.punchTiming):this.punchTotalTicks+1}releaseQueuedPress(e,t){let n=this.ownQueued;n!==null&&(n.waited+=e*30,!(this.punchActive&&!this.ownPulled&&this.punchAgeTicks6&&this.retiredOwnIds.shift()),this.actionId=null,this.punchActive=!1,this.ownActionId=null,this.ownLeadTicks=0,this.ownAuthoritativeAge=null,this.ownWaitedTicks=0,this.ownExpectedTicks=0,this.ownPulled=!1,this.ownSequence=null,this.ownStarted=!1,this.ownRefused=!1}landedHit(e){this.hitstop=Math.max(this.hitstop,e?.045:.078)}setArcadeDislocation(e){this.dislocation=e,this.boxer.headInjury.setJaw(+(e===`jaw`))}react(e,t=`head`,n=1,r=null,i=null,a=90){let o=mP(a/vP,0,1),s=(.55+o*1.05)*(e===`block`?.35:1),c=i===`left`?1:i===`right`||n>=0?-1:1;if(t===`body`){switch(r){case`hook`:this.torsoKick.velocity.x+=3*s,this.torsoKick.velocity.z+=4.6*s*c,this.rootKick.velocity.x+=.5*s*c,this.coverKick.velocity.x+=2.2*s*c;break;case`uppercut`:this.rootKick.velocity.y+=.9*s,this.torsoKick.velocity.x-=1.6*s,this.foldDelay=.1,this.pendingFold=5.2*s;break;default:this.torsoKick.velocity.x+=4.6*s;break}this.rootKick.velocity.z-=.45*s,this.headKick.velocity.z-=.6*s,this.headKick.velocity.y-=.7*s,this.guardKick=Math.max(this.guardKick,.6*s)}else{switch(e===`hit`&&(this.guardKick=Math.max(this.guardKick,.4*s)),r){case`hook`:this.headKick.velocity.x+=2.6*s*c,this.headKick.velocity.z-=.7*s,this.torsoKick.velocity.z+=.9*s*c;break;case`uppercut`:this.headKick.velocity.y+=2.2*s,this.headKick.velocity.z-=1.2*s,this.torsoKick.velocity.x-=1.2*s;break;case`straight`:this.headKick.velocity.z-=2.4*s,this.torsoKick.velocity.x-=.6*s;break;default:this.headKick.velocity.z-=1.5*s,this.torsoKick.velocity.x-=.35*s;break}this.rootKick.velocity.z-=.45*s,e===`block`&&(this.guardKick=Math.max(this.guardKick,.9*s))}if(e===`hit`&&(this.dentSurface(t,c,r,o),this.fallSide=r===`hook`?c:c*.5,this.fallProne=r===`hook`||t===`body`),e===`hit`&&a>=10&&this.ragdoll!==null){let e=CN({target:t,punchClass:r,hand:i,lateral:c,amount:a}),n=this.scratchE.set(e.x,e.y,e.z).applyAxisAngle(QF,this.yaw),o=this.scratchD.set(e.driveX,e.driveY,e.driveZ).applyAxisAngle(QF,this.yaw);this.blows+=1,this.ragdoll.takeBlow(n,o,t,e.twist,iN(r,t,a,this.blows+Math.round(a)))}}stagger(){this.torsoKick.velocity.x-=2.6,this.headKick.velocity.z-=1.4,this.headKick.velocity.y+=.5,this.rootKick.velocity.z-=1.1,this.guardKick=Math.max(this.guardKick,1.1)}windedFor(e,t){this.windedTime=Math.max(this.windedTime,e),this.windedSide=t>=0?1:-1}fallToKnee(e){this.fallKneel=e,e&&(this.authoredNextFall=!0)}dentSurface(e,t,n,r){let i=1.1+r*2.1;if(e===`body`){let e=n===`hook`?t>0?`rightRibs`:`leftRibs`:`solarPlexus`;this.boxer.bodyInjury.impact(e,[n===`hook`?t*i*.8:0,0,-i],9);return}n===`hook`?this.boxer.headInjury.impact(t>0?`rightCheek`:`leftCheek`,[t*i,0,-i*.35],5.5):n===`uppercut`?this.boxer.headInjury.impact(`chin`,[0,i*.7,-i*.6],5):this.boxer.headInjury.impact(n===`jab`?`nose`:`mouth`,[0,0,-i],4.5)}stepFeet(e,t,n,r,i,a){let o=this.scratch,s=this.scratchQ.setFromAxisAngle(QF,a);for(let a=0;a<2;a+=1){let c=this.feet[a],l=a===0==t>0?Oj.leadFoot:Oj.rearFoot;if(this.referee||this.squareWeight>.5?o.set(a===0?.16:-.16,0,a===0?.02:-.02).applyQuaternion(s).add(i):o.set(l.x*t,0,l.z).applyQuaternion(s).add(i),o.y=0,!this.feetInitialized){c.planted.copy(o),c.progress=1,c.stepping=!1;continue}if(c.stepping){c.progress=Math.min(1,c.progress+e/c.duration),c.progress>=1&&(c.stepping=!1,c.planted.copy(c.to));continue}let u=this.feet[1-a];c.planted.distanceTo(o)>(n>.05?.09:.14)&&!u.stepping&&(c.stepping=!0,c.progress=0,c.duration=mP(.24-n*.07,.15,.24),c.from.copy(c.planted),c.to.copy(o).addScaledVector(r,c.duration*.6),c.to.y=0)}this.feetInitialized=!0}footWorld(e,t){let n=this.feet[e];if(!n.stepping)return t.copy(n.planted),0;let r=hj(n.progress,1.6);return t.copy(n.from).lerp(n.to,r),Math.sin(n.progress*Math.PI)*.055}update(e,t,n,r,i,a,o,s){let c=this.boxer,l=e.stance===`orthodox`?1:-1,u=i?.35:1,d=this.touchWalk(e,t,n,o);this.walkBackFromTheFall(n);let p=this.holdOnTheRopes(t,this.mapping.x(e.x)+d.x+this.riseRoot.x,this.mapping.z(e.y)+d.z+this.riseRoot.z),m=this.mapping.x(e.x)+d.x+this.riseRoot.x-p.x,h=this.mapping.z(e.y)+d.z+this.riseRoot.z-p.z;this.place.set(m,0,h),this.rootX===null&&(this.rootX=m,this.rootZ=h);let g=uI*Math.max(n,1/60);this.rootX+=mP(m-this.rootX,-g,g),this.rootZ+=mP(h-this.rootZ,-g,g);let _=Math.atan2(e.facing_x,-e.facing_y);this.yawInitialized||=(this.yaw=_,!0);let v=this.punchActive?3:this.downState===`up`?9:4;this.yaw=pP(this.yaw,_,v,n),c.root.position.set(this.rootX,0,this.rootZ),c.root.rotation.set(0,this.yaw,0),this.hitstop=Math.max(0,this.hitstop-n),this.hitstopScale=this.hitstop>0?.12:1;let y=n*this.hitstopScale;this.syncAction(e,t,o,y),this.debugHoldImpact||(this.boxer.headInjury.update(y),this.boxer.bodyInjury.update(y)),s!==void 0&&this.trackOpponentHead(t,s,n);let b=this.scratchB.set(this.mapping.x(e.velocity_x)*30,0,this.mapping.z(e.velocity_y)*30).add(this.touchVelocity).add(this.riseRootVelocity),x=b.length(),S=Math.hypot(b.x-this.riseRootVelocity.x,b.z-this.riseRootVelocity.z);this.lastSpeed=x,this.stillTime=x<.03?this.stillTime+n:0;let C=this.resting&&this.stillTime>.2&&this.downState===`up`;this.seated=fP(this.seated,+!!C,C?2.2:4,n);let w=this.resting&&this.stillTime>.05&&this.downState===`up`;w&&(this.stoolRoot.set(this.rootX,0,this.rootZ),this.stoolYaw=this.yaw),this.stool.group.visible=w||this.seated>YP&&this.downState===`up`,this.stool.group.position.set(this.stoolRoot.x-this.rootX,0,this.stoolRoot.z-this.rootZ).add(this.scratch.set(0,0,.02).applyAxisAngle(QF,this.stoolYaw)).applyAxisAngle(QF,-this.yaw),this.stool.group.rotation.y=this.stoolYaw-this.yaw,this.celebrateTime=Math.max(0,this.celebrateTime-n),this.celebration=fP(this.celebration,+(this.celebrateTime>0&&this.downState===`up`),3.5,n),this.waveTime=Math.max(0,this.waveTime-n),this.wave=fP(this.wave,+(this.waveTime>0&&this.downState===`up`),4,n),this.breakTime=Math.max(0,this.breakTime-n),this.breakWeight=fP(this.breakWeight,+(this.breakTime>0&&this.downState===`up`),6,n),this.attendWeight=fP(this.attendWeight,+!!this.attending,2.5,n),this.squareWeight=fP(this.squareWeight,this.verdictSide===0?0:1,3,n),this.raisedWeight=fP(this.raisedWeight,+(this.verdictSide!==0&&(this.verdict===`winner`||this.verdict===`level`)),3.2,n),this.bowedWeight=fP(this.bowedWeight,+(this.verdictSide!==0&&this.verdict===`loser`),2.2,n);for(let e of[0,1])this.wristWeights[e]=fP(this.wristWeights[e],this.wrists[e]===null?0:1,3.2,n);this.treatWeight=fP(this.treatWeight,+!!this.treating,3,n),this.windedTime=Math.max(0,this.windedTime-n);let T=this.fallKneel&&this.downState===`falling`;this.winded=fP(this.winded,this.windedTime>0&&this.downState===`up`||T?1:0,this.windedTime>0||T?12:4,n),this.enswell.group.visible=this.treatWeight>.4&&this.treatProp===`enswell`,this.bottle.group.visible=this.treatWeight>.4&&this.treatProp===`bottle`;let E=this.countdownTicks!==null&&this.countdownTicks<=eF&&this.countdownTicks>=tF;this.touchWeight=fP(this.touchWeight,+!!E,6,n);let D=e.stamina/Math.max(1,e.maximum_stamina);this.tired=fP(this.tired,mP((.55-D)/.5,0,1),2,n),this.stunAmount=fP(this.stunAmount,Math.min(1,e.stunned_ticks/24),8,n);let O=e.defense;this.guardHigh=fP(this.guardHigh,+(O===`guard_high`),14,n),this.guardLow=fP(this.guardLow,+(O===`guard_low`),14,n),this.guardSpent=fP(this.guardSpent,+(e.guard0||e.clinch_startup_ticks>0),10,n),this.foulWeight=fP(this.foulWeight,+!!e.is_foul_recovery_target,8,n),this.tauntWeight=fP(this.tauntWeight,+(e.taunt_ticks>0),10,n);let k=Math.hypot(this.mapping.x(t.x)-this.mapping.x(e.x),this.mapping.z(t.y)-this.mapping.z(e.y)),ne=this.referee||e.is_downed||t.is_downed?0:1-pj(yP,CP,k);this.crowding=fP(this.crowding,ne,9,n);let A=1/0;if(this.hasLiveHead&&!this.referee&&this.downState===`up`){let e=this.boxer.rig.bones.head.getWorldPosition(this.ownHead),t=e.x-this.liveOpponentHead.x,r=e.z-this.liveOpponentHead.z,i=Math.hypot(t,r);if(A=Math.hypot(t,e.y-this.liveOpponentHead.y,r),A1e-4){let e=(bP-A)/bP*SP*n,a=Math.cos(-this.yaw),o=Math.sin(-this.yaw);this.headSpace.x+=(t*a+r*o)/i*e,this.headSpace.y+=(-t*o+r*a)/i*e,this.headSpace.lengthSq()>1&&this.headSpace.normalize()}}A>xP&&this.headSpace.multiplyScalar(Math.exp(-3*n)),this.tauntEnd=qF(this.tauntEnd,o,e.taunt_ticks>0?e.taunt_ticks:null),gP(this.headKick,n,190,7.5,.24),gP(this.torsoKick,n,150,7,.7),gP(this.rootKick,n,120,8,.12),gP(this.coverKick,n,110,15,.3),this.foldDelay>0&&(this.foldDelay-=n,this.foldDelay<=0&&(this.torsoKick.velocity.x+=this.pendingFold,this.pendingFold=0)),this.guardKick=Math.max(0,this.guardKick-n*2.4),this.ragdoll?.tick(n),this.updateDownState(e,n,i,S);let re=(1.9-this.tired*.7)*(1-this.stunAmount*.6);this.bouncePhase+=n*re*Math.PI*2*u,this.weightPhase+=n*.55*Math.PI*2*u,this.stunPhase+=n*5.5,this.feintTimer-=n,this.feintTimer<=0&&!this.punchActive&&x<.05&&this.downState===`up`&&(this.feintTimer=2.5+this.bouncePhase%1.7,this.feint=1),this.feint=Math.max(0,this.feint-n*3.5);let ie=this.scratchC.set(this.rootX,0,this.rootZ);(this.downState===`up`||this.downState===`rising`)&&this.stepFeet(n,l,x,b,ie,this.yaw);let ae=this.scratchQ.setFromAxisAngle(QF,-this.yaw),oe=l>0?this.foot.L:this.foot.R,j=l>0?this.foot.R:this.foot.L,se=l>0?0:1,ce=+(l>0),le=this.footWorld(se,oe.position),ue=this.footWorld(ce,j.position);oe.position.sub(ie).applyQuaternion(ae),j.position.sub(ie).applyQuaternion(ae);let M=this.boxer.rig.metrics.ankleHeight;oe.position.y=M+le,j.position.y=M+ue,kj(Oj.leadToe,l,oe.toe),kj(Oj.rearToe,l,j.toe),oe.heel=0,j.heel=0,oe.pole.copy(oe.toe).setY(.35).normalize(),j.pole.copy(j.toe).setY(.35).normalize();let N=this.torso,de=Oj.bladeYaw*l,fe=Math.sin(this.bouncePhase)*.012*(1-this.stunAmount)*(x>.6?.6:1),pe=Math.sin(this.weightPhase)*.02,me=.012+this.tired*.03+this.stunAmount*.05;N.hips.set(pe*l+Math.sin(this.stunPhase*.9)*.05*this.stunAmount,Oj.hipsHeight-me+fe,0),N.hipsYaw=de,N.hipsPitch=.02+this.tired*.08,N.hipsRoll=Math.sin(this.stunPhase)*.09*this.stunAmount,N.shouldersYaw=de*.88,N.spinePitch=.16+this.tired*.16+Math.sin(r*(2.1+this.tired*1.8))*.012,N.spineRoll=Math.sin(this.stunPhase*1.3)*.06*this.stunAmount,N.headYaw=-.12*l+Math.sin(r*.9)*.05+Math.sin(this.stunPhase*.7)*.25*this.stunAmount,N.headPitch=.12+Math.sin(r*1.7)*.02+this.stunAmount*.18,N.headRoll=Math.sin(this.stunPhase*1.1)*.14*this.stunAmount,N.headOffset.set(0,0,0);let P=l>0?this.hand.L:this.hand.R,F=l>0?this.hand.R:this.hand.L,he=this.headRestChar(l),ge=mP(this.guardHigh+this.guardLow,0,1),_e=this.scratch.copy(Oj.relaxedLead).lerp(Oj.guardHighLead,this.guardHigh).lerp(Oj.guardLowLead,this.guardLow),ve=this.scratchB.copy(Oj.relaxedRear).lerp(Oj.guardHighRear,this.guardHigh).lerp(Oj.guardLowRear,this.guardLow);_e.y-=this.tired*.12+this.stunAmount*.22,ve.y-=this.tired*.1+this.stunAmount*.2,_e.y-=this.guardSpent*uF,ve.y-=this.guardSpent*uF,_e.z-=this.guardSpent*.04,ve.z-=this.guardSpent*.03,_e.z+=this.feint*.1+Math.sin(r*2.3)*.012*u,_e.x+=Math.sin(r*1.9)*.01*u,ve.y+=Math.cos(r*2.7)*.01*u,_e.z-=this.guardKick*.07,ve.z-=this.guardKick*.05,_e.y-=this.guardKick*.02,_e.add(he),ve.add(he),P.position.set(_e.x*l,_e.y,_e.z),F.position.set(ve.x*l,ve.y,ve.z),kj(Oj.leadKnuckles,l,P.knuckles),kj(Oj.leadPalm,l,P.palm),kj(Oj.rearKnuckles,l,F.knuckles),kj(Oj.rearPalm,l,F.palm),kj(Oj.leadPole,l,P.pole),kj(Oj.rearPole,l,F.pole);let ye=.15*ge,be=.25*ge;if(Math.abs(this.slip)>.001){let e=this.slip;N.headOffset.x+=e*.13,N.spineRoll+=-e*.28,N.hips.x+=e*.03,N.hips.y-=Math.abs(e)*.05,N.spinePitch+=Math.abs(e)*.12,N.headRoll+=-e*.2}if(this.weave>.001){let e=this.weave,t=mP(this.weaveTime/.42,0,1),n=Math.sin(t*Math.PI);N.hips.y-=e*.2,N.spinePitch+=e*.42,N.headOffset.x+=e*(1-2*t)*.16*l,N.headOffset.y-=e*n*.08,N.spineRoll+=e*(1-2*t)*.18*l,P.position.y-=e*.1,F.position.y-=e*.1}if(this.pull>.001){let e=this.pull;N.spinePitch-=e*.34,N.hips.z-=e*.11,N.hips.x-=e*.05*l,N.headOffset.z-=e*.06,N.headPitch-=e*.1}this.punchCommit=fP(this.punchCommit,+!!this.punchActive,10,n);let xe=this.crowding*(1-this.clinchWeight)*(1-this.punchCommit);if(xe>.001&&(N.headOffset.x-=xe*.075,N.headOffset.z-=xe*.05,N.headYaw-=xe*.2,N.spinePitch-=xe*.07,N.hips.z-=xe*.05,P.position.z-=xe*.14,P.position.x-=xe*.035*l,F.position.z-=xe*.07),Math.abs(this.headSpace.x)+Math.abs(this.headSpace.y)>.001){let e=this.headSpace.x,t=this.headSpace.y,n=1-this.clinchWeight;N.spinePitch+=t*.7*n,N.spineRoll-=e*.7*n,N.hips.z+=t*.1*n,N.hips.x+=e*.05*n,N.headOffset.x+=e*.05,N.headOffset.z+=t*.05,N.headPitch+=t*.2,N.headRoll-=e*.3}if(this.ropeLean=fP(this.ropeLean,this.referee||this.downState!==`up`?0:this.ropeLeanTarget,MP,n),this.ropeLean>.001){let e=this.ropeLocal.copy(this.ropeOutward).applyAxisAngle(QF,-this.yaw);N.hipsPitch+=e.z*AP*this.ropeLean,N.hipsRoll-=e.x*jP*this.ropeLean}N.headOffset.add(this.headKick.value),N.headPitch+=-this.headKick.value.z*2.4+this.headKick.value.y*1.6,N.headYaw+=this.headKick.value.x*2.2,N.headRoll+=-this.headKick.value.x*.9,N.spinePitch+=this.torsoKick.value.x,N.spineRoll+=this.torsoKick.value.z*.5,N.hips.x+=this.rootKick.value.x,N.hips.z+=this.rootKick.value.z,N.hips.y-=Math.max(0,this.torsoKick.value.x)*.08,this.torsoKick.value.x>.05&&(P.position.y-=this.torsoKick.value.x*.25,F.position.y-=this.torsoKick.value.x*.2,P.position.z-=this.torsoKick.value.x*.1);let I=Math.max(0,this.rootKick.value.y);N.hips.y+=I,oe.heel+=I*6,j.heel+=I*6;let Se=this.coverKick.value.x;if(Math.abs(Se)>.002){let e=Se>0?this.hand.R:this.hand.L;e.position.y-=Math.abs(Se)*.9,e.position.x+=Se*.2,e.position.z-=Math.abs(Se)*.25,e.pole.y-=Math.abs(Se)*3}if(this.punchActive){ye=0,be=0;let e=this.applyPunch(l,P,F,oe,j,he);this.punchHand===`left`==l>0?ye=e:be=e}if(this.referee&&this.applyRefereePose(l,P,F,oe,j,he,r,o),this.clinchWeight>.001&&this.applyClinchPose(this.clinchWeight,r,l,e,t,P,F,he),this.foulWeight>.001){let e=this.foulWeight;N.hips.y-=e*.16,N.spinePitch+=e*.55,N.headPitch+=e*.2,P.position.lerp(this.scratch.set(.06*l,.72,.16),e),F.position.lerp(this.scratch.set(-.05*l,.7,.14),e),P.pole.set(.6*l,-.6,.5),F.pole.set(-.6*l,-.6,.5)}if(this.tauntWeight>.001){let e=this.tauntEnd===null?0:mP(this.tauntEnd-o,0,cF);this.applyTauntPose(this.tauntWeight,(cF-e)/cF*lF,l,P,F,he)}if(this.dislocation===`shoulder_left`||this.dislocation===`shoulder_right`){let e=this.dislocation===`shoulder_left`?this.hand.L:this.hand.R,t=this.dislocation===`shoulder_left`?1:-1;e.position.set(.24*t,.55,.05),e.pole.set(.9*t,-.3,.2),e.knuckles.set(0,-1,.1),e.palm.set(-t,0,.2)}this.winded>.001&&(this.downState===`up`||T)&&this.applyWindedPose(this.winded,r,l,P,F),this.touchWeight>.001&&this.downState===`up`&&this.applyTouchGlovesPose(this.touchWeight,l,P,F),this.celebration>.001&&this.downState===`up`&&this.applyCelebratePose(this.celebration,r,l,P,F,oe,j),this.wave>.001&&this.downState===`up`&&this.applyWaveOffPose(this.wave,r,l,P,F),this.squareWeight>.001&&this.downState===`up`&&this.applyVerdictPose(this.squareWeight,r,l,P,F,oe,j,he),(this.wristWeights[0]>.001||this.wristWeights[1]>.001)&&this.downState===`up`&&this.applyRaisePose(l,P,F),this.breakWeight>.001&&this.downState===`up`&&this.applyBreakPose(this.breakWeight,l,P,F),this.attendWeight>.001&&this.downState===`up`&&this.applyAttendPose(this.attendWeight,r,l,P,F),this.treatWeight>.001&&this.downState===`up`&&this.applyTreatPose(this.treatWeight,r,l,P,F,oe,j),this.seated>.001&&this.downState===`up`&&this.applySeatedPose(this.seated,r,l,P,F,oe,j),this.downState!==`up`&&this.applyDownPose(l,P,F,oe,j),this.steadyJointPoles(l,P,F,oe,j,n),me=0;let Ce=this.pose;Ce.shrugL=l>0?ye:be,Ce.shrugR=l>0?be:ye;let we=this.ragdoll;if(we?.active===!0&&i&&(we.stop(),we.clearRise()),we?.active===!0&&!we.handingOver)we.setLost(c.isDecapitated,c.isHandDismembered(`left`),c.isHandDismembered(`right`)),we.update(n,this.obstacle);else{if(this.solver.apply(c.root,this.pose),we?.handingOver===!0)we.followPose(n,this.obstacle),this.riseProgress>=IP&&we.handOverSeconds>=GP&&(we.letGo(),this.riseBlendAge=0);else if(we!==null&&we.hasRisePose){let e=pj(0,WP,this.riseBlendAge);e<1&&this.downState!==`up`?we.blendRise(e):we.clearRise()}we?.sample(n)}this.applyDislocation();let L=Math.min(1,(t.trauma.bleeding+t.trauma.left_cut+t.trauma.right_cut)/620*(a===`off`?0:a===`reduced`?.3:1.5));c.setGloveBlood(L),c.setTrunksBlood(mD(e.trauma,a)),pD(c.headInjury,e.trauma,a),hD(c.bodyInjury,e.trauma,a),c.setSkinClearcoat(.25+(1-D)*.4)}applyTauntPose(e,t,n,r,i,a){let o=this.torso,s=.5-.5*Math.cos(t*Math.PI*2);i.position.lerp(this.scratch.set(-.3*n,a.y-.62,.02),e),i.pole.lerp(this.scratch.set(-.5*n,-.4,-.75),e).normalize(),i.knuckles.lerp(this.scratch.set(-.1*n,-.95,.3),e).normalize(),i.palm.lerp(this.scratch.set(0,.3,.95),e).normalize(),r.position.lerp(this.scratch.set(.26*n,a.y-.36+s*.1,.52-s*.24),e),r.pole.lerp(this.scratch.set(.7*n,-.7,.1),e).normalize(),r.knuckles.lerp(this.scratch.set(.1*n,.25+s*.6,.95-s*.7),e).normalize(),r.palm.lerp(this.scratch.set(-.2*n,.95,-.2-s*.5),e).normalize(),o.spinePitch-=e*.12,o.headPitch-=e*.22,o.headYaw+=Math.sin(t*Math.PI)*.15*e,o.shouldersYaw+=Math.sin(t*Math.PI*3)*.1*e,o.hips.x+=Math.sin(t*Math.PI)*.03*e*n,o.headOffset.z+=e*.03}applyClinchPose(e,t,n,r,i,a,o,s){let c=this.torso,l=r.player_idy&&(h.x*=y/v,h.z*=y/v),h.x+=o,h.z+=u,t.position.lerp(h,e),t.pole.lerp(this.clinchTemp.set(r*.85,l?.35:-.6,.25),e).normalize(),t.knuckles.lerp(this.clinchTemp.set(-p*r*.9+f.x*.3,l?-.25:.1,-m*r*.9+f.z*.3),e).normalize(),t.palm.lerp(this.clinchTemp.set(-f.x,l?.1:.35,-f.z),e).normalize()}}holdOnTheRopes(e,t,n){if(this.referee)return this.ropeLeanTarget=0,this.ropeHold.set(0,0,0);this.ropeLeanTarget=NP(t,n,this.ropeExcess,this.ropeOwn);let r=Math.hypot(this.ropeExcess.x,this.ropeExcess.z);r>0&&this.ropeOutward.set(this.ropeExcess.x/r,0,this.ropeExcess.z/r);let i=this.mapping.x(e.x),a=this.mapping.z(e.y);NP(i,a,this.ropeTheirs,this.ropeTheirs);let o=1-pj(EP,DP,Math.hypot(i-t,a-n));return this.ropeHold.set(PP(this.ropeOwn.x,this.ropeTheirs.x*o),0,PP(this.ropeOwn.z,this.ropeTheirs.z*o))}headRestChar(e){let t=this.boxer.rig.metrics;return this.headWorld.set(-.08,t.headHeight-t.hipsHeight+this.torso.hips.y,.02)}syncAction(e,t,n,r){this.shownFighter=e,this.shownOpponent=t;let i=this.ownQueued;if(i!==null&&e.action_id===i.action.id&&(this.ownQueued=null,this.retirePunch(),this.startOwnPunch(i,0)),e.action_id!==null&&e.action_id!==this.actionId&&e.action_id!==this.completedActionId&&!this.retiredOwnIds.includes(e.action_id)){let t=e.action;if(t!==null){let r=e.action_hand??(e.stance===`orthodox`?`left`:`right`),i=this.ownActionId===e.action_id&&this.punchActive,a=this.punchTiming;this.actionId=e.action_id,this.punchClass=t,this.punchHand=r,this.punchTarget=e.action_target??`head`,this.punchPower=e.action_power??`normal`,this.punchTiming={...A(this.punchClass,this.punchTarget,this.punchPower),startup:e.action_startup_ticks,active:e.action_active_ticks,recovery:e.action_recovery_ticks},this.punchTotalTicks=Math.max(1,e.action_startup_ticks+e.action_active_ticks+e.action_recovery_ticks);let o=Math.max(0,n-e.action_start_tick);i?(this.punchAgeTicks=lI(this.punchAgeTicks,a,this.punchTiming),this.ownAuthoritativeAge=o,this.ownRefused||(this.ownPulled=!1,o-this.punchAgeTicks>1&&(this.punchAgeTicks=o))):(this.punchAgeTicks=o,this.ownActionId=null,this.ownLeadTicks=0,this.ownAuthoritativeAge=null),this.ownWaitedTicks=0,this.punchActive=!0}}if(e.action_id===null)this.actionId=null,this.completedActionId=null;else if(e.action_id===this.actionId&&this.punchActive){let t=Math.max(0,n-e.action_start_tick);this.ownActionId===this.actionId?(this.ownAuthoritativeAge=t,!this.ownPulled&&t-this.punchAgeTicks>1.5&&(this.punchAgeTicks=t)):Math.abs(this.punchAgeTicks-t)>1.5&&(this.punchAgeTicks=t)}this.punchActive&&this.ownActionId!==null&&this.ownAuthoritativeAge===null&&!this.ownPulled&&(this.ownWaitedTicks+=r*30,this.ownWaitedTicks>this.ownExpectedTicks*1.5+eI&&this.punchAgeTicks=this.punchTotalTicks&&(this.retirePunch(),a=!0)),e.is_downed&&(this.punchActive&&this.retirePunch(),this.ownQueued=null),this.releaseQueuedPress(r,a);let o=this.actionId??this.ownActionId;o!==this.aimActionId&&(this.aimActionId=o,this.followHead=0),this.followHead=fP(this.followHead,+(o!==null&&o===this.contactId),sI,r)}trackOpponentHead(e,t,n){let r=this.guardOpponentHead.set(this.mapping.x(e.x),0,this.mapping.z(e.y));this.opponentSteadySeconds=aI.has(e.defense)?0:this.opponentSteadySeconds+n,(!this.hasLiveHead||this.opponentSteadySeconds>=oI)&&this.opponentHeadOffset.copy(t).sub(r),r.add(this.opponentHeadOffset),this.liveOpponentHead.copy(t),this.hasLiveHead=!0}ownPunchRate(){if(this.ownActionId===null)return 1;let e=this.punchTiming.startup,t=this.ownAuthoritativeAge;return t===null?this.punchAgeTicks0,h=m?t:n,g=m?n:t,_=m?1:-1,v=+(this.punchPower===`power`),y=+(this.punchTarget===`body`),b,x,S,C=1,w=1;if(u0){let t=this.scratchQ.setFromAxisAngle(QF,-j*e*T);r.toe.applyQuaternion(t),r.pole.applyQuaternion(t)}if(ae>.3){let t=this.scratchQ.setFromAxisAngle(QF,.45*e*T);i.toe.applyQuaternion(t),i.pole.applyQuaternion(t)}let F=this.scratchE;switch(this.punchClass){case`hook`:F.set(.22*_*e,-.06,-.12);break;case`uppercut`:F.set(.05*_*e,-.42,-.02);break;case`straight`:F.set(.02*_*e,-.03,-.09-.05*v);break;default:F.set(0,-.01,-.05-.04*v);break}let he=this.punchStart.copy(O).addScaledVector(F,Math.max(x,this.punchClass===`hook`||this.punchClass===`uppercut`?(1-E)*.6*C:0));if(this.punchClass===`hook`){let t=Math.hypot(he.x-N.x,he.z-N.z),n=Math.hypot(P.x-N.x,P.z-N.z),r=this.punchSweepFrom.copy(he).sub(N).setY(0).normalize(),i=this.punchSweepTo.copy(P).sub(N).setY(0).normalize(),a=Math.acos(mP(r.dot(i),-1,1)),o=r.z*i.x-r.x*i.z>=0?1:-1,s=pj(0,1,E),c=r.applyAxisAngle(QF,a*s*o).normalize();h.position.copy(N).addScaledVector(c,K.lerp(t,n,s)),h.position.y=K.lerp(he.y,P.y,s),h.pole.set(.95*_*e,.08,.3).normalize(),h.knuckles.copy(c).applyAxisAngle(QF,o*Math.PI/2).setY(.05).normalize(),h.palm.set(0,-1,0)}else if(this.punchClass===`uppercut`){let t=pj(0,1,E),n=he,r=this.punchMid.copy(P).lerp(n,.5);r.y=Math.min(n.y,P.y)-.04,r.z+=.08,h.position.copy(n).lerp(r,t*2>1?1:t*2),t>.5&&h.position.copy(r).lerp(P,(t-.5)*2),h.pole.set(.3*_*e,-.2,1),h.knuckles.set(.05*_*e,.9,.35).normalize(),h.palm.set(-.2*_*e,.3,-.95).normalize()}else{h.position.copy(he).lerp(P,Math.min(1,E)),h.position.y+=Math.sin(mP(E,0,1)*Math.PI)*.025,h.pole.set(.55*_*e,-.9,.35);let t=pj(.55,1,E);h.knuckles.copy(de).lerp(this.scratchC.set(.1*_*e,.55,.8),1-t).normalize(),h.palm.set(-.9*_*e,.2,-.3).lerp(this.scratchC.set(0,-1,.1),t).normalize()}if(D>0&&(h.position.lerp(O,D),h.knuckles.lerp(ee,D).normalize(),h.palm.lerp(te,D).normalize(),h.pole.lerp(k,D).normalize()),w<1){let t=(1-h.pole.dot(k)/Math.max(1e-6,h.pole.length()*k.length()))/2;h.knuckles.lerp(ee,1-w).normalize(),h.palm.lerp(te,1-w).normalize(),h.pole.lerp(k,1-w).addScaledVector(this.scratchC.set(_*e,0,0),cI*t*Math.sin(Math.PI*w)).normalize()}let ge=this.scratchE.set(-.1*_*e,s.y-.04,.12+s.z);return g.position.lerp(ge,T*.7),T}applyRefereePose(e,t,n,r,i,a,o,s){let c=this.torso;c.hipsYaw=0,c.shouldersYaw=0,c.hipsPitch=0,c.spinePitch=.06,c.headYaw=Math.sin(o*.7)*.08,c.headPitch=.08,c.hips.y=Oj.hipsHeight+.03,c.hips.x=0,c.hips.z=0,r.toe.set(.12*e,0,1).normalize(),i.toe.set(-.12*e,0,1).normalize(),r.pole.copy(r.toe).setY(.3).normalize(),i.pole.copy(i.toe).setY(.3).normalize();let l=Math.sin(o*1.3)*.01;if(t.position.set(.27*e,.86+l,.12),n.position.set(-.27*e,.86-l,.12),t.pole.set(.5*e,-.4,-.8).normalize(),n.pole.set(-.5*e,-.4,-.8).normalize(),t.knuckles.set(.15*e,-.9,.35).normalize(),n.knuckles.set(-.15*e,-.9,.35).normalize(),t.palm.set(-.9*e,.1,.35).normalize(),n.palm.set(.9*e,.1,.35).normalize(),this.refereeCounting){this.countChangedTick??=s;let r=mP((s-this.countChangedTick)/XP,0,1),i=r=FP&&(this.downState=`down`);else if(this.fallKneel)this.riseProgress=LP;else{let n=(!this.forcedDown&&!this.heldDown&&e.get_up_required>0?mP(e.get_up_meter/e.get_up_required,0,1):0)*LP;if(this.ragdoll?.active===!0&&!this.ragdoll.handingOver){if(n<=0)return;this.handOverFall(e)}this.riseProgress+=mP(n-this.riseProgress,-.4*t,BP*t)}return}if((this.downState===`down`||this.downState===`falling`)&&(this.downState=`rising`,this.ragdoll?.active===!0&&!this.ragdoll.handingOver&&this.handOverFall(e),this.fallKneel&&(this.riseProgress=Math.max(this.riseProgress,LP)),this.riseFrom=this.riseProgress,this.feetInitialized=!1),this.downState!==`rising`)return;let i=r>.3||this.punchActive,a=(1-this.riseFrom)/(i?zP:RP);this.riseProgress=Math.min(1,this.riseProgress+(this.riseProgress=1&&(this.downState=`up`,this.fallKneel=!1)}startFall(e){let t=this.ragdoll;t!==null&&(this.rootX!==null&&Math.hypot(this.place.x-this.rootX,this.place.z-this.rootZ)>KP&&(this.rootX=this.place.x,this.rootZ=this.place.z,this.boxer.root.position.set(this.rootX,0,this.rootZ)),t.start(`crumple`),e.get_up_count>0&&(t.settle(),this.fallAge=FP))}handOverFall(e){let t=this.ragdoll;if(t===null||!t.active||t.handingOver)return;let n=e.stance===`orthodox`?1:-1,r=this.boxer.rig.bones;this.boxer.root.updateMatrixWorld(!0);let i=this.scratchC.set(this.rootX??0,0,this.rootZ),a=this.scratchQ.setFromAxisAngle(QF,-this.yaw),o=this.fallen,s=(e,t)=>t.setFromMatrixPosition(e.matrixWorld).sub(i).applyQuaternion(a),c=(e,t,n,r)=>FF(bF.setFromQuaternion(e.getWorldQuaternion(yF).premultiply(a),`YXZ`),t,n,r);s(r.hips,o.hips),this.landProne=t.faceDown();let l=Math.abs(o.hips.x)>.12?Math.sign(o.hips.x):0,u=this.landProne?jF(this.downTo,DF,OF,n,l,this.boxer.rig.metrics.ankleHeight):jF(this.downTo,TF,EF,n,l,this.boxer.rig.metrics.ankleHeight);this.landSide=l,this.landShift.set(o.hips.x-u.hips.x,0,o.hips.z-u.hips.z);let d=c(r.hips,u.hipsPitch,u.hipsYaw,u.hipsRoll);o.hipsPitch=d.x,o.hipsYaw=d.y,o.hipsRoll=d.z;let f=c(r.upperChest,o.hipsPitch,o.hipsYaw,o.hipsRoll);o.shouldersYaw=f.y,o.spinePitch=f.x-o.hipsPitch,o.spineRoll=f.z-o.hipsRoll;let p=c(r.head,f.x,f.y,f.z);o.headPitch=p.x,o.turn=p.y,o.headRoll=p.z,s(n>0?r.gloveL:r.gloveR,o.leadHand),s(n>0?r.gloveR:r.gloveL,o.rearHand),s(n>0?r.ankleL:r.ankleR,o.leadFoot),s(n>0?r.ankleR:r.ankleL,o.rearFoot);let m=this.boxer.rig.metrics.ankleHeight;o.leadFoot.y=Math.max(o.leadFoot.y,m),o.rearFoot.y=Math.max(o.rearFoot.y,m);let h=(e,t,n,r,i)=>{let o=s(e,this.scratchB),c=s(n,this.scratchD).sub(o).normalize();s(t,r).sub(o),r.addScaledVector(fF.copy(i).applyQuaternion(e.getWorldQuaternion(yF)).applyQuaternion(a),vF),r.addScaledVector(c,-r.dot(c)),r.lengthSq()>1e-6?r.normalize():r.copy(i===hF?pF:mF)},g=n>0;h(g?r.hipL:r.hipR,g?r.kneeL:r.kneeR,g?r.ankleL:r.ankleR,o.leadKnee,hF),h(g?r.hipR:r.hipL,g?r.kneeR:r.kneeL,g?r.ankleR:r.ankleL,o.rearKnee,hF),h(g?r.shoulderL:r.shoulderR,g?r.elbowL:r.elbowR,g?r.gloveL:r.gloveR,o.leadElbow,g?gF:_F),h(g?r.shoulderR:r.shoulderL,g?r.elbowR:r.elbowL,g?r.gloveR:r.gloveL,o.rearElbow,g?_F:gF);let _=(e,t,n)=>{let r=e.getWorldQuaternion(yF).premultiply(a);t.set(0,1,0).applyQuaternion(r),n.set(0,0,1).applyQuaternion(r)};_(g?r.gloveL:r.gloveR,o.leadKnuckles,o.leadPalm),_(g?r.gloveR:r.gloveL,o.rearKnuckles,o.rearPalm),o.ownJoints=1,o.leadHeel=0,o.rearHeel=0,o.palmsDown=+!!this.landProne,this.fallenFromPhysics=!0,this.landSettled=!0,this.downPolesSet=!1,t.handOver(),this.riseBlendAge=0;let v=this.scratchE.set(t.pelvis(this.scratchD).x-(this.rootX??0),0,this.scratchD.z-this.rootZ),y=this.scratchB.copy(v).applyQuaternion(a);for(let e of[o.hips,o.leadHand,o.rearHand,o.leadFoot,o.rearFoot])e.x-=y.x,e.z-=y.z;t.shiftRise(this.scratchC.copy(v).negate()),this.rootX=(this.rootX??0)+v.x,this.rootZ+=v.z,this.boxer.root.position.set(this.rootX,0,this.rootZ),this.riseRoot.set(this.rootX-this.mapping.x(e.x)-this.touchOffset.x,0,this.rootZ-this.mapping.z(e.y)-this.touchOffset.z),this.riseRootFrom.copy(this.riseRoot)}walkBackFromTheFall(e){let t=this.downState===`rising`?1-pj(LP,1,this.riseProgress):this.downState===`up`?0:1,n=this.riseRootFrom.x*t,r=this.riseRootFrom.z*t;e>0?this.riseRootVelocity.set((n-this.riseRoot.x)/e,0,(r-this.riseRoot.z)/e):this.riseRootVelocity.set(0,0,0),this.riseRoot.set(n,0,r),t===0&&this.riseRootFrom.set(0,0,0)}steadyJointPoles(e,t,n,r,i,a){if(this.poleSettle=this.downState===`up`?Math.max(0,this.poleSettle-a):UP,this.poleSettle<=0||this.ragdoll?.active===!0&&!this.ragdoll.handingOver){this.downPolesSet=!1;return}let o=this.boxer.rig.bones,s=e>0;this.characterInverse.copy(this.boxer.root.matrixWorld).invert();for(let e=0;e<4;e+=1){let c=e<2,l=e%2==0,u=l===s,d=c?u?o.hipL:o.hipR:u?o.shoulderL:o.shoulderR,f=c?l?r:i:l?t:n,p=f.position,m=f.pole,h=this.downPoles[e];if(!this.downPolesSet){h.copy(m);continue}let g=this.scratchB.setFromMatrixPosition(d.matrixWorld).applyMatrix4(this.characterInverse).sub(p).negate();if(g.lengthSq()<1e-6){h.copy(m);continue}g.normalize();let _=this.scratchD.copy(h).addScaledVector(g,-h.dot(g)),v=this.scratchE.copy(m).addScaledVector(g,-m.dot(g));if(_.lengthSq()<1e-4||v.lengthSq()<1e-4){h.copy(m);continue}let y=Math.atan2(this.scratch.crossVectors(_,v).dot(g),_.dot(v)),b=HP*Math.max(a,1/120);if(Math.abs(y)<=b){h.copy(m);continue}m.copy(_.normalize().applyAxisAngle(g,Math.sign(y)*b)),h.copy(m)}this.downPolesSet=!0}applyWindedPose(e,t,n,r,i){let a=this.torso,o=K.lerp,s=this.windedSide,c=Math.sin(t*23)*.006;a.hips.y=o(a.hips.y,Oj.hipsHeight-.1,e),a.hips.z=o(a.hips.z,-.04,e),a.hipsPitch=o(a.hipsPitch,.2,e),a.spinePitch=o(a.spinePitch,.62+c,e),a.spineRoll=o(a.spineRoll,s*.16,e),a.headPitch=o(a.headPitch,.38,e);let l=s>0?this.hand.L:this.hand.R,u=l===r?i:r;l.position.lerp($.set(s*.13,.98,.17),e),l.palm.lerp($.set(-s*.6,0,-.8),e).normalize(),l.knuckles.lerp($.set(-s*.5,.5,.2),e).normalize(),l.pole.lerp($.set(s*.9,-.3,-.3),e).normalize(),u.position.lerp($.set(-s*.2,.86,.3),e),u.pole.lerp($.set(-s*.8,-.6,0),e).normalize()}applyTouchGlovesPose(e,t,n,r){let i=this.torso,a=K.lerp;i.hips.z=a(i.hips.z,.06,e),i.hipsYaw=a(i.hipsYaw,i.hipsYaw*.4,e),i.shouldersYaw=a(i.shouldersYaw,0,e),i.spinePitch=a(i.spinePitch,.12,e),i.headPitch=a(i.headPitch,.1,e),n.position.lerp($.set(.11*t,1.24,.5),e),r.position.lerp($.set(-.11*t,1.22,.47),e),n.palm.lerp($.set(-t,0,0),e).normalize(),r.palm.lerp($.set(t,0,0),e).normalize(),n.knuckles.lerp($.set(0,.15,1),e).normalize(),r.knuckles.lerp($.set(0,.15,1),e).normalize(),n.pole.lerp($.set(.7*t,-.7,0),e).normalize(),r.pole.lerp($.set(-.7*t,-.7,0),e).normalize()}touchWalk(e,t,n,r){this.countdownEnd=qF(this.countdownEnd,r,this.countdownTicks);let i=this.countdownEnd===null?0:JF(this.countdownEnd-r),a=this.mapping.x(t.x)-this.mapping.x(e.x),o=this.mapping.z(t.y)-this.mapping.z(e.y),s=Math.hypot(a,o),c=s>nF?(s-nF)/2/s*i:0;return this.touchVelocity.set(a*c-this.touchOffset.x,0,o*c-this.touchOffset.z).divideScalar(Math.max(n,.001)).clampLength(0,sF),this.touchOffset.set(a*c,0,o*c)}applyCelebratePose(e,t,n,r,i,a,o){let s=this.torso,c=K.lerp,l=Math.sin(t*6)*.04;s.hips.y=c(s.hips.y,Oj.hipsHeight+.035+l*.5,e),s.hipsYaw=c(s.hipsYaw,0,e),s.hipsPitch=c(s.hipsPitch,-.12,e),s.hipsRoll=c(s.hipsRoll,0,e),s.shouldersYaw=c(s.shouldersYaw,0,e),s.spinePitch=c(s.spinePitch,-.1,e),s.headPitch=c(s.headPitch,-.25,e),r.position.lerp($.set(.3*n,1.86+l,.08),e),i.position.lerp($.set(-.3*n,1.84+l,.06),e),r.palm.lerp($.set(0,0,1),e).normalize(),i.palm.lerp($.set(0,0,1),e).normalize(),r.knuckles.lerp($.set(.15*n,1,0),e).normalize(),i.knuckles.lerp($.set(-.15*n,1,0),e).normalize(),r.pole.lerp($.set(n,.1,-.3),e).normalize(),i.pole.lerp($.set(-n,.1,-.3),e).normalize(),a.heel=c(a.heel,.45,e),o.heel=c(o.heel,.45,e)}applyAttendPose(e,t,n,r,i){let a=this.torso,o=K.lerp,s=Math.sin(t*3.1)*.03;a.hips.z=o(a.hips.z,.12,e),a.hips.y=o(a.hips.y,Oj.hipsHeight-.04,e),a.hipsPitch=o(a.hipsPitch,.3,e),a.spinePitch=o(a.spinePitch,.42,e),a.headPitch=o(a.headPitch,.4,e),r.position.lerp($.set(.2*n,1.3+s,.62),e),i.position.lerp($.set(-.18*n,1.24-s,.58),e),r.palm.lerp($.set(0,-.6,.8),e).normalize(),i.palm.lerp($.set(0,-.6,.8),e).normalize(),r.knuckles.lerp($.set(.1*n,.5,1),e).normalize(),i.knuckles.lerp($.set(-.1*n,.5,1),e).normalize(),r.pole.lerp($.set(.8*n,-.5,-.1),e).normalize(),i.pole.lerp($.set(-.8*n,-.5,-.1),e).normalize()}applyTreatPose(e,t,n,r,i,a,o){let s=this.torso,c=K.lerp,l=this.treatScratch.set(this.rootX??0,0,this.rootZ),u=this.scratchQ.setFromAxisAngle(QF,-this.yaw),d=$.copy(this.treatTarget).sub(l).applyQuaternion(u),f=this.treatFacingLocal.copy(this.treatFacing).applyQuaternion(u),p=f.z,m=-f.x,h=Math.sin(t*2.6)*.006;s.hips.lerp(this.treatScratch.set(0,.62,.04),e),s.hipsYaw=c(s.hipsYaw,0,e),s.hipsPitch=c(s.hipsPitch,.22,e),s.hipsRoll=c(s.hipsRoll,0,e),s.shouldersYaw=c(s.shouldersYaw,0,e),s.spinePitch=c(s.spinePitch,.3,e),s.headPitch=c(s.headPitch,.2,e),a.position.x=c(a.position.x,.24*n,e),a.position.z=c(a.position.z,.14,e),o.position.x=c(o.position.x,-.2*n,e),o.position.z=c(o.position.z,-.2,e),o.heel=c(o.heel,.55,e),a.toe.lerp(this.treatScratch.set(.2*n,0,1),e).normalize(),o.toe.lerp(this.treatScratch.set(-.1*n,0,1),e).normalize(),a.pole.lerp(this.treatScratch.set(.25*n,.4,1),e).normalize(),o.pole.lerp(this.treatScratch.set(-.2*n,.4,1),e).normalize();let g=this.treatProp===`bottle`,_=g?.3:.08-h;r.position.lerp(this.treatScratch.set(d.x+f.x*_,d.y+(g?.02:-.07),d.z+f.z*_),e),g?r.knuckles.lerp(this.treatScratch.set(-f.x,-.25,-f.z),e).normalize():r.knuckles.lerp(this.treatScratch.set(0,1,.05),e).normalize(),r.palm.lerp(g?this.treatScratch.set(0,-1,0):this.treatScratch.set(-f.x,0,-f.z),e).normalize(),r.pole.lerp(this.treatScratch.set(.85*n,-.35,.1),e).normalize();let v=this.treatSide*.135;i.position.lerp(this.treatScratch.set(d.x-p*v+f.x*.05,d.y-.2,d.z-m*v+f.z*.05),e),i.knuckles.lerp(this.treatScratch.set(0,.9,.4),e).normalize(),i.palm.lerp(this.treatScratch.set(p*this.treatSide*.8-f.x*.6,.1,m*this.treatSide*.8-f.z*.6),e).normalize(),i.pole.lerp(this.treatScratch.set(-.85*n,-.4,.1),e).normalize()}applyBreakPose(e,t,n,r){let i=this.torso,a=K.lerp;i.hipsPitch=a(i.hipsPitch,.14,e),i.spinePitch=a(i.spinePitch,.08,e);let o=this.scratchC.set(.48*t,1.22,.52),s=this.scratchD.set(-.48*t,1.22,.52);if(this.breakAimed){let[e,n]=this.breakFighters,r=this.scratch.subVectors(n,e).setY(0).normalize(),i=this.scratchQ.setFromAxisAngle(QF,-this.yaw),a=this.scratchB.set(this.rootX??0,0,this.rootZ);o.copy(e).addScaledVector(r,QP).sub(a).applyQuaternion(i),s.copy(n).addScaledVector(r,-QP).sub(a).applyQuaternion(i),o.x*tFP*.15;let s=this.downResult;if(this.downState!==`falling`){let a=this.riseProgress,c=0;if(this.fallKneel)c=pj(LP,1,a),GF(s,this.placeKneel(this.downFrom,e),o,c);else if(a0==t>0;return e.hips.set(.02*t,.53,-.06),e.hipsYaw=Oj.bladeYaw*t*.3,e.hipsPitch=.24,e.hipsRoll=0,e.shouldersYaw=Oj.bladeYaw*t*.2,e.spinePitch=.62,e.headPitch=.48,(r?e.leadHand:e.rearHand).set(n*.13,.66,.2),(r?e.rearHand:e.leadHand).set(.13*t,.66,.37),e.leadFoot.set(.16*t,.125,.32),e.rearFoot.set(-.12*t,.12,-.5),e.leadHeel=0,e.rearHeel=0,e.palmsDown=1,e}placeLying(e){let t=this.boxer.rig.metrics.ankleHeight;return IF(this.landProne?jF(this.downFrom,DF,OF,e,this.landSide,t):jF(this.downFrom,TF,EF,e,this.landSide,t),this.landShift)}placeDown(e,t,n){return jF(e,t,null,n,0,this.boxer.rig.metrics.ankleHeight)}chooseLanding(e){let t=this.boxer.rig.metrics.ankleHeight,n=1/0;for(let r=0;r=n||(n=u.length()+o,this.landProne=s,this.landSide=c,this.landShift.copy(u))}}ropeShift(e,t){let n=this.scratchQ.setFromAxisAngle(QF,this.yaw);bF.set(e.hipsPitch+e.spinePitch*.7,e.hipsYaw,e.hipsRoll,`YXZ`),BF[0].set(0,RF,0).applyEuler(bF).add(e.hips),BF[1].set(.2,zF,0).applyEuler(bF).add(e.hips),BF[2].set(-.2,zF,0).applyEuler(bF).add(e.hips),BF[3].copy(e.leadHand),BF[4].copy(e.rearHand),BF[5].copy(e.leadFoot).z+=e.leadHeel>0?.25:.19,BF[6].copy(e.rearFoot).z+=e.rearHeel>0?.25:.19,BF[7].copy(e.hips);let r=1/0,i=-1/0,a=1/0,o=-1/0;for(let e=0;e0==t>0,c=s?n:r,l=s?r:n;i.pole.lerp($.set(.15*t,.35,1),e).normalize(),a.pole.lerp($.set(-.1*t,-1,.3),e).normalize(),i.toe.lerp($.set(.15*t,0,1),e).normalize(),a.toe.lerp($.set(0,-.35,-1),e).normalize(),c.palm.lerp($.set(-o*.75,0,-.65),e).normalize(),c.knuckles.lerp($.set(-o*.35,.25,.9),e).normalize(),c.pole.lerp($.set(o*.9,-.35,-.25),e).normalize(),l.palm.lerp($.set(0,-1,.1),e).normalize(),l.knuckles.lerp($.set(.2*t,-.2,1),e).normalize(),l.pole.lerp($.set(t*.7,.1,-.7),e).normalize()}writeDown(e,t,n,r,i,a,o,s){let c=this.torso,l=1-s;this.downStand=s,c.hips.copy(e.hips),c.hipsYaw=e.hipsYaw,c.hipsPitch=e.hipsPitch,c.hipsRoll=e.hipsRoll,c.shouldersYaw=e.shouldersYaw,c.spinePitch=e.spinePitch,c.spineRoll=c.spineRoll*s+e.spineRoll,c.headPitch=e.headPitch,c.headYaw=c.headYaw*s+e.turn,c.headRoll=c.headRoll*s+e.headRoll,c.headOffset.multiplyScalar(s),c.headOffset.z-=o,n.position.copy(e.leadHand),r.position.copy(e.rearHand),CF(n,t,e.palmsDown,l),CF(r,-t,e.palmsDown,l),i.position.copy(e.leadFoot),a.position.copy(e.rearFoot),i.toe.lerp(fF.set(.2*t,0,1).normalize(),l).normalize(),a.toe.lerp(fF.set(-.2*t,0,1).normalize(),l).normalize(),i.heel=e.leadHeel,a.heel=e.rearHeel;let u=fF.set(0,0,1).applyEuler(bF.set(e.hipsPitch,e.hipsYaw,e.hipsRoll,`YXZ`));if(i.pole.lerp(u,l),a.pole.lerp(u,l),e.ownJoints>0){let t=e.ownJoints*l;PF(i.pole,e.leadKnee,t),PF(a.pole,e.rearKnee,t),PF(n.pole,e.leadElbow,t),PF(r.pole,e.rearElbow,t),PF(n.knuckles,e.leadKnuckles,t),PF(n.palm,e.leadPalm,t),PF(r.knuckles,e.rearKnuckles,t),PF(r.palm,e.rearPalm,t)}}applyDislocation(){if(this.dislocation!==`jaw`)return;let e=this.boxer.bone(`head`);e!==null&&(e.quaternion.multiply(dF),e.updateWorldMatrix(!1,!0))}dispose(){this.stool.dispose(),this.enswell.dispose(),this.bottle.dispose(),this.boxer.dispose()}},JP=.44,YP=.2,XP=30,ZP=.75,QP=.26,$P=1.2,eF=48,tF=14,nF=1.34,rF=80,iF=56,aF=24,oF=4,sF=1.5,cF=60,lF=4,uF=.15,$=new J,dF=new cm().setFromEuler(new Hm(.08,.2,-.22)),fF=new J,pF=new J(0,0,1),mF=new J(0,-1,0),hF=new J(0,0,1),gF=new J(1,0,0),_F=new J(-1,0,0),vF=.04,yF=new cm,bF=new Hm,xF={pole:fj(.4,.9,.2),knuckles:fj(.6,.2,-.75).normalize(),palm:fj(0,1,.2).normalize()},SF={pole:fj(.6,.2,-.7),knuckles:fj(.25,-.1,1).normalize(),palm:fj(0,-1,.1).normalize()};function CF(e,t,n,r){fF.lerpVectors(xF.pole,SF.pole,n),fF.x*=t,e.pole.lerp(fF,r),fF.lerpVectors(xF.knuckles,SF.knuckles,n),fF.x*=t,e.knuckles.lerp(fF.normalize(),r).normalize(),fF.lerpVectors(xF.palm,SF.palm,n),fF.x*=t,e.palm.lerp(fF.normalize(),r).normalize()}function wF(e={}){return{hips:new J,hipsYaw:0,hipsPitch:0,hipsRoll:0,shouldersYaw:0,spinePitch:0,spineRoll:0,headPitch:0,headRoll:0,leadHand:new J,rearHand:new J,leadFoot:new J,rearFoot:new J,leadHeel:0,rearHeel:0,palmsDown:0,turn:0,leadKnee:new J(0,0,1),rearKnee:new J(0,0,1),leadElbow:new J(0,-1,0),rearElbow:new J(0,-1,0),leadKnuckles:new J(0,0,1),leadPalm:new J(0,-1,0),rearKnuckles:new J(0,0,1),rearPalm:new J(0,-1,0),ownJoints:0,...e}}var TF=wF({hips:fj(0,.14,-.4),hipsPitch:-1.42,spinePitch:.1,headPitch:-.5,leadHand:fj(.52,.1,-.62),rearHand:fj(-.5,.1,-.7),leadFoot:fj(.17,0,.16),rearFoot:fj(-.16,0,-.02)}),EF=wF({hips:fj(.22,0,0),hipsYaw:-.35,hipsRoll:-.25,shouldersYaw:-.2,leadFoot:fj(.05,0,0),rearFoot:fj(.05,0,0)}),DF=wF({hips:fj(0,.25,.32),hipsPitch:1.5,spinePitch:.05,headPitch:.2,palmsDown:1,leadHand:fj(.34,.12,.78),rearHand:fj(-.3,.12,.62),leadFoot:fj(.16,0,-.82),rearFoot:fj(-.15,0,-.86),leadHeel:1.3,rearHeel:1.3}),OF=wF({hips:fj(.12,0,0),hipsYaw:.3,hipsRoll:.15,shouldersYaw:.15,leadFoot:fj(.04,0,0),rearFoot:fj(.04,0,0)}),kF=wF({hips:fj(.05,.52,-.2),hipsYaw:Oj.bladeYaw*.15,hipsPitch:1.38,shouldersYaw:Oj.bladeYaw*.15,spinePitch:.35,headPitch:1,palmsDown:1,leadHand:fj(.2,.115,.36),rearHand:fj(-.2,.115,.32),leadFoot:fj(.15,0,-.74),rearFoot:fj(-.14,0,-.78),leadHeel:1.25,rearHeel:1.25}),AF=wF({hips:fj(0,.47,-.05),hipsYaw:Oj.bladeYaw*.12,hipsPitch:.2,shouldersYaw:Oj.bladeYaw*.4,spinePitch:.3,headPitch:.2,palmsDown:1,leadHand:fj(.15,.68,.32),rearHand:fj(-.26,.42,.02),leadFoot:fj(.15,0,.36),rearFoot:fj(-.13,0,-.7),rearHeel:1.2});function jF(e,t,n,r,i,a){return e.hips.set(t.hips.x*r,t.hips.y,t.hips.z),e.hipsYaw=t.hipsYaw*r,e.hipsPitch=t.hipsPitch,e.hipsRoll=t.hipsRoll*r,e.shouldersYaw=t.shouldersYaw*r,e.spinePitch=t.spinePitch,e.spineRoll=0,e.headPitch=t.headPitch,e.headRoll=0,kj(t.leadHand,r,e.leadHand),kj(t.rearHand,r,e.rearHand),kj(t.leadFoot,r,e.leadFoot).y+=a,kj(t.rearFoot,r,e.rearFoot).y+=a,e.leadHeel=t.leadHeel,e.rearHeel=t.rearHeel,e.palmsDown=t.palmsDown,e.turn=0,e.ownJoints=0,n===null||i===0?e:(e.hips.addScaledVector(n.hips,i),e.hipsYaw+=n.hipsYaw*i,e.hipsPitch+=n.hipsPitch*i,e.hipsRoll+=n.hipsRoll*i,e.shouldersYaw+=n.shouldersYaw*i,e.spinePitch+=n.spinePitch*i,e.headPitch+=n.headPitch*i,e.leadHand.addScaledVector(n.leadHand,i),e.rearHand.addScaledVector(n.rearHand,i),e.leadFoot.addScaledVector(n.leadFoot,i),e.rearFoot.addScaledVector(n.rearFoot,i),e)}var MF=e=>Math.atan2(Math.sin(e),Math.cos(e)),NF=new J;function PF(e,t,n){if(n<=0)return e;e.normalize();let r=e.angleTo(t);return r<1e-4?e:(NF.crossVectors(e,t),NF.lengthSq()<1e-8&&NF.crossVectors(e,Math.abs(e.y)<.9?QF:pF),e.applyAxisAngle(NF.normalize(),r*Math.min(1,n)))}function FF(e,t,n,r){let i=t+MF(e.x-t),a=n+MF(e.y-n),o=r+MF(e.z-r),s=t+MF(Math.PI-e.x-t),c=n+MF(e.y+Math.PI-n),l=r+MF(e.z+Math.PI-r);return Math.abs(i-t)+Math.abs(a-n)+Math.abs(o-r)<=Math.abs(s-t)+Math.abs(c-n)+Math.abs(l-r)?e.set(i,a,o,`YXZ`):e.set(s,c,l,`YXZ`)}function IF(e,t){return e.hips.add(t),e.leadHand.add(t),e.rearHand.add(t),e.leadFoot.add(t),e.rearFoot.add(t),e}var LF=Cw*.72-.05,RF=.78,zF=.5,BF=Array.from({length:8},()=>new J),VF=[.17,.22,.22,.13,.13,.12,.12,.2],HF=[[!1,1,0],[!1,0,.15],[!1,-1,.3],[!0,1,.4],[!0,0,.5],[!0,-1,.6]];function UF(e,t){return e<-LF&&t>LF?-(e+t)/2:e<-LF?-LF-e:t>LF?LF-t:0}function WF(e,t){return GF(e,t,t,0)}function GF(e,t,n,r){let i=K.lerp;return e.hips.copy(t.hips).lerp(n.hips,r),e.hipsYaw=i(t.hipsYaw,n.hipsYaw,r),e.hipsPitch=i(t.hipsPitch,n.hipsPitch,r),e.hipsRoll=i(t.hipsRoll,n.hipsRoll,r),e.shouldersYaw=i(t.shouldersYaw,n.shouldersYaw,r),e.spinePitch=i(t.spinePitch,n.spinePitch,r),e.spineRoll=i(t.spineRoll,n.spineRoll,r),e.headPitch=i(t.headPitch,n.headPitch,r),e.headRoll=i(t.headRoll,n.headRoll,r),e.leadHand.copy(t.leadHand).lerp(n.leadHand,r),e.rearHand.copy(t.rearHand).lerp(n.rearHand,r),e.leadFoot.copy(t.leadFoot).lerp(n.leadFoot,r),e.rearFoot.copy(t.rearFoot).lerp(n.rearFoot,r),e.leadHeel=i(t.leadHeel,n.leadHeel,r),e.rearHeel=i(t.rearHeel,n.rearHeel,r),e.palmsDown=i(t.palmsDown,n.palmsDown,r),e.turn=i(t.turn,n.turn,r),PF(e.leadKnee.copy(t.leadKnee),n.leadKnee,r),PF(e.rearKnee.copy(t.rearKnee),n.rearKnee,r),PF(e.leadElbow.copy(t.leadElbow),n.leadElbow,r),PF(e.rearElbow.copy(t.rearElbow),n.rearElbow,r),PF(e.leadKnuckles.copy(t.leadKnuckles),n.leadKnuckles,r),PF(e.leadPalm.copy(t.leadPalm),n.leadPalm,r),PF(e.rearKnuckles.copy(t.rearKnuckles),n.rearKnuckles,r),PF(e.rearPalm.copy(t.rearPalm),n.rearPalm,r),e.ownJoints=i(t.ownJoints,n.ownJoints,r),e}var KF=3;function qF(e,t,n){if(n===null)return null;let r=t+n;return e===null||Math.abs(r-e)>KF?r:e}function JF(e){return(1-pj(iF,rF,e))*pj(oF,aF,e)}function YF(){let e=new oh;e.name=`enswell`,e.visible=!1;let t=new H_(3.2,3.2,.9,20),n=new H_(.8,.8,6,10),r=new Pv({color:13159634,roughness:.3,metalness:.9}),i=new Ig(t,r);i.rotation.x=Math.PI/2,i.position.set(0,9.5,3.4),e.add(i);let a=new Ig(n,r);return a.position.set(0,5.5,2.4),e.add(a),{group:e,dispose:()=>{t.dispose(),n.dispose(),r.dispose()}}}function XF(){let e=new oh;e.name=`bottle`,e.visible=!1;let t=new H_(3.3,3.3,15,18),n=new H_(1.3,3.3,3,18),r=new H_(.7,1.1,3.2,10),i=new Pv({color:14280430,roughness:.32,metalness:0}),a=new Pv({color:1920728,roughness:.5,metalness:0}),o=new Ig(t,i);o.position.set(0,9,2.6);let s=new Ig(n,i);s.position.set(0,18,2.6);let c=new Ig(r,a);return c.position.set(0,21,2.6),e.add(o,s,c),{group:e,dispose:()=>{t.dispose(),n.dispose(),r.dispose(),i.dispose(),a.dispose()}}}function ZF(){let e=new oh;e.name=`stool`,e.visible=!1;let t=new H_(.17,.17,.035,18),n=new H_(.012,.014,JP-.02,8),r=new Pv({color:13222581,roughness:.85}),i=new Pv({color:2764083,roughness:.45,metalness:.6}),a=new Ig(t,r);a.position.y=JP-.0175,a.castShadow=!0,e.add(a);for(let t=0;t<4;t+=1){let r=Math.PI/4+t*Math.PI/2,a=new Ig(n,i);a.position.set(Math.cos(r)*.13,(JP-.02)/2,Math.sin(r)*.13),a.rotation.set(-Math.sin(r)*.12,0,Math.cos(r)*.12),a.castShadow=!0,e.add(a)}return{group:e,dispose:()=>{t.dispose(),n.dispose(),r.dispose(),i.dispose()}}}var QF=new J(0,1,0),$F=6,eI=5,tI=1.5,nI=.5,rI=.5,iI=new Set([`hit`,`counter_hit`,`block`,`perfect_block`]),aI=new Set([`slip_left`,`slip_right`,`weave`,`pull`]),oI=.25,sI=30,cI=2;function lI(e,t,n){if(e0)||e>1e3||(this.elapsed+=e,this.frames+=1,this.elapsede.before*.9)return this.scale=e.previous,this.holdWindows=Math.min(mI,fI*2**this.fruitlessAttempts),this.fruitlessAttempts+=1,!0;this.fruitlessAttempts=0}if(this.raised!==null){let e=this.raised;if(t>this.options.slowMs)return this.raised=null,this.scale=e.previous,this.fastWindows=0,this.raiseHoldWindows=Math.min(mI,pI*2**this.failedRaises),this.failedRaises+=1,!0;e.windows+=1,e.windows>=gI&&(this.raised=null,this.failedRaises=0)}if(t>this.options.slowMs){if(this.fastWindows=0,this.holdWindows>0)return--this.holdWindows,!1;if(this.scale<=this.options.minimum)return!1;let e=this.scale;return this.scale=Math.max(this.options.minimum,this.scale-this.options.step),this.pending={before:t,previous:e},!0}return t0?(--this.raiseHoldWindows,!1):this.fastWindows>=hI&&this.scalethis.value?7:.9;return this.value+=(e-this.value)*(1-Math.exp(-n*Math.max(0,t))),e===0&&this.value<.003&&(this.value=0),this.value}},SI=.4,CI=.5;function wI(e,t,n=30,r=SI){let i=t.tick-42,a=t.tick+33,o=e.filter(e=>e.tick>=i-1&&e.tick<=a+1),s=o.at(-1),c=s!==void 0&&s.result!==null&&s.tick({...s,tick:s.tick+t+1,events:[]}))]:o,l=c[0],u=c.at(-1);if(l===void 0||u===void 0||c.length<8||l.tick>t.tick-12||u.tickOI?Math.min(1,(i-OI)/4):0;this.strength+=(a-this.strength)*Math.min(1,t*18);let o=this.history.length>=EI?this.history.shift():new J;if(o.copy(e),this.history.push(o),this.strength<.02||this.history.length<3){this.mesh.visible=!1;return}let s=this.history.length;for(let e=0;e3*t}function NI(e,t,n){let r=e.detail.endsWith(`:head`)||e.detail.endsWith(`:body`);return t===void 0||![`hit`,`counter_hit`].includes(e.kind)||!r||n===null||n.winner_id===null||n.winner_id!==e.actor_id?!1:n.finish_method===`flash_ko`||(n.finish_method===`ko`||n.finish_method===`tko`)&&t.is_downed}function PI(e){for(let t of e.passes)t.dispose?.();e.dispose()}function FI(e,t,n,r,i){e.setRenderTarget(t.readBuffer);let a=e.compileAsync(n,r,i);return e.setRenderTarget(null),a}var II=-4e-4,LI=10;function RI(e,t,n){e.autoUpdate=!t,e.intensity=+!t,e.bias=t?LI:II,(t&&e.map!==null||e.mapSize.x!==n)&&(e.mapSize.set(n,n),e.map?.dispose(),e.map=null)}function zI(e,t){let n=t.fighters.findIndex(t=>t.player_id===e.target_id),r=t.fighters.findIndex(t=>t.player_id===e.actor_id),i=e.kind===`block`||e.kind===`perfect_block`;return{recipientIndex:i?r:n,puncherIndex:i?n:r}}function BI(e,t,n){if(e===void 0||t===void 0)return;let r=n.x(t.x)-n.x(e.x),i=n.z(t.y)-n.z(e.y),a=Math.hypot(r,i);return Number.isFinite(a)&&a>.001?{x:r/a,z:i/a}:void 0}var VI=e=>e.kind===`hit`||e.kind===`counter_hit`,HI=e=>e.kind===`block`||e.kind===`perfect_block`,UI=480,WI=.56,GI=1.8,KI=1.7,qI=1.2,JI=1.6,YI=30,XI=6.5,ZI=.4,QI=600,$I=2.8,eL=.6,tL=[[1,0],[-1,0],[.75,-.66],[-.75,-.66],[.75,.66],[-.75,.66]],nL=.15,rL=.6,iL=2.3,aL=.5,oL=3.42,sL=2.2,cL=.9,lL=`body`,uL=.55,dL=.5,fL=.5,pL=.28,mL=104,hL=260,gL=.05,_L=2.95,vL=2.4,yL=1.6,bL=.98,xL={uniforms:{tDiffuse:{value:null},uTime:{value:0},uVignette:{value:.32},uGrain:{value:.035},uRocked:{value:0}},vertexShader:` varying vec2 vUv; void main() { vUv = uv; @@ -4563,6 +4962,7 @@ uniform sampler2D tDiffuse; uniform float uTime; uniform float uVignette; uniform float uGrain; +uniform float uRocked; varying vec2 vUv; float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7)) + uTime * 43.7) * 43758.5453); @@ -4570,13 +4970,22 @@ float hash(vec2 p) { void main() { vec4 color = texture2D(tDiffuse, vUv); vec2 centered = vUv - 0.5; + if (uRocked > 0.001) { + // Hurt vision: a softened picture with a second image drifting off it, drained of colour. + vec2 spread = vec2(0.0035, 0.0) * uRocked; + vec3 soft = (texture2D(tDiffuse, vUv + spread).rgb + texture2D(tDiffuse, vUv - spread).rgb + texture2D(tDiffuse, vUv + spread.yx * 1.6).rgb + texture2D(tDiffuse, vUv - spread.yx * 1.6).rgb) * 0.25; + vec3 ghost = texture2D(tDiffuse, vUv + vec2(sin(uTime * 2.3), cos(uTime * 1.7)) * 0.011 * uRocked).rgb; + color.rgb = mix(color.rgb, mix(soft, ghost, 0.5), 0.75 * uRocked); + color.rgb = mix(color.rgb, vec3(dot(color.rgb, vec3(0.299, 0.587, 0.114))), 0.65 * uRocked); + } float falloff = smoothstep(0.35, 0.95, dot(centered, centered) * 2.2); color.rgb *= 1.0 - falloff * uVignette; + color.rgb *= mix(vec3(1.0), vec3(0.5, 0.1, 0.08), smoothstep(0.08, 0.9, dot(centered, centered) * 2.2) * uRocked * 0.85); float grain = (hash(floor(vUv * vec2(960.0, 540.0))) - 0.5) * uGrain; color.rgb += grain * (0.15 + color.rgb); gl_FragColor = color; -}`},AE={hit:.18,counter_hit:.3,guard_break:.25,knockdown:1,block:.04,perfect_block:.1};function jE(e,t){if(!(!_E(e)||e.action_id===null))return t.find(t=>vE(t)&&t.action_id===e.action_id&&t.actor_id===e.target_id&&t.target_id===e.actor_id)}function ME(e,t){if(!(!vE(e)||e.action_id===null))return t.find(t=>_E(t)&&t.action_id===e.action_id&&t.actor_id===e.target_id&&t.target_id===e.actor_id)}function NE(e,t){return e.map((n,r)=>{let{puncherIndex:i}=hE(n,t),a=t.fighters[i],o=a?.action_key?.split(`:`)??[],s=o.length>=3?`${o[0]}:${o[2]}`:n.detail,c=ME(n,e);if(vE(n))return{event:n,presentationEvent:{...n,detail:c?.detail||s,direction:c?.direction??a?.facing??n.direction,blood:c?.blood??n.blood},presentImpact:!0,reactAmount:n.amount};if(jE(n,e)!==void 0)return{event:n,presentationEvent:n,presentImpact:!1,reactAmount:n.amount};if(n.kind===`knockdown`){let t=e.slice(0,r).reverse().find(e=>_E(e)&&e.tick===n.tick&&e.actor_id===n.actor_id&&e.target_id===n.target_id);if(t!==void 0)return{event:n,presentationEvent:{...n,detail:t.detail,direction:t.direction,blood:t.blood,action_id:t.action_id},presentImpact:!0,reactAmount:jE(t,e)===void 0?null:t.amount}}return{event:n,presentationEvent:n,presentImpact:!0,reactAmount:n.amount}})}function PE(e){return e.result===null?e.tick-1:e.tick}function FE(e,t,n,r){if(!lE(e,t,n))return null;let i=Math.abs(e.event_id);if(e.detail.endsWith(`:head`))return i%2==0?`decapitation`:`jaw_dislocation`;let a=r?.action_key?.split(`:`)[1],o=a===`left`?`right`:a===`right`||Math.floor(i/2)%2==0?`left`:`right`;return i%2==0?`dismember_${o}`:`shoulder_${o}`}function IE(){let e=document.createElement(`canvas`);e.width=128,e.height=128;let t=e.getContext(`2d`);if(t!==null){let e=t.createRadialGradient(64,64,8,64,64,62);e.addColorStop(0,`rgba(0,0,0,0.68)`),e.addColorStop(.6,`rgba(0,0,0,0.34)`),e.addColorStop(1,`rgba(0,0,0,0)`),t.fillStyle=e,t.fillRect(0,0,128,128)}return new lm(e)}var LE={tick_rate:30,ring_half_width:500,ring_half_height:500};function RE(e,t,n){let r=e.geometry,i=r.getAttribute(`position`),a=r.getIndex(),o=[],s=[];if(a===null)for(let e=0;e=i.start+o))return{action:i.punchClass,action_hand:i.hand,action_target:`head`,action_power:`normal`,action_id:`idle-${n}-${r}`,action_key:`${i.punchClass}:${i.hand}:head:normal`,action_start_tick:e-(t-i.start),action_startup_ticks:a.startup,action_active_ticks:a.active,action_recovery_ticks:a.recovery,action_contact_tick:e-(t-i.start)+a.startup}}return{}}var GE=1000003,KE=1.6;function qE(e,t,n,r,i,a){let o=r<0?-1:1,s=o=>Math.hypot(Pu.clamp(e+n*o*i,-a,a)-e,Pu.clamp(t+r*o*i,-a,a)-t);return s(o)>=i*.8||s(o)>=s(-o)?o:-o}function JE(e){return!e.is_downed&&e.stunned_ticks===0&&e.clinch_ticks===0&&e.clinch_startup_ticks===0&&e.taunt_ticks===0&&!e.is_foul_recovery_target}function YE(e){return e===`decapitation`||e===`dismember_left`||e===`dismember_right`}function XE(e,t,n=!1){return e?{standoff:1.25,clearance:1}:n?{standoff:.45,clearance:.5}:t?{standoff:1.15,clearance:.9}:{standoff:2.05,clearance:1.45}}function ZE(e,t,n,r){let i=e.x/r.x(1),a=e.z/r.z(1),o={player_id:`referee`,x:i,y:a,facing:1,facing_x:Math.round(Math.sin(t)*1e3),facing_y:Math.round(-Math.cos(t)*1e3),velocity_x:n.x/r.x(1)/30,velocity_y:n.z/r.z(1)/30,stance:`orthodox`,defense:`none`,stamina:1e3,maximum_stamina:1e3,conditioning:1e3,guard:700,poise:600,trauma:{head:0,body:0,left_eye:0,right_eye:0,left_cut:0,right_cut:0,swelling:0,bleeding:0},knockdowns:0,warnings:0,deductions:0,stunned_ticks:0,is_downed:!1,action:null,action_hand:null,action_target:null,action_power:null,action_id:null,action_key:null,action_start_tick:0,action_startup_ticks:0,action_active_ticks:0,action_recovery_ticks:0,action_contact_tick:null,queued_actions:0,clinch_startup_ticks:0,clinch_ticks:0,is_foul_recovery_target:!1,taunt_ticks:0,get_up_prompt:null,get_up_meter:0,get_up_required:0,get_up_count:0,get_up_window_start_tick:0,get_up_window_end_tick:0,last_input_sequence:-1};return{self:o,focus:{...o,player_id:`focus`,x:i+Math.sin(t)*300,y:a-Math.cos(t)*300}}}var QE=class{canvas;simulation;settings;renderer;scene=new Hd;camera;composer;ring;arena;referee=null;cornermen=null;trails=[];trailGlove=new Z;cornerShirts=[];cornermanPosition=new Z;cornermanVelocity=new Z;cutmen=null;cutmanProgress=[0,0];cutmanFrom=new Z;cutmanTo=new Z;cutmanEye=new Z;cutmanFacing=new Z;cutmanLast=[new Z,new Z];refereePosition=new Z(.4,0,-2.1);refereeVelocity=new Z;effects;director=new Sx;mapping;buffer;localInput;inputSequenceOf;movement=new $b;evasion=new ix;lastManualTime=0;dedupe=new oc;hudCanvas;cameraOverride=null;manualClock;blobShadows=[];blobTexture;lights=[];keyLight=null;followSpot=null;sizeCheck=new X;refereeAway=new Z;refereeYaw=0;raf=0;previous=performance.now();scaler=new WT;pixelRatioCap=Es()?1.5:2;basePixelRatio=Math.min(this.pixelRatioCap,window.devicePixelRatio||1);players={};playerOrder=[];viewerId=null;final=null;reconnectMs=0;destroyed=!1;graphs=null;glbLoading=!1;refereeShirt=null;graphsReady=Promise.resolve();headCache=[new Z,new Z];headCacheValid=[!1,!1];arcadeInjuries=[null,null];observedInjuryDown=[!1,!1];arcadeInjuryEvents=[null,null];replayInjuries=[null,null];closeUpTarget=new Z;downedPoolAccumulators=[0,0];downedPoolCounts=[0,0];bloodLevel=`full`;viewerHitFlash=0;finishSlowMotion=0;finishSeen=!1;finalRevealAt=0;pendingFinish=null;portraitPull=1;lastPhase=null;restStartedAt=0;roundClock=new m;replayFollow=0;replayFollowAt=0;cornerPosition=new Z;cornerLookAt=new Z;finishCloseUpUntil=0;finishCloseUpIndex=-1;closeUpPosition=new Z;roundCalloutUntil=0;roundCalloutRound=0;tmpCamera=new Z;roundStats=new h;history=[];lastKnockdown=null;replay=null;finishPass;bloomPass;inputLatencyMs=null;frameMsAverage=16.7;replayCameraPosition=new Z;replayLookAt=new Z;frameSeconds=0;pendingContacts=[];onContact=null;onArcadeInjury=null;tmpA=new Z;tmpB=new Z;tmpHead=new Z;tmpHeadQuaternion=new Fu;tmpStump=new Z;tmpStumpOffset=new Z;tmpStumpQuaternion=new Fu;tmpPart=new Z;tmpPartQuaternion=new Fu;constructor(e,t=LE,n,r={}){this.canvas=e,this.simulation=t,this.settings=n,this.manualClock=r.manualClock===!0,this.localInput=r.localInput??null,this.inputSequenceOf=r.inputSequenceOf??null,this.buffer=new ac(8,t.tick_rate),this.mapping=Ox(t),this.renderer=new ab({canvas:e,antialias:!0,powerPreference:`high-performance`,preserveDrawingBuffer:r.manualClock===!0}),this.renderer.shadowMap.enabled=!0,this.renderer.shadowMap.type=1,this.renderer.toneMapping=4,this.renderer.toneMappingExposure=1,this.renderer.setPixelRatio(this.basePixelRatio),this.scene.background=new Q(`#04060b`),this.scene.fog=new Vd(`#04060b`,.042),this.camera=new ag(36,1,.1,80),this.camera.position.set(0,2.05,5.9),this.camera.lookAt(0,1.05,0),this.setupLights(),this.ring=oE(),this.scene.add(this.ring.group),this.arena=lx(),this.scene.add(this.arena.group),this.effects=new Qx(this.scene),this.blobTexture=IE();let i=new qm(1,1);for(let e=0;e<3;e+=1){let t=new up(i,new Qf({map:this.blobTexture,transparent:!0,depthWrite:!1}));t.rotation.x=-Math.PI/2,t.position.y=.006+e*4e-4,t.renderOrder=1,this.blobShadows.push(t),this.scene.add(t)}this.composer=new mb(this.renderer),this.composer.addPass(new hb(this.scene,this.camera));let a=new _b(new X(1280,720),.2,.45,1.08);this.bloomPass=a,this.composer.addPass(a),this.finishPass=new db(kE),this.composer.addPass(this.finishPass),this.composer.addPass(new yb),this.hudCanvas=document.createElement(`canvas`),this.hudCanvas.className=`fight-hud`,this.hudCanvas.style.cssText=`position:absolute;inset:0;width:100%;height:100%;pointer-events:none`,e.insertAdjacentElement(`afterend`,this.hudCanvas),this.bloodLevel=n().blood,this.effects.setBloodLevel(this.bloodLevel),this.ensureGraphs(),this.manualClock||(this.raf=requestAnimationFrame(e=>this.draw(e)))}ensureGraphs(){this.glbLoading||this.graphs!==null||(this.glbLoading=!0,this.graphsReady=fw().then(async e=>{if(this.destroyed)return;let t=new xw(e,{skin:11563071,gear:1920728}),n=new xw(e,{skin:7225640,gear:12131356});this.graphs=[new Nw(t,this.mapping),new Nw(n,this.mapping)],this.syncInjuryPresentation(0),this.syncInjuryPresentation(1),this.refereeShirt=BE();let r=new xw(e,{skin:13081462,gear:3889080,pants:1316380,bodyMap:this.refereeShirt});this.referee=new Nw(r,this.mapping,{referee:!0});let i=zE(`#2b4c9e`),a=zE(`#9e2b2b`);this.cornerShirts.push(i,a);let o=new xw(e,{skin:9067067,gear:1778224,pants:1316380,bodyMap:i}),s=new xw(e,{skin:14264188,gear:1778224,pants:1316380,bodyMap:a});this.cornermen=[new Nw(o,this.mapping,{referee:!0}),new Nw(s,this.mapping,{referee:!0})];let c=zE(`#e3e4e8`);this.cornerShirts.push(c);let l=new xw(e,{skin:12160102,gear:1778224,pants:1316380,bodyMap:c}),u=new xw(e,{skin:5913126,gear:1778224,pants:1316380,bodyMap:c});this.cutmen=[new Nw(l,this.mapping,{referee:!0}),new Nw(u,this.mapping,{referee:!0})],l.root.visible=!1,u.root.visible=!1,this.trails=[1920728,12131356].flatMap(e=>[0,1].map(()=>new QT(new Q(e).lerp(new Q(16777215),.55))));let d=new Nd;d.add(t.root,n.root,r.root,o.root,s.root,l.root,u.root,...this.trails.map(e=>e.mesh),...this.effects.compileStandIns());try{await dE(this.renderer,this.composer,d,this.camera,this.scene)}catch{}if(!this.destroyed){this.scene.add(t.root,n.root,r.root,o.root,s.root,l.root,u.root);for(let e of this.trails)this.scene.add(e.mesh)}}).catch(e=>{this.glbLoading=!1,console.error(`boxer_glb_load_failed`,e)}))}get ready(){return this.graphsReady}get labRigs(){let e=this.graphs;return e===null?[]:[e[0].boxer.root,e[1].boxer.root]}get labScene(){return this.scene}get labEffects(){return this.effects}labSetCameraOverride(e,t){this.cameraOverride={position:new Z(...e),lookAt:new Z(...t)}}labFrame(e,t=!0){this.draw(e*1e3,!0,t)}setPlayers(e,t,n=Object.keys(e)){this.players=e,this.viewerId=t,this.playerOrder=n}updateIdleFighters(e,t,n){let r=this.graphs;if(r===null)return;let i=this.playerOrder.filter(e=>this.players[e]!==void 0).length<2;for(let[a,o]of r.entries()){let r=this.playerOrder[a]??`seat-${a}`,s=!i||r===this.viewerId||this.viewerId===null&&a===0;if(o.boxer.root.visible=s,!s)continue;let c=a===0?-1:1,l=Math.floor(t*this.simulation.tick_rate),u={...VE(r),x:i?0:c*180,y:i?-70:0,facing:i?1:-c,facing_x:i?0:-c*1e3,facing_y:i?-1e3:0,...i?WE(l):{}},d={...VE(this.playerOrder[1-a]??`opponent`),x:i?0:-c*180,y:i?-150:0};o.setResting(!1),o.update(u,d,e,t,this.settings().reducedMotion,this.bloodLevel,i?l:n)}}setFinal(e){if(this.final=e,this.finalRevealAt=this.frameSeconds+y(e),this.pendingFinish=null,e===null||!_.has(e.method))return;let t=this.lastKnockdown===null?null:qT(this.history,this.lastKnockdown.knockdown,this.simulation.tick_rate);if(t!==null&&this.graphs!==null&&!this.settings().reducedMotion){this.startReplay(t);return}let n=this.buffer.latest()?.result?.tick;n===void 0?this.presentFinish(e):this.pendingFinish={final:e,tick:n,latestAt:this.frameSeconds+CE}}presentFinishWhenShown(e){let t=this.pendingFinish;t===null||et.player_id!==e.winner_id&&t.player_id!==null)??-1;if(n>=0&&this.headCacheValid[n]&&!this.settings().reducedMotion&&(this.finishCloseUpIndex=n,this.finishCloseUpUntil=this.frameSeconds+SE),e.winner_id===null)return;let r=t?.findIndex(t=>t.player_id===e.winner_id)??-1;r>=0&&this.graphs?.[r]?.celebrate()}cornerShotFrame(e,t,n){if(t===null||t.phase!==`rest`||n||this.graphs===null)return null;let r=t.fighters.findIndex(e=>e.player_id===this.viewerId),i=e-this.restStartedAt,a=hx(i,t.phase_ticks_remaining/this.simulation.tick_rate,+(r===1));return a===null||!this.graphs[a].stoolVisible?null:(xx(a,this.mapping.x(Tb),gx(i),this.cornerPosition,this.cornerLookAt),{position:this.cornerPosition,lookAt:this.cornerLookAt})}closeUpFrame(e){if(e>=this.finishCloseUpUntil||this.finishCloseUpIndex<0||!this.headCacheValid[this.finishCloseUpIndex])return null;let t=this.arcadeInjuries[this.finishCloseUpIndex]===`decapitation`&&this.effects.severedHeadPosition(this.finishCloseUpIndex,this.closeUpTarget),n=t?this.closeUpTarget:this.headCache[this.finishCloseUpIndex],r=t?.8:1.05,i=(e-(this.finishCloseUpUntil-SE))*.25-.2,a=Math.atan2(-n.x,-n.z),o=(Math.hypot(n.x,n.z)>.4?a:.9)+i;return this.closeUpPosition.set(Pu.clamp(n.x+Math.sin(o)*r,-2.2,TE),n.y+(t?.42:.75),Pu.clamp(n.z+Math.cos(o)*r,-2.2,TE)),this.replayLookAt.copy(n),{position:this.closeUpPosition,lookAt:this.replayLookAt,tight:!0}}startReplay(e){let t=new ac(e.snapshots.length+2,this.simulation.tick_rate);for(let n of e.snapshots)t.push(n);this.replay={plan:e,buffer:t,startedAt:this.frameSeconds,impactFired:!1,side:null},this.replayFollow=0,this.replayFollowAt=0;for(let e of[0,1]){let t=this.arcadeInjuries[e],n=this.arcadeInjuryEvents[e];t!==null&&n!==null&&YE(t)&&(this.replayInjuries[e]={injury:t,event:n},this.restoreInjury(e))}for(let e of this.graphs??[])e.resetTransient(!1);this.finalRevealAt=this.frameSeconds+e.durationSeconds+y(this.final)}endReplay(){this.replay=null,this.reapplyReplayInjuries();let e=this.buffer.latest();for(let[t,n]of(this.graphs??[]).entries())n.resetTransient(e?.fighters[t]?.is_downed===!0);this.final!==null&&this.presentFinish(this.final)}fireReplayImpact(e){let t=this.lastKnockdown;if(t===null)return;let n=t.hit??{...t.knockdown,kind:`hit`,amount:420,blood:60},{recipientIndex:r,puncherIndex:i}=hE(n,e),a=e.fighters[r];if(a===void 0)return;this.tmpA.set(this.mapping.x(a.x),0,this.mapping.z(a.y));let o=gE(e.fighters[i],a,this.mapping);this.effects.addEvent(n,this.tmpA,this.settings().reducedMotion,o);let s=(i>=0?e.fighters[i]:void 0)?.action_key?.split(`:`)??[],c=s[0]??null,l=s[1]??null;this.graphs?.[r]?.react(`hit`,n.detail.endsWith(`:body`)?`body`:`head`,n.direction,c,l,n.amount),i>=0&&this.graphs?.[i]?.landedHit(!1),this.onContact?.(n),this.reapplyReplayInjuries(o)}replayFrame(e,t){let[n,r]=e.fighters,i=this.mapping.x(n.x),a=this.mapping.z(n.y),o=this.mapping.x(r.x),s=this.mapping.z(r.y),c=e.fighters.findIndex(e=>e.player_id===this.replay?.plan.impact.target_id),l=this.replay?.impactFired===!0&&c>=0&&this.headCacheValid[c]===!0,u=1-Math.exp(-3*Math.max(0,t-this.replayFollowAt));this.replayFollowAt=t,this.replayFollow+=(+!!l-this.replayFollow)*u;let d=c<0?0:Math.max(this.portraitPull>1.3?.7:0,this.replayFollow*.8),f=(i+o)/2+((c===1?o:i)-(i+o)/2)*d,p=(a+s)/2+((c===1?s:a)-(a+s)/2)*d,m=-(s-a),h=o-i,g=Math.hypot(m,h)||1;m/=g,h/=g;let _=2.5,v=this.replay;v!==null&&v.side===null&&(v.side=qE(f,p,m,h,_,TE));let y=v?.side??(h<0?-1:1);m*=y,h*=y;let b=.35*Math.sin(t*.7),x=m*Math.cos(b)-h*Math.sin(b),S=m*Math.sin(b)+h*Math.cos(b);this.replayCameraPosition.set(Pu.clamp(f+x*_,-2.2,TE),1.38-Math.min(.25,t*.05),Pu.clamp(p+S*_,-2.2,TE));let C=Math.hypot(this.replayCameraPosition.x-f,this.replayCameraPosition.z-p),w=Math.hypot(f,p);if(C.01){let e=KE-C;this.replayCameraPosition.x=Pu.clamp(this.replayCameraPosition.x-f/w*e,-2.2,TE),this.replayCameraPosition.z=Pu.clamp(this.replayCameraPosition.z-p/w*e,-2.2,TE)}let T=l?Pu.clamp(this.headCache[c].y,.45,1.28):1.28;return this.replayLookAt.set(f,1.28+(T-1.28)*this.replayFollow,p),{position:this.replayCameraPosition,lookAt:this.replayLookAt,tight:!0}}setInputLatency(e){this.inputLatencyMs=e}resyncClock(){this.buffer.resync()}setReconnect(e){this.reconnectMs=e}setBloodLevel(e){e!==`full`&&this.bloodLevel===`full`&&this.restoreAllInjuries(),this.bloodLevel=e,e===`off`&&this.downedPoolAccumulators.fill(0),this.effects.setBloodLevel(e)}setReducedMotion(e){e&&(this.restoreAllInjuries(),this.effects.clearDynamic())}syncInjuryPresentation(e){let t=this.arcadeInjuries[e],n=this.graphs;if(n!==null){let r=n[e];r.boxer.setDecapitated(t===`decapitation`),r.boxer.setHandDismembered(`left`,t===`dismember_left`),r.boxer.setHandDismembered(`right`,t===`dismember_right`);let i=t===`jaw_dislocation`?`jaw`:t===`shoulder_left`||t===`shoulder_right`?t:null;r.setArcadeDislocation(i)}}applyArcadeInjury(e,t,n,r){let i=this.graphs!==null&&(t===`jaw_dislocation`||t===`shoulder_left`||t===`shoulder_right`);if(t===`decapitation`){let t=this.headWorldPose(e);if(t!==null){let a=this.graphs?.[e],o=a===void 0?void 0:RE(a.boxer.headMesh,t.position,t.quaternion);this.effects.decapitate(e,t.position,t.quaternion,r??n.direction,n.event_id,this.skinColor(e),o);let s=this.stumpWorldPose(e);s!==null&&this.effects.anchorStump(e,s.position,s.quaternion),i=!0}}else if(t===`dismember_left`||t===`dismember_right`){let a=t===`dismember_left`?`left`:`right`,o=this.handWorldPose(e,a);if(o!==null){let t=this.graphs?.[e],s=t===void 0?void 0:RE(t.boxer.gloveMesh(a),o.position,o.quaternion);this.effects.dismemberHand(e,a,o.position,o.quaternion,r??n.direction,n.event_id,this.gearColor(e),s),this.effects.anchorHandStump(e,a,o.position,o.quaternion),i=!0}}return i?(this.arcadeInjuries[e]=t,this.arcadeInjuryEvents[e]=n,this.observedInjuryDown[e]=!1,this.syncInjuryPresentation(e),this.onArcadeInjury?.(t,n),!0):!1}reapplyReplayInjuries(e){for(let t of[0,1]){let n=this.replayInjuries[t];if(n===null)continue;this.replayInjuries[t]=null;let r=this.settings();r.blood!==`full`||r.reducedMotion||this.applyArcadeInjury(t,n.injury,{...n.event,event_id:n.event.event_id+GE},e)}}restoreInjury(e){this.arcadeInjuries[e]!==null&&(this.arcadeInjuries[e]=null,this.arcadeInjuryEvents[e]=null,this.observedInjuryDown[e]=!1,this.syncInjuryPresentation(e),this.effects.restoreFighter(e))}restoreAllInjuries(){this.restoreInjury(0),this.restoreInjury(1),this.effects.clearArcadeGore()}headWorldPose(e){let t=this.graphs,n=t===null?null:t[e].boxer.bone(`head`);return n===null||t===null?null:(t[e].boxer.root.updateMatrixWorld(!0),n.getWorldPosition(this.tmpHead),n.getWorldQuaternion(this.tmpHeadQuaternion),this.tmpStumpOffset.set(0,.12,.02).applyQuaternion(this.tmpHeadQuaternion),this.tmpHead.add(this.tmpStumpOffset),{position:this.tmpHead,quaternion:this.tmpHeadQuaternion})}handWorldPose(e,t){let n=this.graphs,r=n===null?null:n[e].boxer.bone(t===`left`?`gloveL`:`gloveR`);return r===null||n===null?null:(n[e].boxer.root.updateMatrixWorld(!0),r.getWorldPosition(this.tmpPart),r.getWorldQuaternion(this.tmpPartQuaternion),{position:this.tmpPart,quaternion:this.tmpPartQuaternion})}gearColor(e){return this.graphs?.[e]?.boxer.gearBaseColor.getHex()??1920728}skinColor(e){return this.graphs?.[e]?.boxer.skinBaseColor.getHex()??11563071}stumpWorldPose(e){let t=this.graphs,n=t===null?null:t[e].boxer.bone(`Neck_012`);return n===null||t===null?null:(t[e].boxer.root.updateMatrixWorld(!0),n.getWorldPosition(this.tmpStump),n.getWorldQuaternion(this.tmpStumpQuaternion),{position:this.tmpStump,quaternion:this.tmpStumpQuaternion})}predictAction(e){let t=this.buffer.latest();if(t===null||this.viewerId===null||this.replay!==null)return;let n=t.fighters.findIndex(e=>e.player_id===this.viewerId);if(n<0)return;let r=t.fighters[n];if(t.phase!==`fight`||!JE(r))return;let i=(this.inputLatencyMs??60)/1e3*this.simulation.tick_rate+this.buffer.interpolationDelayTicks;if(nx(e.kind)){!(this.graphs?.[n]?.ownPunchActive===!0||qb(r,t.tick)>0||r.queued_actions>0)&&e.id!==void 0&&r.stamina>=25&&this.evasion.press(e.kind,e.id,this.manualClock?this.lastManualTime:performance.now(),i,this.simulation.tick_rate);return}e.kind!==`punch`||!Kb(r,e)||this.graphs?.[n]?.predict(e,performance.now()/1e3,this.simulation.tick_rate,i,Gb(r,e,t.tick+1))}acknowledgeActions(e,t){for(let[n,r]of e.fighters.entries()){let i=r.player_id===this.viewerId;i&&this.evasion.acknowledge(r,e.phase===`fight`,this.inputSequenceOf);let a=t.filter(t=>hE(t,e).puncherIndex===n);this.graphs?.[n]?.acknowledge(r,e.phase===`fight`,a,i?this.inputSequenceOf:null)}}push(e){if(!this.buffer.push(e,this.manualClock?this.lastManualTime:performance.now()))return;let t=this.dedupe.accept(e.events);this.acknowledgeActions(e,t),this.history.push(e),this.history.length>yE&&this.history.shift();for(let e of t){if(this.roundStats.record(e),e.kind===`knockdown`){let n=t.find(t=>(t.kind===`hit`||t.kind===`counter_hit`)&&t.target_id===e.target_id)??null;this.lastKnockdown={knockdown:e,hit:n}}e.kind===`referee_break`&&this.referee?.breakClinch()}for(let{event:n,presentationEvent:r,presentImpact:i,reactAmount:a}of NE(t,e)){let t=e.fighters.findIndex(e=>e.player_id===n.target_id),o=e.fighters.findIndex(e=>e.player_id===n.actor_id),{recipientIndex:s,puncherIndex:c}=hE(n,e),l=e.fighters[s]??e.fighters[t]??e.fighters[o]??e.fighters[0];if(this.tmpA.set(this.mapping.x(l.x),0,this.mapping.z(l.y)),[`hit`,`counter_hit`,`block`,`perfect_block`,`guard_break`,`knockdown`].includes(n.kind)){let t=c>=0?e.fighters[c]:null;this.pendingContacts.push({event:n,presentationEvent:r,presentImpact:i,reactAmount:a,contactTick:t?.action_contact_tick??n.tick,recipientIndex:s,puncherIndex:c,injury:FE(n,e.fighters[s],e.result,t??void 0)})}else n.kind===`bleed`&&this.effects.addEvent(n,this.tmpA,this.settings().reducedMotion)}}fireContacts(e){for(let t=0;te){t+=1;continue}this.pendingContacts.splice(t,1);let{event:r,presentationEvent:i,presentImpact:a,reactAmount:o,recipientIndex:s,puncherIndex:c,injury:l}=n,u=this.buffer.latest()?.fighters[s],d=gE(this.buffer.latest()?.fighters[c],u,this.mapping);a&&u!==void 0&&(this.tmpA.set(this.mapping.x(u.x),0,this.mapping.z(u.y)),this.effects.addEvent(i,this.tmpA,this.settings().reducedMotion,d));let f=this.settings();a&&this.arena.excite(AE[r.kind]??0),a&&s>=0&&this.viewerId!==null&&this.buffer.latest()?.fighters[s]?.player_id===this.viewerId&&[`hit`,`counter_hit`,`knockdown`].includes(r.kind)&&(this.viewerHitFlash=Math.max(this.viewerHitFlash,Math.min(1,.35+r.amount/400))),l!==null&&s>=0&&this.arcadeInjuries[s]===null&&f.blood===`full`&&!f.reducedMotion&&this.applyArcadeInjury(s,l,r,d);let p=this.graphs;if(a&&s>=0&&[`hit`,`counter_hit`,`knockdown`].includes(r.kind)&&i.detail.endsWith(`:head`)&&r.amount>=260&&!f.reducedMotion&&f.blood!==`off`){let e=this.headWorldPose(s);e!==null&&(this.tmpB.set(0,-.07,.1).applyQuaternion(e.quaternion).add(e.position),this.effects.spawnTeeth(this.tmpB,d??i.direction,r.kind===`knockdown`?3:1+Math.floor((r.amount-260)/120),r.event_id))}if(a&&s>=0&&o!==null&&[`hit`,`counter_hit`,`block`,`perfect_block`,`knockdown`].includes(r.kind)){let e=r.kind===`block`||r.kind===`perfect_block`,t=i.detail.endsWith(`:body`)?`body`:`head`;if(p!==null){let n=(c>=0?this.buffer.latest()?.fighters[c]:void 0)?.action_key?.split(`:`)??[],r=n[0]??i.detail.split(`:`)[0]??null,a=n[1]??null;p[s].react(e?`block`:`hit`,t,i.direction,r,a,o)}}a&&c>=0&&[`hit`,`counter_hit`,`block`,`perfect_block`,`guard_break`].includes(r.kind)&&p?.[c]?.landedHit(r.kind===`block`||r.kind===`perfect_block`),this.onContact?.(r)}}get resultVisible(){return this.final!==null&&this.frameSeconds>=this.finalRevealAt}get resolutionScale(){return this.scaler.scale}get diagnostics(){let e=this.renderer.getContext(),t=e.getExtension(`WEBGL_debug_renderer_info`),n=t===null?e.getParameter(e.RENDERER):e.getParameter(t.UNMASKED_RENDERER_WEBGL);return{frameMs:this.frameMsAverage,resolutionScale:this.scaler.scale,gpu:this.memoryInfo,graphics:String(n)}}get memoryInfo(){return{geometries:this.renderer.info.memory.geometries,textures:this.renderer.info.memory.textures,programs:this.renderer.info.programs?.length??0}}applyResolutionScale(){let e=this.basePixelRatio*this.scaler.scale;this.renderer.setPixelRatio(e),this.composer.setPixelRatio(e);let t=this.scaler.scale<=bE;this.bloomPass.enabled=!t,this.keyLight!==null&&mE(this.keyLight.shadow,t,this.scaler.scale<.8?1024:2048)}setupLights(){let e=new qh(`#3c4a72`,`#07080c`,.55);this.scene.add(e),this.lights.push(e);let t=new sg(`#fff1dc`,150,24,.56,.55,1.7);t.position.set(.6,7.4,1.2),t.target.position.set(0,.6,0),t.castShadow=!0,t.shadow.mapSize.set(2048,2048),t.shadow.bias=fE,t.shadow.camera.near=3,t.shadow.camera.far=14,this.keyLight=t,this.scene.add(t,t.target),this.lights.push(t);let n=new sg(`#ffe6c8`,95,24,.6,.6,1.7);n.position.set(-2.6,7,-2.2),n.target.position.set(0,.8,0),this.scene.add(n,n.target),this.lights.push(n);for(let[e,t,n,r]of[[`#8fa8ff`,-5.5,3.8,-5.4],[`#7f95e8`,5.6,3.6,-5.6]]){let i=new sg(e,34,30,.72,.9,1.8);i.position.set(t,n,r),i.target.position.set(0,1.2,0),this.scene.add(i,i.target),this.lights.push(i)}let r=new sg(`#ffe8d0`,48,22,.32,.8,1.5);r.position.set(0,5.8,5.4),r.target.position.set(0,1,0),this.scene.add(r,r.target),this.lights.push(r),this.followSpot=r}draw(e,t=!1,n=!0){if(this.destroyed)return;let r=e-this.previous,i=t?1/60:Math.min(.05,Math.max(.001,r/1e3));this.previous=e,!t&&this.scaler.record(r)&&this.applyResolutionScale(),!t&&r>0&&r<1e3&&(this.frameMsAverage+=(r-this.frameMsAverage)*.05);let a=Math.min(this.pixelRatioCap,window.devicePixelRatio||1);a!==this.basePixelRatio&&(this.basePixelRatio=a,this.applyResolutionScale());let o=this.canvas.clientWidth,s=this.canvas.clientHeight;o>0&&s>0&&(this.renderer.getSize(this.sizeCheck),(this.sizeCheck.x!==o||this.sizeCheck.y!==s)&&(this.renderer.setSize(o,s,!1),this.composer.setSize(o,s),this.camera.aspect=o/s,this.portraitPull=Pu.clamp(1.2/this.camera.aspect,1,2.2),this.camera.fov=36*Math.min(1.3,Math.sqrt(this.portraitPull)),this.camera.updateProjectionMatrix()));let c=e/1e3;this.frameSeconds=c,this.finishPass.uniforms.uTime.value=c%1e3,t&&(this.lastManualTime=e);let l=this.settings();this.setBloodLevel(l.blood);let u=this.buffer.latest();if(u?.result!==null&&u?.result!==void 0&&_.has(u.result.finish_method)&&!this.finishSeen&&(this.finishSeen=!0,this.finishSlowMotion=2.2,this.arena.excite(1)),this.finishSlowMotion>0&&!l.reducedMotion){this.finishSlowMotion=Math.max(0,this.finishSlowMotion-i);let e=Math.min(1,this.finishSlowMotion/.5);i*=.3+.7*(1-e)}this.viewerHitFlash=Math.max(0,this.viewerHitFlash-i*3.2);let d=u===null?0:t?PE(u):this.buffer.renderTick(e),f=u===null?null:this.applyLocalPrediction(this.buffer.sample(d),i,e),p=this.replay;if(p!==null){let e=c-p.startedAt;e>=p.plan.durationSeconds?this.endReplay():(d=JT(p.plan,e,this.simulation.tick_rate),f=p.buffer.sample(d)??f,!p.impactFired&&d>=p.plan.impact.tick&&f!==null&&(p.impactFired=!0,this.fireReplayImpact(f)))}let m=this.replay===null?i:i*this.replay.plan.speed,h=1.8,g=!1;if(f!==null){let[e,t]=f.fighters;for(let[e,t]of f.fighters.entries())this.arcadeInjuries[e]===null||this.replay!==null||(t.is_downed?this.observedInjuryDown[e]=!0:this.observedInjuryDown[e]&&f.result===null&&this.restoreInjury(e));let n=this.graphs;if(n!==null){let r=this.headCacheValid[0]?this.headCache[0]:void 0,i=this.headCacheValid[1]?this.headCache[1]:void 0;n[0].boxer.root.visible=!0,n[1].boxer.root.visible=!0,n[0].setResting(f.phase===`rest`),n[1].setResting(f.phase===`rest`);let a=f.phase===`countdown`?f.phase_ticks_remaining:null;n[0].setCountdown(a),n[1].setCountdown(a),f.phase===`fight`&&this.lastPhase===`rest`&&this.replay===null&&(this.roundCalloutUntil=c+xE,this.roundCalloutRound=f.round_number),f.phase===`rest`&&this.lastPhase!==`rest`&&(this.restStartedAt=c),this.lastPhase=f.phase,n[0].update(e,t,m,c,l.reducedMotion,l.blood,d,i),n[1].update(t,e,m,c,l.reducedMotion,l.blood,d,r);for(let[e,t]of n.entries()){let n=t.boxer.bone(`head`);n!==null&&(t.boxer.root.updateMatrixWorld(!0),n.getWorldPosition(this.headCache[e]),this.headCacheValid[e]=!0)}}for(let e=0;e.05&&!t.is_downed){this.downedPoolAccumulators[e]=0;let t=e===0?this.tmpA:this.tmpB;this.tmpHead.set(t.x,this.headHeightOf(e),t.z),this.effects.drip(this.tmpHead,n,l.reducedMotion,e)}else if(n>.3&&t.is_downed&&l.blood!==`off`)for(this.effects.stopDrip(e),this.downedPoolAccumulators[e]+=i*.8;this.downedPoolAccumulators[e]>=1;){--this.downedPoolAccumulators[e];let t=this.downedPoolCounts[e];this.downedPoolCounts[e]=t+1;let n=t*2.399963+e*Math.PI,r=.12+t*.618034%1*.24,i=e===0?this.tmpA:this.tmpB;this.effects.pool(i.x+Math.sin(n)*r,i.z+Math.cos(n)*r,.8)}else this.effects.stopDrip(e),this.downedPoolAccumulators[e]=0}}else this.tmpA.set(-.9,0,0),this.tmpB.set(.9,0,0),this.updateIdleFighters(i,c,d);this.arena.update(c,i,l.reducedMotion),this.effects.update(m),this.updateTrails(m,l.reducedMotion),this.updateReferee(m,c,f,d),this.updateCornermen(m,c,f,d),this.updateCutmen(m,c,f,d),this.updateBlobShadows(),this.fireContacts(d),this.presentFinishWhenShown(d),this.followSpot!==null&&this.followSpot.target.position.set((this.tmpA.x+this.tmpB.x)/2,1,(this.tmpA.z+this.tmpB.z)/2);let v=this.director.update(i,c,{x:this.tmpA.x,z:this.tmpA.z},{x:this.tmpB.x,z:this.tmpB.z},h,g,this.effects.shakeAmount,l.reducedMotion),y=this.replay,b=this.cameraOverride??(y!==null&&f!==null?this.replayFrame(f,c-y.startedAt):this.closeUpFrame(c)??this.cornerShotFrame(c,f,l.reducedMotion)??v);if(this.cameraOverride===null&&this.portraitPull>1){let e=b.tight===!0,t=this.portraitPull/Math.min(1.3,Math.sqrt(this.portraitPull)),n=e?Math.min(t,1.25):t;this.tmpCamera.subVectors(b.position,b.lookAt).multiplyScalar(n),this.camera.position.copy(b.lookAt).add(this.tmpCamera),e&&(this.camera.position.x=Pu.clamp(this.camera.position.x,-2.2,TE),this.camera.position.z=Pu.clamp(this.camera.position.z,-2.2,TE)),this.camera.lookAt(b.lookAt.x,b.lookAt.y-(this.portraitPull-1)*(e?.08:.45),b.lookAt.z)}else this.camera.position.copy(b.position),this.camera.lookAt(b.lookAt);n&&(this.composer.render(),this.drawHudOverlay(f)),!this.destroyed&&!this.manualClock&&(this.raf=requestAnimationFrame(e=>this.draw(e)))}applyLocalPrediction(e,t,n){if(e===null||this.localInput===null||this.viewerId===null)return e;let r=e.fighters.findIndex(e=>e.player_id===this.viewerId),i=this.localInput()??{moveX:0,moveY:0,defense:`none`},a=(this.inputLatencyMs??60)/1e3*this.simulation.tick_rate+this.buffer.interpolationDelayTicks,o=r>=0&&e.phase===`fight`?e.fighters[r]:null,s=this.graphs?.[r]?.ownPunchActive===!0||this.evasion.holdsFeet(n,this.simulation.tick_rate),c=this.movement.update(o,i,s,n,a,e.tick,t,this.simulation.tick_rate),l=this.evasion.pose(n);if(r<0)return e;let u=e.fighters[r],d=o===null?u.defense:rx(o,i,l);if(Math.abs(c.dx)<.01&&Math.abs(c.dy)<.01&&d===u.defense)return e;let f=tx(u,c,e.fighters[+(r===0)]??null),p={...u,x:u.x+f.dx,y:u.y+f.dy,defense:d},m=r===0?[p,e.fighters[1]]:[e.fighters[0],p];return{...e,fighters:m}}headHeightOf(e){return(this.graphs?.[e]?.boxer.metrics.headRestY??1.52)+.04}updateBlobShadows(){let e=[this.tmpA,this.tmpB,this.refereePosition];for(let[t,n]of this.blobShadows.entries()){let r=t<2?this.graphs?.[t]:void 0,i=r?.boxer.root.visible===!0?r.currentRoot:e[t];n.position.x=i.x,n.position.z=i.z;let a=t<2&&this.buffer.latest()?.fighters[t]?.is_downed===!0;n.scale.set(a?2.1:1.25,a?.9:.85,1)}}updateReferee(e,t,n,r){let i=this.referee;if(i===null)return;let a=n?.fighters.find(e=>e.is_downed)??null,o=n?.fighters.some(e=>e.clinch_ticks>0||e.clinch_startup_ticks>0)??!1,s=a===null&&i.breaking,c=a===null?(this.tmpA.x+this.tmpB.x)/2:this.mapping.x(a.x),l=a===null?(this.tmpA.z+this.tmpB.z)/2:this.mapping.z(a.y),u=this.refereeAway.set(this.refereePosition.x-c,0,this.refereePosition.z-l);if(u.lengthSq()<.01&&u.set(0,0,-1),u.normalize(),s){let e=this.tmpB.z-this.tmpA.z,t=this.tmpA.x-this.tmpB.x,n=Math.hypot(e,t),r=e*u.x+t*u.z>=0?1:-1;n>.01&&u.set(e/n*r,0,t/n*r)}let{standoff:d,clearance:f}=XE(a!==null,o,s),p=Pu.clamp(c+u.x*d,-2.4,2.4),m=Pu.clamp(l+u.z*d,-2.4,2.4),h=this.refereePosition.x,g=this.refereePosition.z,_=1-Math.exp(-(s?8:1.6)*e),v=s?Math.min(1,DE*e/Math.max(1e-6,Math.hypot(p-this.refereePosition.x,m-this.refereePosition.z)*_)):1;this.refereePosition.x+=(p-this.refereePosition.x)*_*v,this.refereePosition.z+=(m-this.refereePosition.z)*_*v;for(let e of[this.tmpA,this.tmpB]){let t=this.refereePosition.x-e.x,n=this.refereePosition.z-e.z,r=Math.hypot(t,n);r.001&&(this.refereePosition.x=e.x+t/r*f,this.refereePosition.z=e.z+n/r*f)}e>0&&this.refereeVelocity.set((this.refereePosition.x-h)/e,0,(this.refereePosition.z-g)/e);let y=((Math.atan2(c-this.refereePosition.x,l-this.refereePosition.z)-this.refereeYaw+Math.PI)%(Math.PI*2)+Math.PI*2)%(Math.PI*2)-Math.PI;this.refereeYaw+=y*(1-Math.exp(-(s?8:3)*e));let b=ZE(this.refereePosition,this.refereeYaw,this.refereeVelocity,this.mapping);i.setRefereeCount(a!==null,a?.get_up_count??0),i.aimBreak(this.tmpA,this.tmpB),i.update(b.self,b.focus,e,t,!1,`off`,r)}updateTrails(e,t){let n=this.graphs;if(!(n===null||this.trails.length<4))for(let[r,i]of n.entries())for(let[n,a]of[`gloveL`,`gloveR`].entries()){let o=this.trails[r*2+n];if(!i.boxer.root.visible){o.reset();continue}i.boxer.rig.bones[a].getWorldPosition(this.trailGlove),o.update(this.trailGlove,e,this.camera.position,!t&&this.replay===null)}}updateCornermen(e,t,n,r){let i=this.cornermen;if(i===null)return;let a=n?.phase===`rest`;for(let[n,o]of i.entries()){let i=n===0?-1:1,s=a?EE:wE;this.cornermanPosition.set(i*s,0,-i*s);let c=n===0?3*Math.PI/4:-Math.PI/4;o.attend(a);let l=ZE(this.cornermanPosition,c,this.cornermanVelocity,this.mapping);o.update(l.self,l.focus,e,t,!1,`off`,r)}}updateCutmen(e,t,n,r){let i=this.cutmen,a=this.graphs;if(i===null||a===null)return;let o=n?.phase===`rest`,s=this.mapping.x(Tb);for(let[c,l]of i.entries()){let i=c===0?-1:1,u=o&&a[c].stoolVisible?1:0,d=this.cutmanProgress[c],f=d+Math.sign(u-d)*Math.min(Math.abs(u-d),e/OE);this.cutmanProgress[c]=f;let p=l.boxer.root.visible;if(l.boxer.root.visible=f>.005,!l.boxer.root.visible)continue;this.cutmanFrom.set(i*wE,0,-i*(wE-1)),bx(c===0?0:1,s,_x,32,this.cutmanTo);let m=f*f*(3-2*f);this.cornermanPosition.copy(this.cutmanFrom).lerp(this.cutmanTo,m);let h=this.cutmanLast[c];p||h.copy(this.cornermanPosition),this.cornermanVelocity.copy(this.cornermanPosition).sub(h).divideScalar(Math.max(e,.001)),h.copy(this.cornermanPosition);let g=f>.02&&f<.98,_=this.headCacheValid[c]?this.headCache[c]:null,v=g?Math.atan2(this.cutmanTo.x-this.cutmanFrom.x,this.cutmanTo.z-this.cutmanFrom.z)+(u===1?0:Math.PI):f>=.98?_===null?Math.atan2(i,-i):Math.atan2(_.x-this.cutmanTo.x,_.z-this.cutmanTo.z):Math.atan2(-this.cutmanFrom.x,-this.cutmanFrom.z),y=n?.fighters[c]?.trauma,b=y!==void 0&&y.right_eye+y.right_cut>y.left_eye+y.left_cut?-1:1;if(f>=.98&&_!==null){let e=a[c].boxer.root.rotation.y;this.cutmanFacing.set(Math.sin(e),0,Math.cos(e)),this.cutmanEye.copy(_).addScaledVector(this.cutmanFacing,.09),this.cutmanEye.x+=Math.cos(e)*b*.035,this.cutmanEye.z-=Math.sin(e)*b*.035,this.cutmanEye.y+=.08,l.treat(this.cutmanEye,this.cutmanFacing,b)}else l.treat(null);let x=ZE(this.cornermanPosition,v,this.cornermanVelocity,this.mapping);l.update(x.self,x.focus,e,t,!1,`off`,r)}}drawHudOverlay(e){let t=cE(this.hudCanvas);if(t===null)return;let{context:n,viewport:r}=t;if(n.clearRect(0,0,r.width,r.height),e===null)return;if(this.viewerHitFlash>.01&&!this.settings().reducedMotion){let e=n.createRadialGradient(r.width/2,r.height/2,r.width*.12,r.width/2,r.height/2,r.width*.7);e.addColorStop(0,`rgba(255,235,225,${(this.viewerHitFlash*.18).toFixed(3)})`),e.addColorStop(1,`rgba(140,0,10,${(this.viewerHitFlash*.55).toFixed(3)})`),n.fillStyle=e,n.fillRect(0,0,r.width,r.height)}let i=Math.max(...e.fighters.map(e=>e.trauma.head+e.trauma.body));if(i>350){let e=n.createRadialGradient(r.width/2,r.height/2,r.width*.2,r.width/2,r.height/2,r.width*.72);e.addColorStop(0,`rgba(90,0,8,0)`),e.addColorStop(1,`rgba(75,0,8,${Math.min(.3,i/4600)})`),n.fillStyle=e,n.fillRect(0,0,r.width,r.height)}b(n,r.width,r.height,e,this.players,this.viewerId,this.frameSeconds>=this.finalRevealAt?this.final:null,this.reconnectMs,this.simulation.tick_rate,this.roundStats,this.replay===null?null:`KNOCKOUT REPLAY`,this.inputLatencyMs,this.frameSecondsvoid 0,getItem:()=>null,key:()=>null,removeItem:()=>void 0,setItem:()=>void 0},tD=()=>{try{return window.localStorage}catch{return eD}},nD=()=>({volume:.7,haptics:!0,reducedMotion:window.matchMedia?.(`(prefers-reduced-motion: reduce)`).matches??!1,blood:`full`});function rD(e=tD()){let t=nD();try{let n=JSON.parse(e.getItem($E)??`null`);if(typeof n!=`object`||!n||Array.isArray(n))return t;let r=n;return{volume:typeof r.volume==`number`&&Number.isFinite(r.volume)?Math.max(0,Math.min(1,r.volume)):t.volume,haptics:typeof r.haptics==`boolean`?r.haptics:t.haptics,reducedMotion:typeof r.reducedMotion==`boolean`?r.reducedMotion:t.reducedMotion,blood:r.blood===`reduced`||r.blood===`off`||r.blood===`full`?r.blood:t.blood}}catch{return t}}function iD(e,t=tD()){try{t.setItem($E,JSON.stringify({volume:Math.max(0,Math.min(1,e.volume)),haptics:e.haptics,reducedMotion:e.reducedMotion,blood:e.blood}))}catch{}}var aD=class{value;listeners=new Set;constructor(e=tD()){this.storage=e,this.value=rD(e)}storage;get current(){return this.value}update(e){this.value={...this.value,...e},iD(this.value,this.storage);for(let e of this.listeners)e(this.value)}subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}destroy(){this.listeners.clear()}},oD={stage:`bootstrapping`,player:null,playerId:null,role:null,players:{},playerOrder:[],simulation:null,snapshot:null,final:null,serverTick:0,nextSequence:0,reconnectMs:0,safeError:null},sD=e=>Object.fromEntries(e.map(e=>[e.id,e]));function cD(e,t){if(t.type===`bootstrap`)return{...e,stage:`authorizing`,simulation:t.simulation,safeError:null};if(t.type===`authorized`)return{...e,stage:`connecting`,player:t.player,safeError:null};if(t.type===`connecting`)return{...e,stage:`connecting`};if(t.type===`reconnect-tick`)return{...e,stage:`paused`,reconnectMs:Math.max(0,t.remainingMs)};if(t.type===`fatal`)return{...e,stage:`fatal`,safeError:t.code,reconnectMs:0};let n=t.message;switch(n.type){case`welcome`:return{...e,playerId:n.role===`fighter`?n.player_id:null,role:n.role,players:sD(n.players),playerOrder:n.players.map(e=>e.id),serverTick:n.server_tick,nextSequence:n.role===`fighter`?n.next_sequence:0,safeError:null};case`ticket`:return e;case`waiting`:return{...e,stage:`waiting`};case`ready`:return{...e,stage:e.snapshot?.phase??`countdown`,players:sD(n.players),playerOrder:n.players.map(e=>e.id)};case`paused`:return{...e,stage:`paused`,reconnectMs:n.grace_ms};case`resumed`:return{...e,stage:e.snapshot?.phase??`countdown`,reconnectMs:0};case`snapshot`:return{...e,stage:n.payload.phase,snapshot:n.payload,serverTick:Math.max(e.serverTick,n.payload.tick)};case`final`:return{...e,stage:`complete`,final:n,reconnectMs:0};case`error`:return{...e,stage:`fatal`,safeError:n.code,reconnectMs:0}}}var lD=new Set([`hit`,`counter_hit`,`block`,`perfect_block`,`guard_break`,`knockdown`]),uD=`Move WASD · Jab F/J · Straight R/U · Hook G/H · Uppercut T/Y · Guard Q/E · Body Shift · Power Alt`,dD=`Left side: drag to move · Right pads: L/R punches · Hold BODY, POWER or GUARD · Tap SLIP, WEAVE, PULL or CLINCH`,fD=11e3,pD=4e3,mD=3e3,hD=6,gD=class{root;reloadPage;authorizer;state=oD;settings=new aD;input=new qs;audio=new E(()=>this.settings.current);haptics=new js(()=>this.settings.current);feedbackEvents=new oc;renderer=null;network=null;session=null;abort=new AbortController;destroyed=!1;generation=0;reloadOnRetry=!1;reloading=!1;canvas;status;overlay;roleIndicator;controlsButton;controlsPanel;retry;rematchButton;hint;diagnosticsTimer=null;finalReceivedAt=0;lastFinalMatchId=null;rematchAttempts=0;rematchTimer=null;rematchCountdownTimer=null;resultRevealTimer=null;resultWaitTimer=null;completeSince=null;summaryClock=new m;fightSummary;liveFightStatus;finalSummary;constructor(e,t=()=>window.location.reload(),n=new Cs){this.root=e,this.reloadPage=t,this.authorizer=n,e.innerHTML=`
HANDSauthoritative two-player boxing

`,this.canvas=e.querySelector(`canvas`),this.status=e.querySelector(`[data-status]`),this.overlay=e.querySelector(`[data-overlay]`),this.roleIndicator=e.querySelector(`[data-role]`),this.controlsButton=e.querySelector(`[data-controls]`),this.controlsPanel=e.querySelector(`[data-controls-panel]`),this.retry=e.querySelector(`[data-retry]`),this.rematchButton=e.querySelector(`[data-rematch]`),this.hint=e.querySelector(`[data-hint]`),this.rematchButton.addEventListener(`click`,this.onRematch),this.fightSummary=e.querySelector(`[data-fight-summary]`),this.liveFightStatus=e.querySelector(`[data-fight-status]`),this.finalSummary=e.querySelector(`[data-final]`),this.retry.addEventListener(`click`,this.onRetry),this.bindPanels(),this.syncSettings(),this.input.attachTouch(e.querySelector(`.activity`))}start(){this.authorize()}resetForAuthorization(){this.reloadOnRetry=!1,this.network?.dispose(),this.network=null,this.session?.destroy(),this.session=null,this.renderer?.destroy(),this.renderer=null,this.abort.abort(),this.abort=new AbortController,this.state=oD,this.feedbackEvents.reset(),this.input.setActive(!1),this.input.reset(),this.finalSummary.textContent=``,this.fightSummary.textContent=``,this.liveFightStatus.textContent=``,this.clearRematchTimers(),this.rematchButton.hidden=!0}onRematch=()=>{this.rematchButton.disabled||this.state.stage!==`complete`||(this.rematchAttempts=1,this.authorize())};clearRematchTimers(){this.rematchTimer!==null&&window.clearTimeout(this.rematchTimer),this.rematchCountdownTimer!==null&&window.clearInterval(this.rematchCountdownTimer),this.rematchTimer=null,this.rematchCountdownTimer=null}awaitResultReveal(){this.resultRevealTimer!==null&&window.clearInterval(this.resultRevealTimer);let e=()=>{let e=this.renderer?.resultVisible??!0;this.overlay.hidden=this.state.stage===`complete`&&!e,(e||this.state.stage!==`complete`)&&this.resultRevealTimer!==null&&(window.clearInterval(this.resultRevealTimer),this.resultRevealTimer=null)};this.resultRevealTimer=window.setInterval(e,200),e()}startRematchCountdown(){if(this.clearRematchTimers(),this.state.role!==`fighter`)return;let e=()=>{let e=Math.ceil((this.finalReceivedAt+fD-Date.now())/1e3);this.rematchButton.hidden=this.state.stage!==`complete`,this.rematchButton.disabled=e>0,this.setText(this.rematchButton,e>0?`Rematch in ${e}s`:`Rematch`),e<=0&&this.rematchCountdownTimer!==null&&(window.clearInterval(this.rematchCountdownTimer),this.rematchCountdownTimer=null)};e(),this.rematchCountdownTimer=window.setInterval(e,1e3)}scheduleRematchRetry(){if(this.rematchTimer===null){if(this.rematchAttempts>=hD){this.fail(`rematch_unavailable`);return}this.rematchAttempts+=1,this.setText(this.status,`The ring is still being cleared… trying again.`),this.rematchTimer=window.setTimeout(()=>{this.rematchTimer=null,this.authorize()},mD)}}async authorize(){let e=++this.generation;this.resetForAuthorization(),this.dispatch({type:`connecting`}),this.setText(this.status,`Securing Discord Activity session…`),this.retry.hidden=!0;try{let t=await this.authorizer.authorize(this.abort.signal);if(this.destroyed||e!==this.generation){t.destroy();return}this.session=t,this.dispatch({type:`bootstrap`,simulation:t.bootstrap.simulation}),this.dispatch({type:`authorized`,player:t.player}),this.input.onAction(e=>{this.renderer?.predictAction?.(e),this.network?.notifyAction()});let n=t.takeTicket();if(n===null)throw Error(`ticket_unavailable`);let r=new gc(n,()=>this.input.frame(),{onMessage:e=>this.receive(e),onReconnect:e=>{this.dispatch({type:`reconnect-tick`,remainingMs:e}),this.renderer?.setReconnect(e),this.renderState()},onFatal:e=>this.fail(e),onFreshAuth:()=>{e===this.generation&&this.authorize()}});this.network=r,r.start(),this.setText(this.status,`Connecting to the ring…`)}catch(t){!this.abort.signal.aborted&&e===this.generation&&(this.reloadOnRetry=t instanceof Ee&&t.reloadRequired,this.fail(Me(t)))}}ensureRenderer(){if(this.renderer!==null||this.session===null||this.destroyed)return;let e=new QE(this.canvas,this.session.bootstrap.simulation,()=>this.settings.current,{localInput:()=>this.input.held(),inputSequenceOf:e=>this.network?.sequenceOf(e)??null});e.onContact=e=>{this.audio.event(e),this.haptics.event(e)},e.onArcadeInjury=e=>this.audio.injury(e),this.renderer=e}receive(e){if(e.type===`error`){this.fail(e.code);return}if(e.type===`welcome`&&this.ensureRenderer(),(e.type===`welcome`||e.type===`resumed`)&&this.renderer?.resyncClock(),(e.type===`waiting`||e.type===`ready`)&&(this.rematchAttempts=0),e.type===`final`&&this.rematchAttempts>0&&e.match_id===this.lastFinalMatchId){this.scheduleRematchRetry();return}this.dispatch({type:`message`,message:e}),e.type===`snapshot`&&this.receiveSnapshot(e.payload),e.type===`final`&&(this.rematchAttempts=0,this.lastFinalMatchId=e.match_id,this.finalReceivedAt=Date.now(),this.renderer?.setFinal(e),this.audio.result(e),this.startRematchCountdown(),this.awaitResultReveal()),this.renderer?.setPlayers(this.state.players,this.state.playerId,this.state.playerOrder),this.renderer?.setReconnect(this.state.reconnectMs),this.renderState()}get networkStats(){return{inputLatencyMs:this.network?.inputLatencyMs??null,resolutionScale:this.renderer?.resolutionScale??null,gpu:this.renderer?.memoryInfo??null}}receiveSnapshot(e){this.renderer?.setInputLatency(this.network?.inputLatencyMs??null),this.renderer?.push(e);let t=e.fighters.find(e=>e.player_id===this.state.playerId);this.input.setKnockdown(t?.is_downed===!0),t!==void 0&&this.audio.snapshot(e.tick,t.stamina,t.maximum_stamina,t.trauma.head+t.trauma.body),this.audio.roundClock(e.phase,e.round_number,e.phase_ticks_remaining,this.state.simulation?.tick_rate??30);for(let t of this.feedbackEvents.accept(e.events))lD.has(t.kind)||(this.audio.event(t),this.haptics.event(t))}dispatch(e){this.state=cD(this.state,e)}setText(e,t){e.textContent!==t&&(e.textContent=t)}renderState(){let e={bootstrapping:`Loading…`,authorizing:`Authorizing with Discord…`,connecting:`Connecting securely…`,waiting:`Waiting for one opponent to use Play now in this channel.`,countdown:`Bout countdown.`,fight:`Round ${this.state.snapshot?.round_number??1} in progress.`,knockdown:`Knockdown count ${this.state.snapshot?.fighters.find(e=>e.player_id===this.state.playerId)?.get_up_count??0}.`,foul_recovery:`Foul recovery in progress.`,rest:`Between-round rest.`,paused:`Connection paused. ${Math.ceil(this.state.reconnectMs/1e3)} seconds remain.`,complete:`Bout complete. Scorecards and rating changes are displayed.`,fatal:Ts(this.state.safeError??`safe_error`)},t=this.state.role===`spectator`;if(this.setText(this.status,t?`Spectating — ${e[this.state.stage]}`:e[this.state.stage]),this.status.hidden=[`countdown`,`fight`,`knockdown`,`foul_recovery`,`rest`].includes(this.state.stage),this.overlay.toggleAttribute(`data-raised`,this.state.snapshot!==null),this.state.stage!==`complete`)this.overlay.hidden=!1,this.completeSince=null;else if(this.state.final===null){this.completeSince??=Date.now();let e=Date.now()-this.completeSince;this.overlay.hidden=e{this.resultWaitTimer=null,this.renderState()},pD-e+50))}this.roleIndicator.hidden=!t,this.controlsButton.hidden=t,t&&(this.controlsPanel.hidden=!0,this.controlsButton.setAttribute(`aria-expanded`,`false`));let n=this.state.snapshot?.fighters.find(e=>e.player_id===this.state.playerId),r=t?`Spectating. ${e[this.state.stage]}`:this.state.stage===`knockdown`&&n?.is_downed===!0?`Knockdown. Count ${n.get_up_count}. ${n.get_up_prompt===null?`Wait for your private rhythm instruction.`:`Press ${n.get_up_prompt===`get_up_left`?`left`:`right`} now.`}`:this.state.stage===`paused`?`Connection paused.`:e[this.state.stage];this.setText(this.liveFightStatus,r),this.retry.hidden=this.state.stage!==`fatal`,this.setText(this.retry,this.reloadOnRetry?`Reload`:`Retry securely`),this.state.stage!==`complete`&&(this.rematchButton.hidden=!0);let i=!t&&(this.state.stage===`waiting`||this.state.stage===`countdown`);this.hint.hidden=!i,i&&this.setText(this.hint,Es()?dD:uD);let a=!t&&[`countdown`,`fight`,`knockdown`,`foul_recovery`].includes(this.state.stage),o=!t&&this.state.snapshot!==null&&(this.state.stage===`rest`||this.state.stage===`paused`);if(this.input.setActive(a,a||o),this.network?.setActive(a),this.renderFightSummary(),this.state.final!==null){let e=this.state.final;this.setText(this.finalSummary,`${e.method.replaceAll(`_`,` `)}. ${e.winner_id===null?`Draw`:`${this.state.players[e.winner_id]?.name??`Winner`} wins`}. Scorecards: ${e.scorecards.map(e=>`${e.judge}: ${e.player_one.reduce((e,t)=>e+t,0)} to ${e.player_two.reduce((e,t)=>e+t,0)}`).join(`; `)}. Ratings: ${Object.entries(e.ratings).map(([e,t])=>`${this.state.players[e]?.name??`fighter`} ${t.before} to ${t.after}`).join(`; `)}.`)}}renderFightSummary(){let e=this.state.snapshot;if(e===null)return;let t=this.state.simulation?.tick_rate??30,n=Math.floor(this.summaryClock.ticks(e)/t),r=`${Math.floor(n/60)}:${String(n%60).padStart(2,`0`)}`,i=e.fighters.map(e=>{let t=this.state.players[e.player_id];return`${t?.name??`Fighter`}, ELO ${t?.rating??`unknown`}, stamina ${Math.round(e.stamina)} of ${Math.round(e.maximum_stamina)}, guard ${Math.round(e.guard)}, poise ${Math.round(e.poise)}, conditioning ${Math.round(e.conditioning)}, ${e.warnings} warnings, ${e.knockdowns} knockdowns`}),a=e.fighters.find(e=>e.player_id===this.state.playerId),o=a?.is_downed===!0?a.get_up_prompt===null?`You are down. Count ${a.get_up_count}. Wait for your private rhythm instruction.`:`You are down. Count ${a.get_up_count}. Press ${a.get_up_prompt===`get_up_left`?`left`:`right`} now. Get-up progress ${a.get_up_meter} of ${a.get_up_required}.`:``;this.setText(this.fightSummary,`Round ${e.round_number}. ${e.phase.replace(`_`,` `)}. Clock ${r}. ${i.join(`. `)}. ${o}`.trim())}onRetry=()=>{if(this.reloadOnRetry){this.reloading=!0,this.reloadPage();return}this.authorize()};bindPanels(){let e=(e,t)=>{let n=this.root.querySelector(e),r=this.root.querySelector(t);n.addEventListener(`click`,()=>{r.hidden=!r.hidden,n.setAttribute(`aria-expanded`,String(!r.hidden))})};e(`[data-controls]`,`[data-controls-panel]`),e(`[data-settings]`,`[data-settings-panel]`);let t=this.root.querySelector(`[data-settings-panel]`);this.root.querySelector(`[data-settings]`).addEventListener(`click`,()=>{this.diagnosticsTimer!==null&&window.clearInterval(this.diagnosticsTimer),this.diagnosticsTimer=null,!t.hidden&&(this.refreshDiagnostics(),this.diagnosticsTimer=window.setInterval(()=>this.refreshDiagnostics(),1e3))});let n=this.root.querySelector(`[data-copy-diagnostics]`);n.addEventListener(`click`,()=>{let e=this.diagnosticsText(),t=()=>{this.setText(n,`Copied`),window.setTimeout(()=>this.setText(n,`Copy diagnostics`),1500)},r=()=>{let e=this.root.querySelector(`[data-diagnostics]`),t=window.getSelection();if(t===null)return;let r=document.createRange();r.selectNodeContents(e),t.removeAllRanges(),t.addRange(r),this.setText(n,`Selected. Copy manually`),window.setTimeout(()=>this.setText(n,`Copy diagnostics`),2500)},i=navigator.clipboard;if(i===void 0||typeof i.writeText!=`function`){r();return}i.writeText(e).then(t).catch(r)}),this.root.querySelector(`[data-volume]`).addEventListener(`input`,e=>{this.settings.update({volume:Number(e.target.value)}),this.audio.setVolume()}),this.root.querySelector(`[data-haptics]`).addEventListener(`change`,e=>{this.settings.update({haptics:e.target.checked})}),this.root.querySelector(`[data-motion]`).addEventListener(`change`,e=>{let t=e.target.checked;this.settings.update({reducedMotion:t}),this.renderer?.setReducedMotion(t)}),this.root.querySelector(`[data-blood]`).addEventListener(`change`,e=>{let t=e.target.value;this.settings.update({blood:t}),this.renderer?.setBloodLevel(t)})}diagnosticsText(){let e=this.networkStats,t=this.renderer?.diagnostics,n=t===void 0||t.frameMs<=0?`-`:(1e3/t.frameMs).toFixed(0);return[`stage: ${this.state.stage} · role: ${this.state.role??`-`}`,`input latency: ${e.inputLatencyMs===null?`-`:`${Math.round(e.inputLatencyMs)} ms`}`,`frame: ${t===void 0?`-`:`${t.frameMs.toFixed(1)} ms (${n} fps)`} · render scale: ${t?.resolutionScale??`-`}`,`gpu objects: ${t===void 0?`-`:`${t.gpu.geometries} geometries, ${t.gpu.textures} textures, ${t.gpu.programs} programs`}`,`graphics: ${t?.graphics??`-`}`,`browser: ${navigator.userAgent}`,`pointer: ${Es()?`coarse`:`fine`} · viewport: ${window.innerWidth}x${window.innerHeight} @${window.devicePixelRatio}`,`last error: ${this.state.safeError??`none`}`,`match: ${this.state.final?.match_id??`-`}`].join(` -`)}refreshDiagnostics(){this.setText(this.root.querySelector(`[data-diagnostics]`),this.diagnosticsText())}syncSettings(){let e=this.settings.current;this.root.querySelector(`[data-volume]`).value=String(e.volume),this.root.querySelector(`[data-haptics]`).checked=e.haptics,this.root.querySelector(`[data-motion]`).checked=e.reducedMotion,this.root.querySelector(`[data-blood]`).value=e.blood}fail(e){if(e===`room_closed`&&this.rematchAttempts>0){this.scheduleRematchRetry();return}e===`client_outdated`&&(this.reloadOnRetry=!0),this.dispatch({type:`fatal`,code:e}),this.network?.dispose(),this.network=null,this.session?.destroy(),this.session=null,this.renderer?.destroy(),this.renderer=null,this.input.setActive(!1),this.input.reset(),this.renderState()}destroy(){this.destroyed||(this.destroyed=!0,this.generation+=1,this.abort.abort(),this.network?.dispose(),this.session?.destroy(),this.reloading||this.authorizer.close(),this.renderer?.destroy(),this.input.destroy(),this.audio.destroy(),this.settings.destroy(),this.retry.removeEventListener(`click`,this.onRetry),this.rematchButton.removeEventListener(`click`,this.onRematch),this.clearRematchTimers(),this.diagnosticsTimer!==null&&window.clearInterval(this.diagnosticsTimer),this.resultRevealTimer!==null&&window.clearInterval(this.resultRevealTimer),this.resultWaitTimer!==null&&window.clearTimeout(this.resultWaitTimer),this.diagnosticsTimer=null,this.root.replaceChildren())}},_D={broadcast:{position:[0,1.88,7.15],lookAt:[0,1.12,0]},side:{position:[7.4,1.6,0],lookAt:[0,1.1,0]},top:{position:[.01,8.5,.01],lookAt:[0,0,0]},ringside:{position:[1.6,1.5,4.1],lookAt:[.2,1.25,0]}},vD=[[`hips`,`spine`],[`spine`,`head`],[`chest`,`shoulderL`],[`shoulderL`,`elbowL`],[`elbowL`,`gloveL`],[`chest`,`shoulderR`],[`shoulderR`,`elbowR`],[`elbowR`,`gloveR`],[`hips`,`hipL`],[`hipL`,`kneeL`],[`kneeL`,`ankleL`],[`hips`,`hipR`],[`hipR`,`kneeR`],[`kneeR`,`ankleR`]],yD=class{root;renderer=null;replay=null;raf=0;playing=!1;speed=1;virtualTick=0;carry=0;latencyMs=0;pendingQueue=[];destroyed=!1;overlays={skeleton:!1,hurtbox:!1,hitbox:!1};skeletonLines=[];hurtboxMeshes=[];hitboxMeshes=[];statusEl;metricsEl;canvas;constructor(e){this.root=e,e.innerHTML=`
+}`},SL=[{x:-102,y:-16},{x:102,y:-16}],CL=[0,1],wL=.5,TL={x:0,z:-.2},EL=0,DL=200,OL=1.3,kL=.7,AL=4.5;function jL(e,t,n,r){let i=n.x-e,a=n.y-t,o=Math.hypot(i,a);return o<=Math.max(r,1e-6)?{x:n.x,y:n.y,arrived:!0}:{x:e+i/o*r,y:t+a/o*r,arrived:!1}}var ML=16,NL=.6,PL={hit:.18,counter_hit:.3,guard_break:.25,knockdown:1,block:.04,perfect_block:.1,parry:.22,body_collapse:.45},FL=new Set([`hit`,`counter_hit`,`block`,`perfect_block`,`guard_break`,`knockdown`,`parry`,`body_collapse`,`eye_shut`]),IL=new Set([`parry`,`body_collapse`,`eye_shut`]),LL=1.2,RL={parry:{text:`PARRIED`,seconds:1},eye_shut:{text:`EYE SWOLLEN SHUT`,seconds:1.6}},zL=e=>e.kind===`knockdown`&&e.detail===`body`;function BL(e,t){if(!(!VI(e)||e.action_id===null))return t.find(t=>HI(t)&&t.action_id===e.action_id&&t.actor_id===e.target_id&&t.target_id===e.actor_id)}function VL(e,t){if(!(!HI(e)||e.action_id===null))return t.find(t=>VI(t)&&t.action_id===e.action_id&&t.actor_id===e.target_id&&t.target_id===e.actor_id)}function HL(e,t){return e.map((n,r)=>{if(IL.has(n.kind))return{event:n,presentationEvent:n,presentImpact:!1,reactAmount:null};let{puncherIndex:i}=zI(n,t),a=t.fighters[i],o=a?.action_key?.split(`:`)??[],s=o.length>=3?`${o[0]}:${o[2]}`:n.detail,c=VL(n,e);if(HI(n))return{event:n,presentationEvent:{...n,detail:c?.detail||s,direction:c?.direction??a?.facing??n.direction,blood:c?.blood??n.blood},presentImpact:!0,reactAmount:n.amount};if(BL(n,e)!==void 0)return{event:n,presentationEvent:n,presentImpact:!1,reactAmount:n.amount};if(n.kind===`knockdown`){let t=e.slice(0,r).reverse().find(e=>VI(e)&&e.tick===n.tick&&e.actor_id===n.actor_id&&e.target_id===n.target_id);if(t!==void 0)return{event:n,presentationEvent:{...n,detail:t.detail,direction:t.direction,blood:t.blood,action_id:t.action_id},presentImpact:!0,reactAmount:BL(t,e)===void 0?null:t.amount}}return{event:n,presentationEvent:n,presentImpact:!0,reactAmount:n.amount}})}function UL(e){let t=e?.trauma;if(t===void 0)return{side:1,lift:.08,lateral:.035,prop:`enswell`};switch(e?.corner_choice){case`cut`:return{side:t.right_cut>t.left_cut?-1:1,lift:.11,lateral:.04,prop:`enswell`};case`swelling`:return{side:t.right_eye>t.left_eye?-1:1,lift:.08,lateral:.035,prop:`enswell`};case`breath`:return{side:1,lift:-.015,lateral:0,prop:`bottle`};default:return{side:t.right_eye+t.right_cut>t.left_eye+t.left_cut?-1:1,lift:.08,lateral:.035,prop:`enswell`}}}function WL(e){return e?.split(`:`)[1]===`right`?1:-1}function GL(e){return e.result===null?e.tick-1:e.tick}var KL=80,qL=[`jab`,`straight`,`hook`,`uppercut`];function JL(e,t,n,r){if(!NI(e,t,n))return null;let i=r?.action_key?.split(`:`),a=e.detail.split(`:`).find(e=>qL.includes(e))??i?.[0],o=i?.[1]===`right`?`left`:`right`,s=e.amount;return e.detail.endsWith(`:head`)?n?.finish_method===`flash_ko`||e.kind===`counter_hit`&&s>=95?`head_burst`:a===`uppercut`&&s>=70||a===`straight`&&s>=KL?`decapitation`:a===`hook`&&s>=70?`eye_${o}`:`jaw_dislocation`:a===`hook`&&s>=95?`dismember_${o}`:a===`hook`&&s>=70?`shoulder_${o}`:`ribs_${o}`}function YL(){let e=document.createElement(`canvas`);e.width=128,e.height=128;let t=e.getContext(`2d`);if(t!==null){let e=t.createRadialGradient(64,64,8,64,64,62);e.addColorStop(0,`rgba(0,0,0,0.68)`),e.addColorStop(.6,`rgba(0,0,0,0.34)`),e.addColorStop(1,`rgba(0,0,0,0)`),t.fillStyle=e,t.fillRect(0,0,128,128)}return new F_(e)}var XL=new J(0,.12,.02),ZL=new J(0,1,0),QL=new WeakMap;function $L(e){let t=QL.get(e);return t===void 0&&(t=OE(e,vR),QL.set(e,t)),t}function eR(e){return e.y>qE-1.4}var tR=new WeakMap;function nR(e){let t=tR.get(e);return t===void 0&&(t=OE(e,eR),tR.set(e,t)),t}function rR(e,t,n){let r=e.bone(`head`);if(r===null)return null;e.root.updateMatrixWorld(!0);let i=e.headMesh,a=nR(i.geometry);if(a.length===0)return null;let o=t.length===a.length*3?t:new Float32Array(a.length*3);n.position.set(0,0,0);for(let[t,r]of a.entries())fR(e,r,n.scratch),n.scratch.toArray(o,t*3),n.position.add(n.scratch);return n.position.multiplyScalar(1/a.length),r.getWorldQuaternion(n.quaternion),n.across.set(1,0,0).applyQuaternion(n.quaternion),n.scratch.set(0,1,0).applyQuaternion(n.quaternion),n.quaternion.setFromUnitVectors(ZL,n.scratch),o}function iR(e,t,n,r){let i=e?.currentRoot;return r.set(i?.x??n.x(t.x),0,i?.z??n.z(t.y))}function aR(e,t,n,r,i){let a=Math.hypot(e-n,t-r);return a>=i?{x:e,z:t}:a<1e-6?{x:n,z:r-i}:{x:n+(e-n)/a*i,z:r+(t-r)/a*i}}var oR=[0,1.05,-1.05,2.1,-2.1,Math.PI];function sR(e,t,n,r,i,a,o=[]){let s=n===null?0:Math.hypot(n.x,n.z),c=n!==null&&s>=.35?[Math.atan2(n.x,n.z)]:[];for(let e of oR)c.push(a+e);for(let n of c){let a=K.clamp(e+Math.sin(n)*r,-i,i),s=K.clamp(t+Math.cos(n)*r,-i,i),c=Math.hypot(a-e,s-t);if(!(c{let r=K.clamp(((n.x-e)*(a-e)+(n.z-t)*(s-t))/(c*c),.25,1);return Math.hypot(e+(a-e)*r-n.x,t+(s-t)*r-n.z)1e-9&&(s=Math.min(s,(Math.sign(n)*i-e)/n)),Math.abs(r)>1e-9&&(s=Math.min(s,(Math.sign(r)*a-t)/r));for(let i of[-1,1])for(let a of[-1,1]){let c=i*n+a*r;c>1e-9&&(s=Math.min(s,(o-i*e-a*t)/c))}return Math.max(0,s)}function lR(e,t,n,r,i,a,o,s=1/0){let c=Math.hypot(n-e,r-t);if(c>=i)return null;let l=c<1e-6?1:(n-e)/c,u=c<1e-6?0:(r-t)/c,d=i-c,f=cR(e,t,-l,-u,a,o,s),p=cR(n,r,l,u,a,o,s),m=Math.min(d/2,f),h=Math.min(d-m,p);m=Math.min(d-h,f);let g=K.clamp;return{ax:g(e-l*m,-a,a),ay:g(t-u*m,-o,o),bx:g(n+l*h,-a,a),by:g(r+u*h,-o,o)}}var uR=new WeakMap;function dR(e,t,n,r,i,a,o){let s=uR.get(e);if(s!==void 0&&s.width===t&&s.height===n)return s.top;let c=_n(t,n,mn(e,r,i,r.map(e=>a.total(e.player_id))),!0).y;return uR.set(e,{width:t,height:n,top:c}),c}function fR(e,t,n){let r=e.headMesh;return kD(n.fromBufferAttribute(r.geometry.getAttribute(`position`),t),e.look,n),r.applyBoneTransform(t,n).applyMatrix4(r.matrixWorld)}var pR=new Dm,mR=new Dm,hR=new Mm;function gR(e,t,n){let r=e.geometry;pR.fromBufferAttribute(r.getAttribute(`skinIndex`),t),mR.fromBufferAttribute(r.getAttribute(`skinWeight`),t),n.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);for(let t=0;t<4;t+=1){let r=mR.getComponent(t);if(r===0)continue;let i=pR.getComponent(t);hR.multiplyMatrices(e.skeleton.bones[i].matrixWorld,e.skeleton.boneInverses[i]);for(let e=0;e<16;e+=1)n.elements[e]+=hR.elements[e]*r}return n.premultiply(e.bindMatrixInverse).multiply(e.bindMatrix)}var _R=new J(-1,0,0);function vR(e){return e.y>GE-KE*(e.z- -4)-.3}var yR={tick_rate:30,ring_half_width:500,ring_half_height:500};function bR(e,t,n,r,i,a){let o=e.geometry,s=o.getAttribute(`position`),c=o.getIndex(),l=[],u=c!==null&&r===void 0?[]:null;if(c===null||u===null)for(let e=0;e=i.start+o))return{action:i.punchClass,action_hand:i.hand,action_target:`head`,action_power:`normal`,action_id:`idle-${n}-${r}`,action_key:`${i.punchClass}:${i.hand}:head:normal`,action_start_tick:e-(t-i.start),action_startup_ticks:a.startup,action_active_ticks:a.active,action_recovery_ticks:a.recovery,action_contact_tick:e-(t-i.start)+a.startup}}return{}}var ER=1000003,DR=1.6;function OR(e,t,n,r,i,a){let o=r<0?-1:1,s=o=>Math.hypot(K.clamp(e+n*o*i,-a,a)-e,K.clamp(t+r*o*i,-a,a)-t);return s(o)>=i*.8||s(o)>=s(-o)?o:-o}function kR(e){return!e.is_downed&&e.stunned_ticks===0&&e.clinch_ticks===0&&e.clinch_startup_ticks===0&&e.taunt_ticks===0&&!e.is_foul_recovery_target}function AR(e,t){if(e===null)return null;let n=t.fighters.find(t=>t.player_id===e.target_id),r=t.fighters.find(t=>t.player_id===e.actor_id);return JL(e,n===void 0?void 0:{...n,is_downed:!0},{finish_method:`ko`,winner_id:e.actor_id},r)}var jR=.2,MR=2,NR=.42,PR=new Set([`decapitation`,`head_burst`,`eye_left`,`eye_right`,`dismember_left`,`dismember_right`,`ribs_left`,`ribs_right`]),FR=1.6,IR=3,LR=.8,RR=new J(0,-.1,.08);function zR(e,t,n=NR){return Math.min(n,.05+.045*Math.sqrt(Math.max(0,e))*Math.min(1.6,Math.max(0,t)))}function BR(e,t,n){return e.is_downed||t?.isDown===!0||n!==void 0&&e.player_id===n}var VR=6,HR=11,UR=.9,WR=7;function GR(e,t){let n=t===`left`?`leftRibs`:`rightRibs`;e.impact(n,[t===`left`?-6:VR,-6*.15,-6*.25],HR),e.set(n,{bruise:1.2,swell:0,blood:1.4})}function KR(e){return e!==`jaw_dislocation`&&e!==`shoulder_left`&&e!==`shoulder_right`}var qR=new WeakMap;function JR(e,t){let n=qR.get(e);if(n===void 0){let t=e.getAttribute(`position`),r=new J,i=new J,a=$E.map(e=>{let n=0,a=1/0;i.set(e[0],e[1],e[2]);for(let e=0;ethis.onAnnouncement?.(e),cue:e=>this.onCrowdCue?.(e)});touchControls=$l();resultAnnounced=!1;captionText=``;rocked=new xI;tmpA=new J;tmpB=new J;contactPoint=new J;cornerPanelTop=null;mouthPoint=new J;tmpHead=new J;tmpHeadQuaternion=new cm;tmpStump=new J;tmpStumpOffset=new J;tmpStumpQuaternion=new cm;tmpStumpAcross=new J;tmpPart=new J;tmpPartQuaternion=new cm;constructor(e,t=yR,n,r={}){this.canvas=e,this.simulation=t,this.settings=n,this.manualClock=r.manualClock===!0,this.localInput=r.localInput??null,this.inputSequenceOf=r.inputSequenceOf??null,this.buffer=new Ru(8,t.tick_rate),this.mapping=Ew(t),this.renderer=new PC({canvas:e,antialias:!0,powerPreference:`high-performance`,preserveDrawingBuffer:r.manualClock===!0}),this.renderer.shadowMap.enabled=!0,this.renderer.shadowMap.type=1,this.renderer.toneMapping=4,this.renderer.toneMappingExposure=1,this.renderer.setPixelRatio(this.basePixelRatio),this.scene.background=new Y(`#04060b`),this.scene.fog=new mh(`#04060b`,.042),this.camera=new My(36,1,.1,80),this.camera.position.set(0,2.05,5.9),this.camera.lookAt(0,1.05,0),this.setupLights(),this.ring=GN(),this.scene.add(this.ring.group),this.arena=aT(),this.scene.add(this.arena.group),this.effects=new fk(this.scene,$l()?1024:2048),this.effects.useUploader(this.renderer),this.blobTexture=YL();let i=new bv(1,1);for(let e=0;e<3;e+=1){let t=new Ig(i,new wg({map:this.blobTexture,transparent:!0,depthWrite:!1}));t.rotation.x=-Math.PI/2,t.position.y=.006+e*4e-4,t.renderOrder=1,this.blobShadows.push(t),this.scene.add(t)}this.composer=new UC(this.renderer),this.composer.addPass(new WC(this.scene,this.camera));let a=new KC(new q(1280,720),.2,.45,1.08);this.bloomPass=a,this.composer.addPass(a),this.finishPass=new BC(xL),this.composer.addPass(this.finishPass),this.composer.addPass(new JC),this.hudCanvas=document.createElement(`canvas`),this.hudCanvas.className=`fight-hud`,this.hudCanvas.style.cssText=`position:absolute;inset:0;width:100%;height:100%;pointer-events:none`,e.insertAdjacentElement(`afterend`,this.hudCanvas),this.bloodLevel=n().blood,this.effects.setBloodLevel(this.bloodLevel),this.ensureGraphs(),this.manualClock||(this.raf=requestAnimationFrame(e=>this.draw(e)))}ensureGraphs(){this.glbLoading||this.graphs!==null||(this.glbLoading=!0,this.graphsReady=nP().then(async e=>{if(this.destroyed)return;let t=new hP(e,{skin:11563071,gear:1920728}),n=new hP(e,{skin:7225640,gear:12131356});this.graphs=[new qP(t,this.mapping),new qP(n,this.mapping)],this.lookIds.fill(null),this.syncInjuryPresentation(0),this.syncInjuryPresentation(1);let r=new hP(e,{skin:13081462,gear:3889080,look:_D.referee,outfit:xA});this.referee=new qP(r,this.mapping,{referee:!0});let i=new hP(e,{skin:9067067,gear:1778224,look:_D.blueCorner,outfit:SA}),a=new hP(e,{skin:14264188,gear:1778224,look:_D.redCorner,outfit:CA});this.cornermen=[new qP(i,this.mapping,{referee:!0}),new qP(a,this.mapping,{referee:!0})];let o=new hP(e,{skin:12160102,gear:1778224,look:_D.blueCutman,outfit:wA}),s=new hP(e,{skin:5913126,gear:1778224,look:_D.redCutman,outfit:wA});this.cutmen=[new qP(o,this.mapping,{referee:!0}),new qP(s,this.mapping,{referee:!0})],o.root.visible=!1,s.root.visible=!1,this.trails=[1920728,12131356].flatMap(e=>[0,1].map(()=>new kI(new Y(e).lerp(new Y(16777215),.55))));let c=new oh;c.add(t.root,n.root,r.root,i.root,a.root,o.root,s.root,...this.trails.map(e=>e.mesh),...this.effects.compileStandIns());let l=t.headInjury.open;t.headInjury.setOpen(!0);try{await FI(this.renderer,this.composer,c,this.camera,this.scene)}catch{}if(!this.destroyed){this.scene.add(t.root,n.root,r.root,i.root,a.root,o.root,s.root);for(let e of this.trails)this.scene.add(e.mesh);this.compileShadowsOf(c),t.headInjury.setOpen(l)}}).catch(e=>{this.glbLoading=!1,console.error(`boxer_glb_load_failed`,e)}))}compileShadowsOf(e){let t=this.keyLight;if(!(t===null||e.children.length===0)){for(let t of e.children)t.frustumCulled=!1,t.onBeforeShadow=(e,t,n,r,i,a)=>{a.needsUpdate=!0};this.scene.add(e),t.shadow.needsUpdate=!0,this.renderer.setRenderTarget(this.composer.readBuffer);try{this.renderer.render(this.scene,this.camera)}catch{}finally{this.renderer.setRenderTarget(null),this.scene.remove(e)}}}get ready(){return this.graphsReady}get labRigs(){let e=this.graphs;return e===null?[]:[e[0].boxer.root,e[1].boxer.root]}get labScene(){return this.scene}get labEffects(){return this.effects}labSetCameraOverride(e,t){this.cameraOverride={position:new J(...e),lookAt:new J(...t)}}labFrame(e,t=!0){this.draw(e*1e3,!0,t)}setCornerPanelTop(e){this.cornerPanelTop=e}viewForward(){return this.ownViewActive?(this.ownForward.x=this.fighterCam.forwardX,this.ownForward.z=this.fighterCam.forwardZ,this.ownForward):null}setPlayers(e,t,n=Object.keys(e)){this.players=e,this.viewerId=t,this.playerOrder=n}updateIdleFighters(e,t,n){let r=this.graphs;if(r===null)return;let i=this.playerOrder.filter(e=>this.players[e]!==void 0).length<2;for(let[a,o]of r.entries()){let r=this.playerOrder[a]??`seat-${a}`,s=!i||r===this.viewerId||this.viewerId===null&&a===0;if(o.boxer.root.visible=s,!s)continue;this.wearLook(a===0?0:1,r);let c=a===0?-1:1,l=Math.floor(t*this.simulation.tick_rate),u={...SR(r),x:i?0:c*180,y:i?-70:0,facing:i?1:-c,facing_x:i?0:-c*1e3,facing_y:i?-1e3:0,...i?TR(l):{}},d={...SR(this.playerOrder[1-a]??`opponent`),x:i?0:-c*180,y:i?-150:0};o.setResting(!1),o.update(u,d,e,t,this.settings().reducedMotion,this.bloodLevel,i?l:n)}}setFinal(e){if(e!==null&&e.match_id===this.final?.match_id||(this.final=e,this.stoppageWinner=-1,this.stoppageRaiseAt=1/0,this.stoppageSpot=null,this.raiseFramed=!1,this.ceremony=this.ceremonyFor(e),this.finalRevealAt=this.frameSeconds+(this.ceremony===null?sn(e):AL),this.ovationUntil=e===null?0:this.frameSeconds+ML,e!==null&&this.commentary.finish(e,this.players,this.ceremony!==null,this.frameSeconds),this.ceremony===null&&this.endCeremony(),this.pendingFinish=null,e===null||!rn.has(e.method)))return;let t=this.lastKnockdown===null?null:wI(this.history,this.lastKnockdown.hit??this.lastKnockdown.knockdown,this.simulation.tick_rate),n=this.finishingInjury(e);if(t!==null&&this.graphs!==null&&!this.settings().reducedMotion){n!==null&&(this.replayInjuries[n.index]={injury:n.injury,event:n.event}),this.startReplay(t);return}if(n!==null){let e=this.pendingContacts.find(e=>e.event.event_id===n.event.event_id);e===void 0?this.applyArcadeInjury(n.index,n.injury,n.event):e.injury??=n.injury}let r=this.buffer.latest()?.result?.tick;r===void 0?this.presentFinish(e):this.pendingFinish={final:e,tick:r,latestAt:this.frameSeconds+aL}}presentFinishWhenShown(e){let t=this.pendingFinish;t===null||ee.player_id===t.hit.target_id)??-1;return r<0||this.arcadeInjuries[r]!==null?null:{index:r,injury:t.finisher,event:t.hit}}ceremonyFor(e){let t=this.buffer.latest()?.fighters;if(e===null||t===void 0||e.method!==`decision`&&e.method!==`draw`||t.some(e=>e.is_downed))return null;let n=t.findIndex(t=>t.player_id===e.winner_id);return{winnerSeat:n===0||n===1?n:null,positions:null,refereeArrived:!1,arrivedAt:null,announced:!1}}endCeremony(){for(let e of this.graphs??[])e.awaitVerdict(null);this.referee?.raise(null,null)}ceremonyFighters(e,t,n,r,i=null){e.positions===null&&(e.positions=[{x:t[0].x+(i?.[0].x??0),y:t[0].y+(i?.[0].y??0)},{x:t[1].x+(i?.[1].x??0),y:t[1].y+(i?.[1].y??0)}],e.marks=e.positions[0].x<=e.positions[1].x?[0,1]:[1,0]);let a=e.marks??CL,o=e.refereeArrived;for(let r of[0,1]){let i=e.positions[r],s=a[r],c=jL(i.x,i.y,SL[s],DL*n),l=Math.hypot(c.x-i.x,c.y-i.y),u=n>0?1/(n*this.simulation.tick_rate):0,d=!c.arrived&&l>1e-6;Object.assign(this.drawnFighters[r],t[r],{x:c.x,y:c.y,velocity_x:(c.x-i.x)*u,velocity_y:(c.y-i.y)*u,facing_x:d?Math.round((c.x-i.x)/l*1e3):0,facing_y:d?Math.round((c.y-i.y)/l*1e3):-1e3,facing:d&&c.x=kL){e.announced=!0,this.commentary.verdict(this.frameSeconds);for(let t of[0,1])this.graphs?.[t]?.announce(e.winnerSeat===null?`level`:e.winnerSeat===t?`winner`:`loser`);this.finalRevealAt=Math.min(this.finalRevealAt,this.frameSeconds+.35),this.arena.excite(1)}if(e.announced){let t=t=>{let n=a[0]===t?0:1,r=e.winnerSeat===null||e.winnerSeat===n,i=this.graphs?.[n]?.boxer.rig.bones[t===0?`gloveL`:`gloveR`];return r&&i!==void 0?i.getWorldPosition(this.ceremonyWrists[t]):null};this.referee?.raise(t(1),t(0))}return this.drawnFighters}ceremonyFrame(e){let t=this.ceremony,n=this.buffer.latest();if(t===null||t.arrivedAt===null||this.replay!==null||this.final===null||n===null)return null;let{width:r,height:i}=this.hudViewport,a=dR(this.final,r,i,n.fighters,this.players,this.roundStats,this.viewerId),o=wT(this.camera.aspect,this.camera.fov,(i-a)/Math.max(1,i)),s=this.settings().reducedMotion?0:Math.sin((e-t.arrivedAt)*.35)*.06*o.distance;return this.cornerPosition.set(s,o.height+.05*o.distance,o.distance),this.cornerLookAt.set(0,o.height,0),{position:this.cornerPosition,lookAt:this.cornerLookAt,framed:!0}}raiseFrame(e,t,n){let r=this.buffer.latest();if(this.stoppageWinner<0||this.final===null||this.replay!==null||this.ceremony!==null||r===null||e0?-1:1)*eL,o=this.stoppageSpot?.z??0;this.raiseFramed||this.raiseCentre.set(i.x+a/2,0,i.z+o/2),this.raiseCentre.lerp(this.tmpCamera.set(i.x+a/2,0,i.z+o/2),1-Math.exp(-3*t)),this.raiseFramed=!0;let{width:s,height:c}=this.hudViewport,l=dR(this.final,s,c,r.fighters,this.players,this.roundStats,this.viewerId),u=wT(this.camera.aspect,this.baseFov,(c-l)/Math.max(1,c)),d=e-(this.stoppageRaiseAt-$I),f=n?0:Math.sin(d*.35)*.06*u.distance;return this.cornerPosition.set(K.clamp(this.raiseCentre.x,-1.4,1.4)+f,u.height+.05*u.distance,this.raiseCentre.z+u.distance),this.cornerLookAt.set(this.raiseCentre.x,u.height,this.raiseCentre.z),{position:this.cornerPosition,lookAt:this.cornerLookAt,framed:!0}}presentFinish(e){this.referee?.waveOff();let t=this.buffer.latest()?.fighters,n=t?.findIndex(t=>t.player_id!==e.winner_id&&t.player_id!==null)??-1;if(n>=0&&this.headCacheValid[n]&&!this.settings().reducedMotion&&(this.finishCloseUpIndex=n,this.finishCloseUpUntil=this.frameSeconds+KI,this.finishCloseUpBearing=null,this.finishCloseUpAt=this.frameSeconds),e.winner_id===null)return;let r=t?.findIndex(t=>t.player_id===e.winner_id)??-1;r<0||(this.graphs?.[r]?.celebrate(QI),this.stoppageWinner=r,this.stoppageRaiseAt=this.frameSeconds+$I)}cornerShotFrame(e,t,n){if(t===null||t.phase!==`rest`||n||this.graphs===null)return null;let r=t.fighters.findIndex(e=>e.player_id===this.viewerId),i=e-this.restStartedAt,a=gT(i,t.phase_ticks_remaining/this.simulation.tick_rate,+(r===1));return a===null||!this.graphs[a].stoolVisible||this.cutmen!==null&&this.cutmanProgress[a]e.player_id===this.viewerId)===1),n=this.mapping.x(s);return xT(t,n,-3.5,YI,this.cornerPosition).setY(XI),xT(t,n,0,0,this.cornerLookAt),this.cornerLookAt.lerp(xT(+(t===0),n,0,0,this.tmpCamera),ZI).setY(.9),{position:this.cornerPosition,lookAt:this.cornerLookAt,framed:!0}}closeUpFrame(e){if(e>=this.finishCloseUpUntil||this.finishCloseUpIndex<0||!this.headCacheValid[this.finishCloseUpIndex])return null;let t=this.arcadeInjuries[this.finishCloseUpIndex],n=t===`decapitation`&&this.effects.severedHeadPosition(this.finishCloseUpIndex,this.closeUpTarget),r=n?null:this.graphs?.[this.finishCloseUpIndex]?.fallBody??null;if(this.finishCloseUpBearing===null&&r!==null&&!r.body.asleep&&e-this.finishCloseUpAt.4?l:.9,c)}let u=this.finishCloseUpBearing+c;return this.closeUpPosition.set(K.clamp(o.x+Math.sin(u)*s,-2.2,sL),o.y+(n?.42:a?.5:.75),K.clamp(o.z+Math.cos(u)*s,-2.2,sL)),this.replayLookAt.copy(o),{position:this.closeUpPosition,lookAt:this.replayLookAt,tight:!0}}raiseSpot(e,t){let n=e.x>0?-1:1,r={x:n*eL,z:0},i=-1/0;for(let[a,o]of tL){let s=e.x+n*a*eL,c=e.z+o*eL,l=Math.min(iL-Math.abs(s),iL-Math.abs(c),Math.hypot(s-t.x,c-t.z)-rL);for(let e of this.graphs??[]){let t=e.fallBody;if(t!==null)for(let e of[0,1,2,3,4])t.bodyPoint(e,this.bodyPoint),l=Math.min(l,Math.hypot(s-this.bodyPoint.x,c-this.bodyPoint.z)-uL-.05)}for(let e of[0,1])this.effects.severedHeadPosition(e,this.closeUpTarget)&&(l=Math.min(l,Math.hypot(s-this.closeUpTarget.x,c-this.closeUpTarget.z)-cL-.05));if(l>=0)return{x:s-e.x,z:c-e.z};l>i&&(i=l,r={x:s-e.x,z:c-e.z})}return r}nearerGlove(e,t){e.getWorldPosition(this.stoppageWrist),t.getWorldPosition(this.stoppageOtherWrist);let n=e=>Math.hypot(e.x-this.refereePosition.x,e.z-this.refereePosition.z);return n(this.stoppageOtherWrist)-.5*n.length()}startReplay(e){let t=new Ru(e.snapshots.length+2,this.simulation.tick_rate);for(let n of e.snapshots)t.push(n);this.replay={plan:e,buffer:t,startedAt:this.frameSeconds,impactFired:!1,side:null},this.drawnOffsetsLive=!1,this.commentary.replay(this.frameSeconds),this.replayFollow=0,this.replayFollowAt=0;for(let e of[0,1]){let t=this.arcadeInjuries[e],n=this.arcadeInjuryEvents[e];t!==null&&n!==null&&KR(t)&&(this.replayInjuries[e]={injury:t,event:n},this.restoreInjury(e))}for(let t of this.pendingContacts.splice(0)){let n=t.recipientIndex;t.injury!==null&&(n===0||n===1)&&this.replayInjuries[n]===null&&this.arcadeInjuries[n]===null&&(this.replayInjuries[n]={injury:t.injury,event:t.event}),t.event.event_id!==e.impact.event_id&&(t.injury=null,t.contactTick=1/0,this.pendingContacts.push(t))}let n=e.snapshots[0]?.fighters.findIndex(t=>t.player_id===e.impact.target_id)??-1;n>=0&&e.impact.detail.endsWith(`:head`)&&this.effects.returnMouthpiece(n);let r=this.lastKnockdown?.knockdown.detail===lL;for(let[t,i]of(this.graphs??[]).entries()){let a=this.liveFallTicks[t]??null;i.resetTransient(!1),a!==null&&a>=e.impact.tick-1&&i.primeReplayFall(),t===n&&r&&i.fallToKnee(!0)}this.finalRevealAt=this.frameSeconds+e.durationSeconds+sn(this.final)}endReplay(){this.replay=null,this.drawnOffsetsLive=!1,this.reapplyReplayInjuries();let e=this.buffer.latest();for(let[t,n]of(this.graphs??[]).entries()){let r=e?.fighters[t],i=r!==void 0&&r.player_id===this.flashKnockout?.loserId;n.resetTransient(r?.is_downed===!0||i),i&&n.knockOut()}this.final!==null&&this.presentFinish(this.final)}fireReplayImpact(e){let t=this.lastKnockdown;if(t===null)return;let n=t.hit??{...t.knockdown,kind:`hit`,amount:420,blood:60},{recipientIndex:r,puncherIndex:i}=zI(n,e),a=e.fighters[r];if(a===void 0)return;iR(this.graphs?.[r],a,this.mapping,this.contactPoint);let o=BI(e.fighters[i],a,this.mapping);this.effects.addEvent(n,this.contactPoint,this.settings().reducedMotion,o);let s=i>=0?e.fighters[i]:void 0,c=s?.action_key?.split(`:`)??[],l=c[0]??null,u=c[1]??null;this.graphs?.[r]?.react(`hit`,n.detail.endsWith(`:body`)?`body`:`head`,n.direction,l,u,n.amount),t.knockdown.detail===`body`&&t.knockdown.tick>n.tick&&this.graphs?.[r]?.windedFor((t.knockdown.tick-n.tick)/this.simulation.tick_rate+.15,WL(s?.action_key??null)),a.player_id===this.flashKnockout?.loserId&&this.graphs?.[r]?.knockOut(),i>=0&&this.graphs?.[i]?.landedHit(!1),n.detail.endsWith(`:head`)&&!this.settings().reducedMotion&&this.knockOutMouthpiece(r,o??n.direction,n.event_id+ER,!0),this.onContact?.(n),this.reapplyReplayInjuries(o);for(let t of this.pendingContacts)t.contactTick===1/0&&(t.contactTick=this.replay?.plan.impact.tick??e.tick)}replayFrame(e,t){let[n,r]=e.fighters,i=this.mapping.x(n.x),a=this.mapping.z(n.y),o=this.mapping.x(r.x),s=this.mapping.z(r.y),c=e.fighters.findIndex(e=>e.player_id===this.replay?.plan.impact.target_id),l=this.replay?.impactFired===!0&&c>=0&&this.headCacheValid[c]===!0,u=1-Math.exp(-3*Math.max(0,t-this.replayFollowAt));this.replayFollowAt=t,this.replayFollow+=(+!!l-this.replayFollow)*u;let d=c<0?0:Math.max(this.portraitPull>1.3?.7:0,this.replayFollow*.8),f=l?this.headCache[c].x:c===1?o:i,p=l?this.headCache[c].z:c===1?s:a,m=(i+o)/2+(f-(i+o)/2)*d,h=(a+s)/2+(p-(a+s)/2)*d,g=-(s-a),_=o-i,v=Math.hypot(g,_)||1;g/=v,_/=v;let y=2.5,b=this.replay;b!==null&&b.side===null&&(b.side=OR(m,h,g,_,y,sL));let x=b?.side??(_<0?-1:1);g*=x,_*=x;let S=.35*Math.sin(t*.7),C=g*Math.cos(S)-_*Math.sin(S),w=g*Math.sin(S)+_*Math.cos(S);this.replayCameraPosition.set(K.clamp(m+C*y,-2.2,sL),1.38-Math.min(.25,t*.05),K.clamp(h+w*y,-2.2,sL));let T=Math.hypot(this.replayCameraPosition.x-m,this.replayCameraPosition.z-h),E=Math.hypot(m,h);if(T.01){let e=DR-T;this.replayCameraPosition.x=K.clamp(this.replayCameraPosition.x-m/E*e,-2.2,sL),this.replayCameraPosition.z=K.clamp(this.replayCameraPosition.z-h/E*e,-2.2,sL)}let D=l?K.clamp(this.headCache[c].y,.45,1.28):1.28;return this.replayLookAt.set(m,1.28+(D-1.28)*this.replayFollow,h),{position:this.replayCameraPosition,lookAt:this.replayLookAt,tight:!0}}setInputLatency(e){this.inputLatencyMs=e,this.lagWarned=ln(e,this.lagWarned)}resyncClock(){this.buffer.resync()}setReconnect(e){this.reconnectMs=e}setBloodLevel(e){e!==`full`&&this.bloodLevel===`full`&&this.restoreAllInjuries(),this.bloodLevel=e,e===`off`&&this.downedPoolAccumulators.fill(0),this.effects.setBloodLevel(e)}setReducedMotion(e){e&&(this.restoreAllInjuries(),this.effects.clearDynamic())}syncInjuryPresentation(e){let t=this.arcadeInjuries[e],n=this.graphs;if(n!==null){let r=n[e];t===`head_burst`?r.boxer.setHeadBurst(!0):r.boxer.setDecapitated(t===`decapitation`),r.boxer.headInjury.setEyeOut(t===`eye_left`?`left`:t===`eye_right`?`right`:null),r.boxer.setHandDismembered(`left`,t===`dismember_left`),r.boxer.setHandDismembered(`right`,t===`dismember_right`);let i=t===`jaw_dislocation`?`jaw`:t===`shoulder_left`||t===`shoulder_right`?t:null;r.setArcadeDislocation(i)}}applyArcadeInjury(e,t,n,r){let i=this.graphs!==null&&(t===`jaw_dislocation`||t===`shoulder_left`||t===`shoulder_right`);if(t===`decapitation`){let t=this.headWorldPose(e);if(t!==null){let a=this.graphs?.[e],o=a===void 0?void 0:xR(a.boxer,t.position,t.quaternion);this.effects.decapitate(e,t.position,t.quaternion,r??n.direction,n.event_id,this.skinColor(e),o);let s=this.stumpWorldPose(e);s!==null&&this.effects.anchorStump(e,s.position,s.quaternion,s.rim,s.across),i=!0}}else if(t===`head_burst`){let t=this.headWorldPose(e);if(t!==null){this.effects.burstHead(e,t.position,r??n.direction,n.event_id);let a=this.burstStumpPose(e);a!==null&&this.effects.anchorStump(e,a.position,a.quaternion,a.rim,a.across),i=!0}}else if(t===`eye_left`||t===`eye_right`){let a=this.graphs?.[e]?.boxer;a!==void 0&&YR(a,t===`eye_left`?`left`:`right`,this.tmpPart,this.tmpStumpOffset,this.tmpPartQuaternion)&&(this.effects.gougeEye(e,this.tmpPart,this.tmpStumpOffset,r??n.direction,n.event_id),i=!0)}else if(t===`dismember_left`||t===`dismember_right`){let a=t===`dismember_left`?`left`:`right`,o=this.handWorldPose(e,a);if(o!==null){let t=this.graphs?.[e],s=t===void 0?void 0:{...bR(t.boxer.gloveMesh(a),o.position,o.quaternion),gloveBlood:t.boxer.gloveBloodLevel};this.effects.dismemberHand(e,a,o.position,o.quaternion,r??n.direction,n.event_id,this.gearColor(e),s),this.effects.anchorHandStump(e,a,o.position,o.quaternion),i=!0}}else if(t===`ribs_left`||t===`ribs_right`){let t=this.graphs?.[e],a=this.buffer.latest()?.fighters[e];if(t!==void 0&&a!==void 0){if(t.boxer.rig.bones.upperChest.getWorldPosition(this.tmpPart).y>UR){iR(t,a,this.mapping,this.contactPoint);let e={...n,event_id:n.event_id+WR,kind:`counter_hit`,detail:`hook:body`,amount:Math.max(n.amount,95),blood:100};this.effects.addEvent(e,this.contactPoint,!1,r)}i=!0}}return i?(this.arcadeInjuries[e]=t,this.arcadeInjuryEvents[e]=n,this.observedInjuryDown[e]=!1,this.syncInjuryPresentation(e),this.onArcadeInjury?.(t,n),!0):!1}reapplyReplayInjuries(e){for(let t of[0,1]){let n=this.replayInjuries[t];if(n===null)continue;this.replayInjuries[t]=null;let r=this.settings();r.blood!==`full`||r.reducedMotion||this.applyArcadeInjury(t,n.injury,{...n.event,event_id:n.event.event_id+ER},e)}}restoreInjury(e){this.arcadeInjuries[e]!==null&&(this.arcadeInjuries[e]=null,this.arcadeInjuryEvents[e]=null,this.observedInjuryDown[e]=!1,this.syncInjuryPresentation(e),this.effects.restoreFighter(e))}restoreAllInjuries(){this.restoreInjury(0),this.restoreInjury(1),this.effects.clearArcadeGore()}headWorldPose(e){let t=this.graphs,n=t===null?null:t[e].boxer.bone(`head`);return n===null||t===null?null:(t[e].boxer.root.updateMatrixWorld(!0),n.getWorldPosition(this.tmpHead),n.getWorldQuaternion(this.tmpHeadQuaternion),this.tmpStumpOffset.copy(XL).applyQuaternion(this.tmpHeadQuaternion),this.tmpHead.add(this.tmpStumpOffset),{position:this.tmpHead,quaternion:this.tmpHeadQuaternion})}handWorldPose(e,t){let n=this.graphs,r=n===null?null:n[e].boxer.bone(t===`left`?`gloveL`:`gloveR`);return r===null||n===null?null:(n[e].boxer.root.updateMatrixWorld(!0),r.getWorldPosition(this.tmpPart),r.getWorldQuaternion(this.tmpPartQuaternion),{position:this.tmpPart,quaternion:this.tmpPartQuaternion})}gearColor(e){return this.graphs?.[e]?.boxer.gearBaseColor.getHex()??1920728}skinColor(e){return this.graphs?.[e]?.boxer.skinBaseColor.getHex()??11563071}stumpWorldPose(e){let t=this.graphs?.[e]?.boxer,n=t?.bone(`head`);if(t===void 0||n==null)return null;t.root.updateMatrixWorld(!0);let r=t.headMesh,i=$L(r.geometry);if(i.length===0)return null;this.stumpRim.length!==i.length*3&&(this.stumpRim=new Float32Array(i.length*3)),this.tmpStump.set(0,0,0);for(let[e,n]of i.entries())fR(t,n,this.tmpStumpOffset),this.tmpStumpOffset.toArray(this.stumpRim,e*3),this.tmpStump.add(this.tmpStumpOffset);return this.tmpStump.multiplyScalar(1/i.length),n.getWorldPosition(this.tmpStumpOffset).sub(this.tmpStump).normalize(),this.tmpStumpAcross.set(1,0,0).applyQuaternion(n.getWorldQuaternion(this.tmpStumpQuaternion)),this.tmpStumpQuaternion.setFromUnitVectors(ZL,this.tmpStumpOffset),{position:this.tmpStump,quaternion:this.tmpStumpQuaternion,rim:this.stumpRim,across:this.tmpStumpAcross}}burstStumpPose(e){let t=this.graphs?.[e]?.boxer;if(t===void 0)return null;let n=rR(t,this.stumpRim,this.burstStump);return n===null?null:(this.stumpRim=n,{position:this.burstStump.position,quaternion:this.burstStump.quaternion,rim:n,across:this.burstStump.across})}predictAction(e){let t=this.buffer.latest();if(t===null||this.viewerId===null||this.replay!==null)return;let n=t.fighters.findIndex(e=>e.player_id===this.viewerId);if(n<0)return;let r=t.fighters[n];if(t.phase!==`fight`||!kR(r))return;let i=(this.inputLatencyMs??60)/1e3*this.simulation.tick_rate+this.buffer.interpolationDelayTicks;if(pw(e.kind)){!(this.graphs?.[n]?.ownPunchActive===!0||nw(r,t.tick)>0||r.queued_actions>0)&&e.id!==void 0&&r.stamina>=25&&this.evasion.press(e.kind,e.id,this.manualClock?this.lastManualTime:performance.now(),i,this.simulation.tick_rate,10+oe(r.style).evasionTicks);return}e.kind===`punch`&&this.graphs?.[n]?.predict(e,performance.now()/1e3,this.simulation.tick_rate,i,tw(r,e,t.tick+1))}acknowledgeActions(e,t){for(let[n,r]of e.fighters.entries()){let i=r.player_id===this.viewerId;i&&this.evasion.acknowledge(r,e.phase===`fight`,this.inputSequenceOf);let a=t.filter(t=>zI(t,e).puncherIndex===n);this.graphs?.[n]?.acknowledge(r,e.phase===`fight`,a,i?this.inputSequenceOf:null)}}push(e){if(!this.buffer.push(e,this.manualClock?this.lastManualTime:performance.now()))return;let t=this.dedupe.accept(e.events);this.acknowledgeActions(e,t),this.history.push(e),this.history.length>UI&&this.history.shift();for(let n of t){if(this.roundStats.record(n,t),n.kind===`knockdown`){let r=t.find(e=>VI(e)&&e.target_id===n.target_id&&e.tick===n.tick)??(n.detail===`body`?this.recordedHit(n):null);this.lastKnockdown={knockdown:n,hit:r,finisher:r!==null&&en(r,e.events)?null:AR(r,e)};let i=e.fighters.findIndex(e=>e.player_id===n.target_id);this.graphs?.[i]?.fallToKnee(n.detail===lL)}if(n.kind===`result`&&n.detail===`flash_ko`){let e=t.findLast(e=>(e.kind===`hit`||e.kind===`counter_hit`)&&e.actor_id===n.actor_id)??null;e!==null&&e.target_id!==null&&(this.flashKnockout={hitEventId:e.event_id,loserId:e.target_id},this.lastKnockdown={knockdown:{...e,kind:`knockdown`},hit:e,finisher:null})}n.kind===`referee_break`&&this.referee?.breakClinch(),n.kind===`box`&&(this.eventCallout={text:`BOX!`,until:this.frameSeconds+LL}),n.kind===`body_collapse`&&n.target_id===this.viewerId&&(this.ownCollapseUntil=n.tick+n.amount),this.movement.observe(n,this.viewerId)}this.commentary.observe(e,t,this.players,this.simulation.tick_rate,this.frameSeconds);for(let{event:n,presentationEvent:r,presentImpact:i,reactAmount:a}of HL(t,e)){let t=e.fighters.findIndex(e=>e.player_id===n.target_id),o=e.fighters.findIndex(e=>e.player_id===n.actor_id),{recipientIndex:s,puncherIndex:c}=zI(n,e),l=e.fighters[s]??e.fighters[t]??e.fighters[o]??e.fighters[0];if(iR(this.graphs?.[e.fighters.indexOf(l)],l,this.mapping,this.contactPoint),FL.has(n.kind)){let t=c>=0?e.fighters[c]:null;this.pendingContacts.push({event:n,presentationEvent:r,presentImpact:i,reactAmount:a,contactTick:t?.action_contact_tick??n.tick,recipientIndex:s,puncherIndex:c,injury:JL(n,e.fighters[s],e.result,t??void 0)})}else n.kind===`bleed`&&this.effects.addEvent(n,this.contactPoint,this.settings().reducedMotion)}}fireContacts(e){for(let t=0;te){t+=1;continue}this.pendingContacts.splice(t,1);let{event:r,presentationEvent:i,reactAmount:a,recipientIndex:o,puncherIndex:s,injury:c}=n,l=n.presentImpact&&!zL(i),u=this.buffer.latest()?.fighters[o],d=BI(this.buffer.latest()?.fighters[s],u,this.mapping);this.presentFightEvent(r,o,s),l&&u!==void 0&&(iR(this.graphs?.[o],u,this.mapping,this.contactPoint),this.effects.addEvent(i,this.contactPoint,this.settings().reducedMotion,d));let f=this.settings();(n.presentImpact||IL.has(r.kind))&&this.arena.excite(PL[r.kind]??0),l&&o>=0&&this.viewerId!==null&&this.buffer.latest()?.fighters[o]?.player_id===this.viewerId&&[`hit`,`counter_hit`,`knockdown`].includes(r.kind)&&(this.viewerHitFlash=Math.max(this.viewerHitFlash,Math.min(1,.35+r.amount/400))),c!==null&&o>=0&&this.arcadeInjuries[o]===null&&f.blood===`full`&&!f.reducedMotion&&this.applyArcadeInjury(o,c,r,d);let p=this.graphs;l&&o>=0&&!f.reducedMotion&&zD(r.kind,r.amount,i.detail.endsWith(`:head`))&&this.knockOutMouthpiece(o,d??i.direction,r.event_id,!1);let m=l&&o>=0?_E(r.kind,r.amount,i.detail.endsWith(`:head`)):0;if(m>0&&!f.reducedMotion&&f.blood!==`off`){let e=this.headWorldPose(o);e!==null&&(this.mouthPoint.set(0,-.07,.1).applyQuaternion(e.quaternion).add(e.position),this.effects.spawnTeeth(this.mouthPoint,d??i.direction,m,r.event_id))}if(l&&o>=0&&a!==null&&[`hit`,`counter_hit`,`block`,`perfect_block`,`knockdown`].includes(r.kind)){let e=r.kind===`block`||r.kind===`perfect_block`,t=i.detail.endsWith(`:body`)?`body`:`head`;if(p!==null){let n=(s>=0?this.buffer.latest()?.fighters[s]:void 0)?.action_key?.split(`:`)??[],c=n[0]??i.detail.split(`:`)[0]??null,l=n[1]??null;p[o].react(e?`block`:`hit`,t,i.direction,c,l,a),this.flashKnockout?.hitEventId===r.event_id&&p[o].knockOut()}}l&&s>=0&&[`hit`,`counter_hit`,`block`,`perfect_block`,`guard_break`].includes(r.kind)&&p?.[s]?.landedHit(r.kind===`block`||r.kind===`perfect_block`),(n.presentImpact||!VI(r))&&this.onContact?.(r)}}presentFightEvent(e,t,n){let r=RL[e.kind];r!==void 0&&(this.eventCallout={text:r.text,until:this.frameSeconds+r.seconds});let i=this.graphs;if(!(i===null||t<0)&&(e.kind===`parry`&&i[t]?.stagger(),e.kind===`body_collapse`)){let r=n>=0?this.buffer.latest()?.fighters[n]:void 0;i[t]?.windedFor(e.amount/this.simulation.tick_rate+.15,WL(r?.action_key??null))}}recordedHit(e){if(e.action_id===null)return null;for(let t=this.history.length-1;t>=0;--t){let n=this.history[t].events.find(t=>(t.kind===`hit`||t.kind===`counter_hit`)&&t.action_id===e.action_id&&t.target_id===e.target_id);if(n!==void 0)return n}return null}enterRest(e){this.restStartedAt=e,this.effects.clearMouthpieces()}knockOutMouthpiece(e,t,n,r){let i=this.headWorldPose(e);i!==null&&(this.mouthPoint.set(0,-.075,.1).applyQuaternion(i.quaternion).add(i.position),this.effects.ejectMouthpiece(e,this.mouthPoint,i.quaternion,t,n,this.gearColor(e),r))}get resultVisible(){return this.final!==null&&this.frameSeconds>=this.finalRevealAt}get resolutionScale(){return this.scaler.scale}get diagnostics(){let e=this.renderer.getContext(),t=e.getExtension(`WEBGL_debug_renderer_info`),n=t===null?e.getParameter(e.RENDERER):e.getParameter(t.UNMASKED_RENDERER_WEBGL);return{frameMs:this.frameMsAverage,resolutionScale:this.scaler.scale,gpu:this.memoryInfo,graphics:String(n)}}get memoryInfo(){return{geometries:this.renderer.info.memory.geometries,textures:this.renderer.info.memory.textures,programs:this.renderer.info.programs?.length??0}}applyResolutionScale(){let e=this.basePixelRatio*this.scaler.scale;this.renderer.setPixelRatio(e),this.composer.setPixelRatio(e);let t=this.scaler.scale<=WI;this.bloomPass.enabled=!t,this.keyLight!==null&&RI(this.keyLight.shadow,t,this.scaler.scale<.8?1024:2048),this.arena.setLowTier(t),this.effects.setLowTier(t)}setupLights(){let e=new by(`#3c4a72`,`#07080c`,.55);this.scene.add(e),this.lights.push(e);let t=new Py(`#fff1dc`,150,24,.56,.55,1.7);t.position.set(.6,7.4,1.2),t.target.position.set(0,.6,0),t.castShadow=!0,t.shadow.mapSize.set(2048,2048),t.shadow.bias=II,t.shadow.camera.near=3,t.shadow.camera.far=14,this.keyLight=t,this.scene.add(t,t.target),this.lights.push(t);let n=new Py(`#ffe6c8`,95,24,.6,.6,1.7);n.position.set(-2.6,7,-2.2),n.target.position.set(0,.8,0),this.scene.add(n,n.target),this.lights.push(n);for(let[e,t,n,r]of[[`#8fa8ff`,-5.5,3.8,-5.4],[`#7f95e8`,5.6,3.6,-5.6]]){let i=new Py(e,34,30,.72,.9,1.8);i.position.set(t,n,r),i.target.position.set(0,1.2,0),this.scene.add(i,i.target),this.lights.push(i)}let r=new Py(`#ffe8d0`,48,22,.32,.8,1.5);r.position.set(0,5.8,5.4),r.target.position.set(0,1,0),this.scene.add(r,r.target),this.lights.push(r),this.followSpot=r}draw(e,t=!1,n=!0){if(this.destroyed)return;let r=e-this.previous,i=t?1/60:Math.min(.05,Math.max(.001,r/1e3));this.previous=e,!t&&this.scaler.record(r)&&this.applyResolutionScale(),!t&&r>0&&r<1e3&&(this.frameMsAverage+=(r-this.frameMsAverage)*.05);let a=Math.min(this.pixelRatioCap,window.devicePixelRatio||1);a!==this.basePixelRatio&&(this.basePixelRatio=a,this.applyResolutionScale());let o=this.canvas.clientWidth,s=this.canvas.clientHeight;o>0&&s>0&&(this.renderer.getSize(this.sizeCheck),(this.sizeCheck.x!==o||this.sizeCheck.y!==s)&&(this.renderer.setSize(o,s,!1),this.composer.setSize(o,s),this.camera.aspect=o/s,this.portraitPull=K.clamp(1.2/this.camera.aspect,1,2.2),this.baseFov=36*Math.min(1.3,Math.sqrt(this.portraitPull)),this.camera.fov=this.baseFov,this.camera.updateProjectionMatrix()));let c=e/1e3;this.frameSeconds=c,this.finishPass.uniforms.uTime.value=c%1e3,t&&(this.lastManualTime=e);let l=this.settings();this.setBloodLevel(l.blood);let u=this.buffer.latest(),d=i,f=u?.result!==null&&u?.result!==void 0&&rn.has(u.result.finish_method);if(this.cheer(c,i),f&&!this.finishSeen&&(this.finishSeen=!0,this.finishSlowMotion=2.2,this.arena.excite(1)),this.finishSlowMotion>0&&!l.reducedMotion){this.finishSlowMotion=Math.max(0,this.finishSlowMotion-i);let e=Math.min(1,this.finishSlowMotion/.5);i*=.3+.7*(1-e)}this.viewerHitFlash=Math.max(0,this.viewerHitFlash-i*3.2);let p=u===null?0:t?GL(u):this.buffer.renderTick(e),m=this.updateRocked(u===null?null:this.shownAt(p),d,l.reducedMotion);m!==null&&u!==null&&this.onRocked?.(m,u.tick);let h=u===null?null:this.applyLocalPrediction(this.buffer.sample(p),i,e),g=this.replay;if(g!==null){let e=c-g.startedAt;e>=g.plan.durationSeconds?this.endReplay():(p=TI(g.plan,e,this.simulation.tick_rate),h=g.buffer.sample(p)??h,!g.impactFired&&p>=g.plan.impact.tick&&h!==null&&(g.impactFired=!0,this.fireReplayImpact(h)))}let _=this.replay===null?i:i*this.replay.plan.speed,v=1.8,y=!1,b=null;if(h!==null){let e=this.replay===null?this.ceremony:null,[t,n]=e===null?this.standApart(h.fighters,_):this.ceremonyFighters(e,h.fighters,_,c,this.drawnOffsets);for(let[e,t]of h.fighters.entries())this.arcadeInjuries[e]===null||this.replay!==null||(t.is_downed?this.observedInjuryDown[e]=!0:this.observedInjuryDown[e]&&h.result===null&&this.restoreInjury(e));let r=this.graphs;if(r!==null){this.wearLook(0,t.player_id),this.wearLook(1,n.player_id);let e=this.headCacheValid[0]?this.headCache[0]:void 0,i=this.headCacheValid[1]?this.headCache[1]:void 0;r[0].boxer.root.visible=!0,r[1].boxer.root.visible=!0;let a=MI(h,this.simulation.tick_rate);r[0].setResting(a),r[1].setResting(a);let o=h.phase===`countdown`?h.phase_ticks_remaining:null;r[0].setCountdown(o),r[1].setCountdown(o);let s=this.replay!==null||h.phase===`complete`;r[0].stayDown(s),r[1].stayDown(s),h.phase===`fight`&&this.lastPhase===`rest`&&this.replay===null&&(this.roundCalloutUntil=c+GI,this.roundCalloutRound=h.round_number),h.phase===`rest`&&this.lastPhase!==`rest`&&this.enterRest(c),this.lastPhase=h.phase,this.anticipatePunches(u,p),r[0].setObstacle(r[1].boxer.root.position.x,r[1].boxer.root.position.z),r[1].setObstacle(r[0].boxer.root.position.x,r[0].boxer.root.position.z),r[0].update(t,n,_,c,l.reducedMotion,l.blood,p,i),r[1].update(n,t,_,c,l.reducedMotion,l.blood,p,e);for(let[e,t]of r.entries()){let n=t.boxer.bone(`head`);n!==null&&(t.boxer.root.updateMatrixWorld(!0),n.getWorldPosition(this.headCache[e]),this.headCacheValid[e]=!0);let r=t.fallBody!==null;r&&!this.fallingLive[e]&&this.replay===null&&(this.liveFallTicks[e]=p),this.fallingLive[e]=r}}for(let e=0;ee.player_id===this.viewerId),C=l.camera===`fighter`&&S>=0&&jI(h)?this.fighterCam.update(i,c,S===0?this.tmpA:this.tmpB,S===0?this.tmpB:this.tmpA,this.effects.shakeAmount,l.reducedMotion,this.camera.aspect):null;C===null&&this.fighterCam.reset();let w=this.replay,T=this.cameraOverride??(w!==null&&h!==null?this.replayFrame(h,c-w.startedAt):this.closeUpFrame(c)??this.raiseFrame(c,i,l.reducedMotion)??this.cornerShotFrame(c,h,l.reducedMotion)??this.restWideFrame(h)??this.ceremonyFrame(c)??C??x);this.ownViewActive=C!==null&&T===C;let E=this.ownViewActive?this.baseFov*AT:this.baseFov;if(this.camera.fov!==E&&(this.camera.fov=E,this.camera.updateProjectionMatrix()),this.cameraOverride===null&&this.portraitPull>1&&T.framed!==!0&&!this.ownViewActive){let e=T.tight===!0,t=this.portraitPull/Math.min(1.3,Math.sqrt(this.portraitPull)),n=e?Math.min(t,1.25):t;this.tmpCamera.subVectors(T.position,T.lookAt).multiplyScalar(n),this.camera.position.copy(T.lookAt).add(this.tmpCamera),e&&(this.camera.position.x=K.clamp(this.camera.position.x,-2.2,sL),this.camera.position.z=K.clamp(this.camera.position.z,-2.2,sL)),this.camera.lookAt(T.lookAt.x,T.lookAt.y-(this.portraitPull-1)*(e?.08:.45),T.lookAt.z)}else this.camera.position.copy(T.position),this.camera.lookAt(T.lookAt);this.effects.setViewDistance(this.camera.position.distanceTo(T.lookAt)),this.arena.makeRoomForCamera(this.camera.position);let D=T===x?ON(Math.max(this.tmpA.z,this.tmpB.z)):T.framed===!0?EL:1;this.ring.setNearRopeOpacity(this.ring.nearRopeOpacity()+(D-this.ring.nearRopeOpacity())*(1-Math.exp(-6*i))),n&&(this.composer.render(),this.drawHudOverlay(h)),!this.destroyed&&!this.manualClock&&(this.raf=requestAnimationFrame(e=>this.draw(e)))}applyLocalPrediction(e,t,n){if(e===null||this.localInput===null||this.viewerId===null)return e;let r=e.fighters.findIndex(e=>e.player_id===this.viewerId),i=this.localInput()??{moveX:0,moveY:0,defense:`none`},a=(this.inputLatencyMs??60)/1e3*this.simulation.tick_rate+this.buffer.interpolationDelayTicks,o=this.ownCollapseUntil>e.tick,s=r>=0&&e.phase===`fight`&&!o?e.fighters[r]:null,c=this.graphs?.[r]?.ownPunchActive===!0||this.evasion.holdsFeet(n,this.simulation.tick_rate),l=this.movement.update(s,i,c,n,a,e.tick,t,this.simulation.tick_rate),u=this.evasion.pose(n);if(r<0)return e;let d=e.fighters[r],f=s===null?d.defense:mw(s,i,u);if(Math.abs(l.dx)<.01&&Math.abs(l.dy)<.01&&f===d.defense)return e;let p=fw(d,l,e.fighters[+(r===0)]??null),m={...d,x:d.x+p.dx,y:d.y+p.dy,defense:f},h=r===0?[m,e.fighters[1]]:[e.fighters[0],m];return{...e,fighters:h}}anticipatePunches(e,t){if(!(this.replay!==null||e===null||e.phase!==`fight`))for(let[n,r]of(this.graphs??[]).entries()){let i=e.fighters[n];i!==void 0&&!i.is_downed&&r.anticipate(i,i.action_start_tick-t)}}cheer(e,t){ee.clinch_ticks>0||e.clinch_startup_ticks>0||e.is_downed)?null:lR(n.x,n.y,r.x,r.y,mL,c-u,l-u,m),[a,o]=this.drawnTargets;a.x=i===null?0:i.ax-n.x,a.y=i===null?0:i.ay-n.y,o.x=i===null?0:i.bx-r.x,o.y=i===null?0:i.by-r.y,this.clearOfTheFallen(e);let s=1-Math.exp(-14*t);for(let e of[0,1]){let n=this.drawnOffsets[e],r=this.drawnTargets[e],i=r.x-n.x,a=r.y-n.y;if(this.drawnOffsetsLive){i*=s,a*=s;let e=Math.hypot(i,a);e>hL*t&&(i*=hL*t/e,a*=hL*t/e)}n.x+=i,n.y+=a}return this.drawnOffsetsLive=!0,[this.drawnAt(0,n),this.drawnAt(1,r)]}drawnAt(e,t){let n=this.drawnOffsets[e];if(Math.abs(n.x)<.01&&Math.abs(n.y)<.01)return t;let r=dw(t.x+n.x,t.y+n.y);return Object.assign(this.drawnFighters[e],t,{x:r.x,y:r.y})}clearOfTheFallen(e){for(let t of[0,1]){let n=this.graphs?.[t]?.fallBody??null,r=+(t===0),i=e[r];if(n===null||i.is_downed||(this.graphs?.[r]?.fallBody??null)!==null)continue;let a=this.drawnTargets[r],o=this.mapping.x(i.x+a.x),s=this.mapping.z(i.y+a.y),c=this.clearOf(n,o,s);a.x+=(c.x-o)/this.mapping.x(1),a.y+=(c.z-s)/this.mapping.z(1);return}}clearOf(e,t,n){let r={x:t,z:n};for(let t of[0,1,2,3,4])e.bodyPoint(t,this.bodyPoint),r=aR(r.x,r.z,this.bodyPoint.x,this.bodyPoint.z,dL);let i=this.insideRopes(r.x,r.z);if(Math.hypot(i.x-r.x,i.z-r.z)<1e-6)return r;let a=t=>[0,1,2,3,4].every(n=>(e.bodyPoint(n,this.bodyPoint),Math.hypot(t.x-this.bodyPoint.x,t.z-this.bodyPoint.z)>=dL-1e-6));for(let e=gL;e<=2.000000001;e+=gL)for(let[t,n]of[[1,0],[-1,0],[0,1],[0,-1]]){let r=this.insideRopes(i.x+t*e,i.z+n*e);if(a(r))return r}return i}insideRopes(e,t){let n=dw(e/this.mapping.x(1),t/this.mapping.z(1));return{x:this.mapping.x(n.x),z:this.mapping.z(n.y)}}headHeightOf(e){return(this.graphs?.[e]?.boxer.metrics.headRestY??1.52)+.04}bleed(e,t,n){for(let[r,i]of e.entries()){let e=BR(i,this.graphs?.[r],this.flashKnockout?.loserId),a=this.arcadeInjuries[r]??null,o=a!==null&&PR.has(a),s=(i.trauma.bleeding+i.trauma.left_cut+i.trauma.right_cut)/380,c=o?Math.max(s,FR):s;if(c>.05&&!e)this.downedPoolAccumulators[r]=0,this.downedPoolCounts[r]=0,this.effects.drip(this.dripPoint(r),c,n.reducedMotion,r);else if(c>jR&&e&&n.blood!==`off`){this.effects.stopDrip(r),this.downedPoolAccumulators[r]+=t*MR*(o?IR:1);let e=this.poolSource(r,a);for(;this.downedPoolAccumulators[r]>=1;){--this.downedPoolAccumulators[r];let t=this.downedPoolCounts[r];this.downedPoolCounts[r]=t+1;let n=zR(t,c,o?LR:NR),i=t*2.399963+r*Math.PI;this.effects.pool(e.x+Math.sin(i)*n*.25,e.z+Math.cos(i)*n*.25,n,t+r*7)}}else this.effects.stopDrip(r),this.downedPoolAccumulators[r]=0,e||(this.downedPoolCounts[r]=0)}}dripPoint(e){let t=this.headWorldPose(e);if(t!==null)return t.position.add(this.tmpStumpOffset.copy(RR).applyQuaternion(t.quaternion));if(this.headCacheValid[e])return this.tmpHead.copy(this.headCache[e]);let n=e===0?this.tmpA:this.tmpB;return this.tmpHead.set(n.x,this.headHeightOf(e),n.z)}poolSource(e,t){if(t===`dismember_left`||t===`dismember_right`){let n=this.handWorldPose(e,t===`dismember_left`?`left`:`right`);if(n!==null)return n.position}return this.headCacheValid[e]?this.headCache[e]:e===0?this.tmpA:this.tmpB}updateBlobShadows(){let e=[this.tmpA,this.tmpB,this.refereePosition];for(let[t,n]of this.blobShadows.entries()){let r=t<2?this.graphs?.[t]:void 0,i=r?.fallBody?.centre(this.bodyPoint)??null??(r?.boxer.root.visible===!0?r.currentRoot:e[t]);n.position.x=i.x,n.position.z=i.z;let a=t<2?this.buffer.latest()?.fighters[t]:void 0,o=a!==void 0&&BR(a,r,this.flashKnockout?.loserId);n.scale.set(o?2.1:1.25,o?.9:.85,1)}}updateReferee(e,t,n,r){let i=this.referee;if(i===null)return;i.boxer.root.visible=this.replay===null;let a=this.blobShadows[2];a!==void 0&&(a.visible=this.replay===null);let o=n?.fighters.find(e=>e.is_downed)??(n?.phase===`knockdown`?n.fighters.find(e=>e.player_id===this.lastKnockdown?.knockdown.target_id):void 0)??null,s=n?.fighters.some(e=>e.clinch_ticks>0||e.clinch_startup_ticks>0)??!1,c=o===null&&i.breaking,l=o===null?void 0:this.graphs?.[n?.fighters.indexOf(o)??-1],u=l?.fallBody?.centre(this.bodyPoint)??null,d=o!==null&&!o.is_downed?l?.currentRoot??null:null,f=o===null?(this.tmpA.x+this.tmpB.x)/2:u?.x??d?.x??this.mapping.x(o.x),p=o===null?(this.tmpA.z+this.tmpB.z)/2:u?.z??d?.z??this.mapping.z(o.y),m=this.refereeAway.set(this.refereePosition.x-f,0,this.refereePosition.z-p);if(m.lengthSq()<.01&&m.set(0,0,-1),m.normalize(),c){let e=this.tmpB.z-this.tmpA.z,t=this.tmpA.x-this.tmpB.x,n=Math.hypot(e,t),r=e*m.x+t*m.z>=0?1:-1;n>.01&&m.set(e/n*r,0,t/n*r)}let{standoff:h,clearance:g}=XR(o!==null,s,c),_=K.clamp(f+m.x*h,-2.4,2.4),v=K.clamp(p+m.z*h,-2.4,2.4),y=this.refereePosition.x,b=this.refereePosition.z,x=this.replay===null&&this.ceremony?.positions!==null?this.ceremony:null,S=x===null&&this.replay===null&&this.stoppageWinner>=0&&this.frameSeconds>=this.stoppageRaiseAt?this.stoppageWinner===0?this.tmpA:this.tmpB:null,C=S===null?null:this.stoppageSpot??=this.raiseSpot(S,this.stoppageWinner===0?this.tmpB:this.tmpA),w=C!==null&&C.x<0?-1:1;if(S!==null&&C!==null){let t=jL(this.refereePosition.x,this.refereePosition.z,{x:S.x+C.x,y:S.z+C.z},OL*e);this.refereePosition.x=t.x,this.refereePosition.z=t.y}else if(x!==null){let t=jL(this.refereePosition.x,this.refereePosition.z,{x:TL.x,y:TL.z},OL*e);this.refereePosition.x=t.x,this.refereePosition.z=t.y,x.refereeArrived=t.arrived}else{let t=1-Math.exp(-(c?8:1.6)*e),n=c?Math.min(1,vL*e/Math.max(1e-6,Math.hypot(_-this.refereePosition.x,v-this.refereePosition.z)*t)):1;this.refereePosition.x+=(_-this.refereePosition.x)*t*n,this.refereePosition.z+=(v-this.refereePosition.z)*t*n}let T=S===null?x===null?g:x.refereeArrived?0:wL:eL*.85;for(let e of T>0?[this.tmpA,this.tmpB]:[]){let t=this.refereePosition.x-e.x,n=this.refereePosition.z-e.z,r=Math.hypot(t,n);r.001&&(this.refereePosition.x=e.x+t/r*T,this.refereePosition.z=e.z+n/r*T)}for(let e of x===null?this.graphs??[]:[]){let t=e.fallBody;if(t!==null)for(let e of[0,1,2,3,4]){t.bodyPoint(e,this.bodyPoint);let n=aR(this.refereePosition.x,this.refereePosition.z,this.bodyPoint.x,this.bodyPoint.z,uL);this.refereePosition.x=n.x,this.refereePosition.z=n.z}}for(let e of[0,1]){if(!this.effects.severedHeadPosition(e,this.closeUpTarget))continue;let t=aR(this.refereePosition.x,this.refereePosition.z,this.closeUpTarget.x,this.closeUpTarget.z,cL);this.refereePosition.x=t.x,this.refereePosition.z=t.z}e>0&&this.refereeVelocity.set((this.refereePosition.x-y)/e,0,(this.refereePosition.z-b)/e);let E=S!==null&&C!==null&&Math.hypot(this.refereePosition.x-S.x-C.x,this.refereePosition.z-S.z-C.z)<=nL,D=x!==null&&!x.refereeArrived,O=S===null?x===null?Math.atan2(f-this.refereePosition.x,p-this.refereePosition.z):D?Math.atan2(TL.x-this.refereePosition.x,TL.z-this.refereePosition.z):0:E?0:Math.atan2(S.x-this.refereePosition.x,S.z-this.refereePosition.z);if(S!==null){let e=this.graphs?.[this.stoppageWinner]?.boxer.rig.bones,t=E&&e!==void 0?this.nearerGlove(e.gloveL,e.gloveR):null;i.raise(w<0?t:null,w>0?t:null)}let ee=((O-this.refereeYaw+Math.PI)%(Math.PI*2)+Math.PI*2)%(Math.PI*2)-Math.PI;this.refereeYaw+=ee*(1-Math.exp(-(c?8:3)*e));let te=ZR(this.refereePosition,this.refereeYaw,this.refereeVelocity,this.mapping);i.setRefereeCount(o!==null,o?.get_up_count??0),i.aimBreak(this.tmpA,this.tmpB),i.update(te.self,te.focus,e,t,!1,`off`,r)}updateTrails(e,t){let n=this.graphs;if(!(n===null||this.trails.length<4))for(let[r,i]of n.entries())for(let[n,a]of[`gloveL`,`gloveR`].entries()){let o=this.trails[r*2+n];if(!i.boxer.root.visible){o.reset();continue}i.boxer.rig.bones[a].getWorldPosition(this.trailGlove),o.update(this.trailGlove,e,this.camera.position,!t&&this.replay===null&&!i.isDown)}}updateCornermen(e,t,n,r){let i=this.cornermen;if(i===null)return;let a=MI(n,this.simulation.tick_rate);for(let[n,o]of i.entries()){let i=n===0?-1:1,s=a?_L:oL;this.cornermanPosition.set(i*s,0,-i*s);let c=n===0?3*Math.PI/4:-Math.PI/4;o.attend(a);let l=ZR(this.cornermanPosition,c,this.cornermanVelocity,this.mapping);o.update(l.self,l.focus,e,t,!1,`off`,r)}}updateCutmen(e,t,n,r){let i=this.cutmen,a=this.graphs;if(i===null||a===null)return;let o=n?.phase===`rest`,c=this.mapping.x(s);for(let[s,l]of i.entries()){let i=s===0?-1:1,u=o&&a[s].stoolVisible?1:0,d=this.cutmanProgress[s],f=d+Math.sign(u-d)*Math.min(Math.abs(u-d),e/yL);this.cutmanProgress[s]=f;let p=l.boxer.root.visible;if(l.boxer.root.visible=f>.005,!l.boxer.root.visible)continue;this.cutmanFrom.set(i*oL,0,-i*(oL-1)),xT(s===0?0:1,c,vT,32,this.cutmanTo);let m=f*f*(3-2*f);this.cornermanPosition.copy(this.cutmanFrom).lerp(this.cutmanTo,m);let h=this.cutmanLast[s];p||h.copy(this.cornermanPosition),this.cornermanVelocity.copy(this.cornermanPosition).sub(h).divideScalar(Math.max(e,.001)),h.copy(this.cornermanPosition);let g=f>.02&&f=bL?_===null?Math.atan2(i,-i):Math.atan2(_.x-this.cutmanTo.x,_.z-this.cutmanTo.z):Math.atan2(-this.cutmanFrom.x,-this.cutmanFrom.z),y=UL(n?.fighters[s]);if(f>=bL&&_!==null){let e=a[s].boxer.root.rotation.y;this.cutmanFacing.set(Math.sin(e),0,Math.cos(e)),this.cutmanEye.copy(_).addScaledVector(this.cutmanFacing,.09),this.cutmanEye.x+=Math.cos(e)*y.side*y.lateral,this.cutmanEye.z-=Math.sin(e)*y.side*y.lateral,this.cutmanEye.y+=y.lift,l.treat(this.cutmanEye,this.cutmanFacing,y.side,y.prop)}else l.treat(null);let b=ZR(this.cornermanPosition,v,this.cornermanVelocity,this.mapping);l.update(b.self,b.focus,e,t,!1,`off`,r)}}drawHudOverlay(e){let t=AI(this.hudCanvas);if(t===null)return;let{context:n,viewport:r}=t;if(this.hudViewport.width=r.width,this.hudViewport.height=r.height,n.clearRect(0,0,r.width,r.height),e===null)return;if(this.viewerHitFlash>.01&&!this.settings().reducedMotion){let e=n.createRadialGradient(r.width/2,r.height/2,r.width*.12,r.width/2,r.height/2,r.width*.7);e.addColorStop(0,`rgba(255,235,225,${(this.viewerHitFlash*.18).toFixed(3)})`),e.addColorStop(1,`rgba(140,0,10,${(this.viewerHitFlash*.55).toFixed(3)})`),n.fillStyle=e,n.fillRect(0,0,r.width,r.height)}let i=Math.max(...e.fighters.map(e=>e.trauma.head+e.trauma.body));if(i>350){let e=n.createRadialGradient(r.width/2,r.height/2,r.width*.2,r.width/2,r.height/2,r.width*.72);e.addColorStop(0,`rgba(90,0,8,0)`),e.addColorStop(1,`rgba(75,0,8,${Math.min(.3,i/4600)})`),n.fillStyle=e,n.fillRect(0,0,r.width,r.height)}un(n,r.width,r.height,e,this.players,this.viewerId,this.frameSeconds>=this.finalRevealAt?this.final:null,this.reconnectMs,this.simulation.tick_rate,this.roundStats,this.replay===null?null:`KNOCKOUT REPLAY`,this.lagWarned?this.inputLatencyMs:null,this.frameSecondsthis.avatars.get(e),this.lastKnockdown?.knockdown.target_id??null,this.touchControls&&e.fighters.some(e=>e.player_id===this.viewerId)),this.drawCaption(n,r.width,r.height,e)}updateRocked(e,t,n){let r=e?.fighters.find(e=>e.player_id===this.viewerId),i=e===null||this.replay!==null||this.final!==null?0:bI(r,e.phase),a=this.rocked.update(n?0:i,t);return this.finishPass.uniforms.uRocked.value=n?0:a,r===void 0?null:i}shownAt(e){for(let t=this.history.length-1;t>=0;--t)if(this.history[t].tick<=e)return this.history[t];return null}drawCaption(e,t,n,r){let i=this.final!==null&&this.frameSeconds>=this.finalRevealAt?this.final:null;i!==null&&!this.resultAnnounced&&(this.resultAnnounced=!0,this.commentary.resultShown(this.frameSeconds));let a=this.commentary.current(this.frameSeconds),o=this.settings(),s=a===null||!o.commentary?``:a.line.text;if(s!==this.captionText&&(this.captionText=s,this.hudCanvas.dataset.caption=s),a===null||!o.commentary)return;let c=r.fighters.find(e=>e.player_id===this.viewerId),l=Nt(t,n),u=i===null?null:dR(i,t,n,r.fighters,this.players,this.roundStats,this.viewerId)/l,d=this.frameSeconds0});p!==null&&(e.save(),e.scale(l,l),XT(e,a,p,o.reducedMotion),e.restore())}destroy(){if(!this.destroyed){if(this.destroyed=!0,cancelAnimationFrame(this.raf),this.raf=0,this.buffer.clear(),this.dedupe.reset(),this.pendingContacts.length=0,this.effects.dispose(),this.avatars.dispose(),this.arena.dispose(),KN(this.ring),this.graphs!==null){for(let e of this.graphs)this.scene.remove(e.boxer.root),e.dispose();this.graphs=null}this.referee!==null&&(this.scene.remove(this.referee.boxer.root),this.referee.dispose(),this.referee=null);for(let e of this.trails)this.scene.remove(e.mesh),e.dispose();if(this.trails=[],this.cornermen!==null){for(let e of this.cornermen)this.scene.remove(e.boxer.root),e.dispose();this.cornermen=null}if(this.cutmen!==null){for(let e of this.cutmen)this.scene.remove(e.boxer.root),e.dispose();this.cutmen=null}for(let e of this.lights)this.scene.remove(e);this.keyLight?.shadow.map?.dispose(),this.keyLight?.shadow.dispose(),this.blobTexture.dispose();for(let e of this.blobShadows)this.scene.remove(e),e.geometry.dispose(),e.material.dispose();this.renderer.renderLists.dispose(),PI(this.composer),this.bloomPass.materialHighPassFilter.dispose(),rP(),this.renderer.dispose(),this.hudCanvas.remove()}}},$R=[`broadcast`,`close`,`fighter`],ez=`hands.preferences.v1`,tz={length:0,clear:()=>void 0,getItem:()=>null,key:()=>null,removeItem:()=>void 0,setItem:()=>void 0},nz=()=>{try{return window.localStorage}catch{return tz}},rz=()=>({volume:.7,haptics:!0,reducedMotion:window.matchMedia?.(`(prefers-reduced-motion: reduce)`).matches??!1,blood:`full`,commentary:!0,announcer:!0,camera:`broadcast`});function iz(e=nz()){let t=rz();try{let n=JSON.parse(e.getItem(ez)??`null`);if(typeof n!=`object`||!n||Array.isArray(n))return t;let r=n;return{volume:typeof r.volume==`number`&&Number.isFinite(r.volume)?Math.max(0,Math.min(1,r.volume)):t.volume,haptics:typeof r.haptics==`boolean`?r.haptics:t.haptics,reducedMotion:typeof r.reducedMotion==`boolean`?r.reducedMotion:t.reducedMotion,blood:r.blood===`reduced`||r.blood===`off`||r.blood===`full`?r.blood:t.blood,commentary:typeof r.commentary==`boolean`?r.commentary:t.commentary,announcer:typeof r.announcer==`boolean`?r.announcer:t.announcer,camera:$R.includes(r.camera)?r.camera:t.camera}}catch{return t}}function az(e,t=nz()){try{t.setItem(ez,JSON.stringify({volume:Math.max(0,Math.min(1,e.volume)),haptics:e.haptics,reducedMotion:e.reducedMotion,blood:e.blood,commentary:e.commentary,announcer:e.announcer,camera:e.camera}))}catch{}}var oz=class{value;listeners=new Set;constructor(e=nz()){this.storage=e,this.value=iz(e)}storage;get current(){return this.value}update(e){this.value={...this.value,...e},az(this.value,this.storage);for(let e of this.listeners)e(this.value)}subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}destroy(){this.listeners.clear()}},sz={stage:`bootstrapping`,player:null,playerId:null,role:null,players:{},playerOrder:[],simulation:null,snapshot:null,final:null,select:null,serverTick:0,nextSequence:0,reconnectMs:0,safeError:null},cz=e=>Object.fromEntries(e.map(e=>[e.id,e]));function lz(e,t){if(t.type===`bootstrap`)return{...e,stage:`authorizing`,simulation:t.simulation,safeError:null};if(t.type===`authorized`)return{...e,stage:`connecting`,player:t.player,safeError:null};if(t.type===`connecting`)return{...e,stage:`connecting`};if(t.type===`reconnect-tick`)return{...e,stage:`paused`,reconnectMs:Math.max(0,t.remainingMs)};if(t.type===`fatal`)return{...e,stage:`fatal`,safeError:t.code,reconnectMs:0};let n=t.message;switch(n.type){case`welcome`:return{...e,playerId:n.role===`fighter`?n.player_id:null,role:n.role,players:cz(n.players),playerOrder:n.players.map(e=>e.id),serverTick:n.server_tick,nextSequence:n.role===`fighter`?n.next_sequence:0,safeError:null};case`ticket`:return e;case`waiting`:return{...e,stage:`waiting`,select:null};case`select`:return{...e,stage:`select`,select:n,players:cz(n.players),playerOrder:n.players.map(e=>e.id)};case`ready`:return{...e,stage:e.snapshot?.phase??`countdown`,select:null,players:cz(n.players),playerOrder:n.players.map(e=>e.id)};case`paused`:return{...e,stage:`paused`,reconnectMs:n.grace_ms};case`resumed`:return{...e,stage:e.snapshot?.phase??`countdown`,reconnectMs:0};case`snapshot`:return{...e,stage:n.payload.phase,snapshot:n.payload,serverTick:Math.max(e.serverTick,n.payload.tick)};case`final`:return{...e,stage:`complete`,final:n,reconnectMs:0};case`error`:return{...e,stage:`fatal`,safeError:n.code,reconnectMs:0}}}var uz=new Set([`hit`,`counter_hit`,`block`,`perfect_block`,`guard_break`,`knockdown`,`parry`,`body_collapse`,`eye_shut`]),dz=11e3,fz=1500,pz=4e3,mz=3e3,hz=6,gz=5e3,_z=[{level:`rookie`,label:`Rookie`,detail:`Slow hands, leaves openings`},{level:`contender`,label:`Contender`,detail:`Works the body, punishes mistakes`},{level:`champion`,label:`Champion`,detail:`Slips, counters, finishes`}],vz=e=>Ye.includes(e),yz=class{root;reloadPage;authorizer;state=sz;settings=new oz;input=new Eu;audio=new zn(()=>this.settings.current);haptics=new iu(()=>this.settings.current);voice=new Tn(()=>this.settings.current);feedbackEvents=new zu;renderer=null;network=null;session=null;abort=new AbortController;destroyed=!1;generation=0;reloadOnRetry=!1;reloading=!1;canvas;status;overlay;roleIndicator;controlsButton;controlsPanel;retry;rematchButton;hint;cpuPicker;cpuPrompt;cpuLevel=null;rematchOpponent=null;lastPhase=null;cpuRematchLevel=null;cpuRematch=null;cpuRematchTimer=null;diagnosticsTimer=null;finalReceivedAt=0;lastFinalMatchId=null;rematchAttempts=0;rematchTimer=null;rematchCountdownTimer=null;resultRevealTimer=null;resultWaitTimer=null;completeSince=null;summaryClock=new Qt;fightSummary;liveFightStatus;finalSummary;corner;stylePicker;constructor(e,t=()=>window.location.reload(),n=new Rl){this.root=e,this.reloadPage=t,this.authorizer=n,e.innerHTML=`
HANDStwo-player boxing

`,this.canvas=e.querySelector(`canvas`),this.status=e.querySelector(`[data-status]`),this.overlay=e.querySelector(`[data-overlay]`),this.roleIndicator=e.querySelector(`[data-role]`),this.controlsButton=e.querySelector(`[data-controls]`),this.controlsPanel=e.querySelector(`[data-controls-panel]`),this.retry=e.querySelector(`[data-retry]`),this.rematchButton=e.querySelector(`[data-rematch]`),this.hint=e.querySelector(`[data-hint]`),this.cpuPicker=e.querySelector(`[data-cpu]`),this.cpuPrompt=e.querySelector(`[data-cpu-prompt]`),this.cpuPicker.addEventListener(`click`,this.onCpuPick),this.rematchButton.addEventListener(`click`,this.onRematch),this.fightSummary=e.querySelector(`[data-fight-summary]`),this.liveFightStatus=e.querySelector(`[data-fight-status]`),this.finalSummary=e.querySelector(`[data-final]`),this.retry.addEventListener(`click`,this.onRetry),this.bindPanels(),this.syncSettings(),this.input.attachTouch(e.querySelector(`.activity`)),this.corner=new Xl(e.querySelector(`.activity`),e=>this.network?.sendCornerChoice(e)??!1),this.stylePicker=new pt(e.querySelector(`.activity`),(e,t)=>this.network?.chooseStyle(e,t)??!1),this.input.setViewForward(()=>this.renderer?.viewForward()??null),window.addEventListener(`keydown`,this.onCameraKey)}onCameraKey=e=>{if(e.code!==`KeyK`||e.repeat||e.ctrlKey||e.metaKey||e.altKey)return;let t=e.target;if(t instanceof HTMLInputElement||t instanceof HTMLSelectElement||t instanceof HTMLTextAreaElement)return;e.preventDefault();let n=$R[($R.indexOf(this.settings.current.camera)+1)%$R.length];this.settings.update({camera:n}),this.root.querySelector(`[data-camera]`).value=n};start(){this.authorize()}resetForAuthorization(){this.reloadOnRetry=!1,this.network?.dispose(),this.network=null,this.session?.destroy(),this.session=null,this.renderer?.destroy(),this.renderer=null,this.voice.cancel(),this.audio.reset(),this.abort.abort(),this.abort=new AbortController,this.state=sz,this.feedbackEvents.reset(),this.input.setActive(!1),this.input.reset(),this.finalSummary.textContent=``,this.fightSummary.textContent=``,this.liveFightStatus.textContent=``,this.clearRematchTimers(),this.rematchButton.hidden=!0,this.cpuLevel=null,this.lastPhase=null}onCpuPick=e=>{let t=e.target?.closest(`[data-cpu-level]`)?.dataset.cpuLevel;!vz(t)||this.state.stage!==`waiting`||this.state.role!==`fighter`||this.cpuLevel!==null||this.callCpu(t)};callCpu(e){this.network?.requestCpu(e)===!0&&(this.cpuLevel=e,this.renderState())}get cpuBout(){return Object.values(this.state.players).some(e=>e.cpu===!0)}onRematch=()=>{this.rematchButton.disabled||this.state.stage!==`complete`||(this.rematchAttempts=1,this.rematchOpponent=this.cpuBout||this.state.role!==`fighter`?null:Object.values(this.state.players).find(e=>e.id!==this.state.playerId)?.name??null,this.authorize())};clearRematchTimers(){this.rematchTimer!==null&&window.clearTimeout(this.rematchTimer),this.rematchCountdownTimer!==null&&window.clearInterval(this.rematchCountdownTimer),this.rematchTimer=null,this.rematchCountdownTimer=null,this.clearCpuRematch()}scheduleCpuRematch(e){this.clearCpuRematch(),this.cpuRematch={level:e,at:Date.now()+gz},this.cpuRematchTimer=window.setInterval(()=>{let e=this.cpuRematch;e===null||this.state.stage!==`waiting`||this.state.role!==`fighter`||this.cpuLevel!==null?this.clearCpuRematch():Date.now()>=e.at&&(this.clearCpuRematch(),this.callCpu(e.level)),this.renderState()},250)}clearCpuRematch(){this.cpuRematchTimer!==null&&window.clearInterval(this.cpuRematchTimer),this.cpuRematchTimer=null,this.cpuRematch=null}awaitResultReveal(){this.resultRevealTimer!==null&&window.clearInterval(this.resultRevealTimer);let e=()=>{let e=this.renderer?.resultVisible??!0;this.overlay.hidden=this.state.stage===`complete`&&!e,(e||this.state.stage!==`complete`)&&this.resultRevealTimer!==null&&(window.clearInterval(this.resultRevealTimer),this.resultRevealTimer=null)};this.resultRevealTimer=window.setInterval(e,200),e()}startRematchCountdown(){this.clearRematchTimers();let e=this.state.role===`fighter`?`Rematch`:`Next bout`,t=()=>{let t=Math.ceil((this.finalReceivedAt+dz-Date.now())/1e3);this.rematchButton.hidden=this.state.stage!==`complete`,this.rematchButton.disabled=t>0,this.setText(this.rematchButton,t>0?`${e} in ${t}s`:e),t<=0&&this.rematchCountdownTimer!==null&&(window.clearInterval(this.rematchCountdownTimer),this.rematchCountdownTimer=null)};t(),this.rematchCountdownTimer=window.setInterval(t,1e3)}scheduleRematchRetry(){if(this.rematchTimer===null){if(this.rematchAttempts>=hz){this.fail(`rematch_unavailable`);return}this.rematchAttempts+=1,this.setText(this.status,`The ring is still being cleared… trying again.`),this.rematchTimer=window.setTimeout(()=>{this.rematchTimer=null,this.authorize()},mz)}}async authorize(){let e=++this.generation;this.resetForAuthorization(),this.dispatch({type:`connecting`}),this.setText(this.status,`Securing Discord Activity session…`),this.retry.hidden=!0;try{let t=await this.authorizer.authorize(this.abort.signal);if(this.destroyed||e!==this.generation){t.destroy();return}this.session=t,this.dispatch({type:`bootstrap`,simulation:t.bootstrap.simulation}),this.dispatch({type:`authorized`,player:t.player}),this.input.onAction(e=>{this.renderer?.predictAction?.(e),this.network?.notifyAction()});let n=t.takeTicket();if(n===null)throw Error(`ticket_unavailable`);let r=new Yu(n,()=>this.input.frame(),{onMessage:e=>this.receive(e),onReconnect:e=>{this.dispatch({type:`reconnect-tick`,remainingMs:e}),this.renderer?.setReconnect(e),this.renderState()},onFatal:e=>this.fail(e),onFreshAuth:()=>{e===this.generation&&this.authorize()}});this.network=r,r.start(),this.setText(this.status,`Connecting to the ring…`)}catch(t){!this.abort.signal.aborted&&e===this.generation&&(this.reloadOnRetry=t instanceof Bn&&t.reloadRequired,this.fail(Kn(t)))}}ensureRenderer(){if(this.renderer!==null||this.session===null||this.destroyed)return;let e=new QR(this.canvas,this.session.bootstrap.simulation,()=>this.settings.current,{localInput:()=>this.input.held(),inputSequenceOf:e=>this.network?.sequenceOf(e)??null});e.onContact=e=>{this.audio.event(e),this.haptics.event(e)},e.onArcadeInjury=e=>this.audio.injury(e),e.onAnnouncement=e=>this.voice.speak(e),e.onCrowdCue=()=>this.audio.chant(),e.onRocked=(e,t)=>this.audio.rocked(e,t),this.renderer=e}receive(e){if(e.type===`error`){this.fail(e.code);return}e.type===`welcome`&&this.ensureRenderer(),(e.type===`welcome`||e.type===`resumed`)&&this.renderer?.resyncClock();let t=this.rematchAttempts>0;if((e.type===`waiting`||e.type===`ready`)&&(this.rematchAttempts=0),e.type===`ready`&&(this.rematchOpponent=null),e.type===`ready`&&this.rememberStyle(e.players),e.type===`final`&&this.rematchAttempts>0&&e.match_id===this.lastFinalMatchId){this.scheduleRematchRetry();return}this.dispatch({type:`message`,message:e}),e.type===`snapshot`&&this.receiveSnapshot(e.payload),(e.type===`select`||e.type===`ready`)&&!e.players.some(e=>e.cpu===!0)&&(this.cpuLevel=null),e.type===`waiting`&&(this.cpuLevel===null?t&&this.cpuRematchLevel!==null&&this.scheduleCpuRematch(this.cpuRematchLevel):this.callCpu(this.cpuLevel)),e.type===`final`&&(this.cpuRematchLevel=this.cpuBout?this.cpuLevel:null,this.rematchAttempts=0,this.lastFinalMatchId=e.match_id,this.finalReceivedAt=Date.now(),this.renderer?.setFinal(e),this.audio.result(e),this.startRematchCountdown(),this.awaitResultReveal()),this.renderer?.setPlayers(this.state.players,this.state.playerId,this.state.playerOrder),this.renderer?.setReconnect(this.state.reconnectMs),this.renderState()}get networkStats(){return{inputLatencyMs:this.network?.inputLatencyMs??null,resolutionScale:this.renderer?.resolutionScale??null,gpu:this.renderer?.memoryInfo??null}}receiveSnapshot(e){this.lastPhase===`countdown`&&e.phase!==`countdown`&&this.voice.finishLine(fz),this.lastPhase=e.phase,this.renderer?.setInputLatency(this.network?.inputLatencyMs??null),this.renderer?.push(e);let t=e.fighters.find(e=>e.player_id===this.state.playerId);this.input.setKnockdown(t?.is_downed===!0),t!==void 0&&this.audio.snapshot(e.tick,t.stamina,t.maximum_stamina,t.trauma.head+t.trauma.body),this.audio.roundClock(e.phase,e.round_number,e.phase_ticks_remaining,this.state.simulation?.tick_rate??30),this.audio.tension(zd(e));for(let t of this.feedbackEvents.accept(e.events))uz.has(t.kind)||(this.audio.event(t),this.haptics.event(t))}rememberStyle(e){let t=e.find(e=>e.id===this.state.playerId)?.style;this.state.role===`fighter`&&t!==void 0&&ot(t)}dispatch(e){this.state=lz(this.state,e)}setText(e,t){e.textContent!==t&&(e.textContent=t)}renderState(){let e={bootstrapping:`Loading…`,authorizing:`Authorizing with Discord…`,connecting:`Connecting securely…`,waiting:this.state.role===`spectator`?`Waiting for the fighters…`:this.cpuLevel===null?this.cpuRematch===null?this.rematchOpponent===null?`Waiting for an opponent. Anyone in this channel can join with Play now.`:`Waiting for ${this.rematchOpponent} to take the rematch…`:`Rematch with the ${_z.find(e=>e.level===this.cpuRematch?.level)?.label??`computer`} in ${Math.max(1,Math.ceil((this.cpuRematch.at-Date.now())/1e3))}s, unless someone joins first.`:`Calling in the computer…`,select:`Pick how your fighter boxes.`,countdown:`Bout countdown.`,fight:`Round ${this.state.snapshot?.round_number??1} in progress.`,knockdown:`Knockdown count ${this.state.snapshot?.fighters.find(e=>e.player_id===this.state.playerId)?.get_up_count??0}.`,foul_recovery:`Foul recovery in progress.`,rest:`Between-round rest.`,paused:`Connection paused. ${Math.ceil(this.state.reconnectMs/1e3)} seconds remain.`,complete:this.cpuBout?`Bout complete. Scorecards are displayed. Bouts against the computer are unrated.`:`Bout complete. Scorecards and rating changes are displayed.`,fatal:Ql(this.state.safeError??`safe_error`)},t=this.state.role===`spectator`;this.setText(this.status,t?`Spectating — ${e[this.state.stage]}`:e[this.state.stage]);let n=this.state.stage===`complete`&&this.state.final!==null;if(this.status.hidden=n||[`select`,`countdown`,`fight`,`knockdown`,`foul_recovery`,`rest`].includes(this.state.stage),this.overlay.toggleAttribute(`data-raised`,this.state.snapshot!==null),this.overlay.toggleAttribute(`data-result`,n),this.state.stage!==`complete`)this.overlay.hidden=!1,this.completeSince=null;else if(this.state.final===null){this.completeSince??=Date.now();let e=Date.now()-this.completeSince;this.overlay.hidden=e{this.resultWaitTimer=null,this.renderState()},pz-e+50))}this.roleIndicator.hidden=!t,this.controlsButton.hidden=t,t&&(this.controlsPanel.hidden=!0,this.controlsButton.setAttribute(`aria-expanded`,`false`));let r=this.state.snapshot?.fighters.find(e=>e.player_id===this.state.playerId),i=t?`Spectating. ${e[this.state.stage]}`:this.state.stage===`knockdown`&&r?.is_downed===!0?`Knockdown. Count ${r.get_up_count}. ${r.get_up_prompt===null?`Wait for your private rhythm instruction.`:`Press ${r.get_up_prompt===`get_up_left`?`left`:`right`} now.`}`:this.state.stage===`paused`?`Connection paused.`:e[this.state.stage];this.setText(this.liveFightStatus,i),this.corner.update(this.state.snapshot,this.state.playerId,this.state.role===`fighter`&&this.state.stage===`rest`),this.renderer?.setCornerPanelTop(this.corner.top()),this.stylePicker.update(this.state.stage===`select`?this.state.select:null,this.state.playerId,this.state.role),this.retry.hidden=this.state.stage!==`fatal`,this.setText(this.retry,this.reloadOnRetry?`Reload`:`Retry securely`),this.state.stage!==`complete`&&(this.rematchButton.hidden=!0),this.cpuPicker.hidden=t||this.state.stage!==`waiting`||this.cpuLevel!==null,this.setText(this.cpuPrompt,this.cpuRematch===null?this.rematchOpponent===null?`No one here yet? Fight the computer.`:`Or fight the computer instead:`:`Or start now:`);let a=!t&&(this.state.stage===`waiting`||this.state.stage===`countdown`);this.hint.hidden=!a,a&&this.setText(this.hint,fu($l(),window.innerWidth)),this.overlay.toggleAttribute(`data-touch-hint`,a&&this.state.stage===`countdown`&&$l());let o=!t&&[`countdown`,`fight`,`knockdown`,`foul_recovery`].includes(this.state.stage),s=!t&&this.state.snapshot!==null&&(this.state.stage===`rest`||this.state.stage===`paused`);if(this.input.setActive(o,o||s),this.network?.setActive(o),this.renderFightSummary(),this.state.final!==null){let e=this.state.final;this.setText(this.finalSummary,`${e.method.replaceAll(`_`,` `)}. ${e.winner_id===null?`Draw`:`${this.state.players[e.winner_id]?.name??`Winner`} wins`}. Scorecards: ${e.scorecards.map(e=>`${e.judge}: ${e.player_one.reduce((e,t)=>e+t,0)} to ${e.player_two.reduce((e,t)=>e+t,0)}`).join(`; `)}. ${this.cpuBout?`Unrated bout against the computer.`:`Ratings: ${Object.entries(e.ratings).map(([e,t])=>`${this.state.players[e]?.name??`fighter`} ${t.before} to ${t.after}`).join(`; `)}.`}`)}}renderFightSummary(){let e=this.state.snapshot;if(e===null)return;let t=this.state.simulation?.tick_rate??30,n=Math.floor(this.summaryClock.ticks(e)/t),r=`${Math.floor(n/60)}:${String(n%60).padStart(2,`0`)}`,i=e.fighters.map(e=>{let t=this.state.players[e.player_id];return`${t?.name??`Fighter`}, ${t?.cpu===!0?`computer opponent`:`ELO ${t?.rating??`unknown`}`}, stamina ${Math.round(e.stamina)} of ${Math.round(e.maximum_stamina)}, guard ${Math.round(e.guard)}, poise ${Math.round(e.poise)}, conditioning ${Math.round(e.conditioning)}, ${e.warnings} warnings, ${e.knockdowns} knockdowns`}),a=e.fighters.find(e=>e.player_id===this.state.playerId),o=a?.is_downed===!0?a.get_up_prompt===null?`You are down. Count ${a.get_up_count}. Wait for your private rhythm instruction.`:`You are down. Count ${a.get_up_count}. Press ${a.get_up_prompt===`get_up_left`?`left`:`right`} now. Get-up progress ${a.get_up_meter} of ${a.get_up_required}.`:``;this.setText(this.fightSummary,`Round ${e.round_number}. ${e.phase.replace(`_`,` `)}. Clock ${r}. ${i.join(`. `)}. ${o}`.trim())}onRetry=()=>{if(this.reloadOnRetry){this.reloading=!0,this.reloadPage();return}this.authorize()};bindPanels(){let e=(e,t)=>{let n=this.root.querySelector(e),r=this.root.querySelector(t);n.addEventListener(`click`,()=>{r.hidden=!r.hidden,n.setAttribute(`aria-expanded`,String(!r.hidden))})};e(`[data-controls]`,`[data-controls-panel]`),e(`[data-settings]`,`[data-settings-panel]`);let t=this.root.querySelector(`[data-settings-panel]`);this.root.querySelector(`[data-settings]`).addEventListener(`click`,()=>{this.diagnosticsTimer!==null&&window.clearInterval(this.diagnosticsTimer),this.diagnosticsTimer=null,!t.hidden&&(this.refreshDiagnostics(),this.diagnosticsTimer=window.setInterval(()=>this.refreshDiagnostics(),1e3))});let n=this.root.querySelector(`[data-copy-diagnostics]`);n.addEventListener(`click`,()=>{let e=this.diagnosticsText(),t=()=>{this.setText(n,`Copied`),window.setTimeout(()=>this.setText(n,`Copy diagnostics`),1500)},r=()=>{let e=this.root.querySelector(`[data-diagnostics]`),t=window.getSelection();if(t===null)return;let r=document.createRange();r.selectNodeContents(e),t.removeAllRanges(),t.addRange(r),this.setText(n,`Selected. Copy manually`),window.setTimeout(()=>this.setText(n,`Copy diagnostics`),2500)},i=navigator.clipboard;if(i===void 0||typeof i.writeText!=`function`){r();return}i.writeText(e).then(t).catch(r)}),this.root.querySelector(`[data-volume]`).addEventListener(`input`,e=>{this.settings.update({volume:Number(e.target.value)}),this.audio.setVolume(),this.voice.settingsChanged()}),this.root.querySelector(`[data-haptics]`).addEventListener(`change`,e=>{this.settings.update({haptics:e.target.checked})}),this.root.querySelector(`[data-motion]`).addEventListener(`change`,e=>{let t=e.target.checked;this.settings.update({reducedMotion:t}),this.renderer?.setReducedMotion(t)}),this.root.querySelector(`[data-commentary]`).addEventListener(`change`,e=>{this.settings.update({commentary:e.target.checked})}),this.root.querySelector(`[data-announcer]`).addEventListener(`change`,e=>{let t=e.target.checked;this.settings.update({announcer:t}),t||this.voice.cancel()}),this.root.querySelector(`[data-blood]`).addEventListener(`change`,e=>{let t=e.target.value;this.settings.update({blood:t}),this.renderer?.setBloodLevel(t)}),this.root.querySelector(`[data-camera]`).addEventListener(`change`,e=>{let t=$R.find(t=>t===e.target.value);t!==void 0&&this.settings.update({camera:t})})}diagnosticsText(){let e=this.networkStats,t=this.renderer?.diagnostics,n=t===void 0||t.frameMs<=0?`-`:(1e3/t.frameMs).toFixed(0);return[`stage: ${this.state.stage} · role: ${this.state.role??`-`}`,`input latency: ${e.inputLatencyMs===null?`-`:`${Math.round(e.inputLatencyMs)} ms`}`,`frame: ${t===void 0?`-`:`${t.frameMs.toFixed(1)} ms (${n} fps)`} · render scale: ${t?.resolutionScale??`-`}`,`gpu objects: ${t===void 0?`-`:`${t.gpu.geometries} geometries, ${t.gpu.textures} textures, ${t.gpu.programs} programs`}`,`graphics: ${t?.graphics??`-`}`,`browser: ${navigator.userAgent}`,`pointer: ${$l()?`coarse`:`fine`} · viewport: ${window.innerWidth}x${window.innerHeight} @${window.devicePixelRatio}`,`last error: ${this.state.safeError??`none`}`,`match: ${this.state.final?.match_id??`-`}`].join(` +`)}refreshDiagnostics(){this.setText(this.root.querySelector(`[data-diagnostics]`),this.diagnosticsText())}syncSettings(){let e=this.settings.current;this.root.querySelector(`[data-volume]`).value=String(e.volume),this.root.querySelector(`[data-haptics]`).checked=e.haptics,this.root.querySelector(`[data-motion]`).checked=e.reducedMotion,this.root.querySelector(`[data-blood]`).value=e.blood,this.root.querySelector(`[data-commentary]`).checked=e.commentary,this.syncAnnouncerSetting(),this.voice.onVoicesChanged=()=>this.syncAnnouncerSetting(),this.root.querySelector(`[data-camera]`).value=e.camera}syncAnnouncerSetting(){let e=this.root.querySelector(`[data-announcer]`),t=this.voice.hasVoice;e.checked=this.settings.current.announcer&&t,e.disabled=!t;let n=this.voice.supported?t?null:`This device has no English voice of its own`:`This browser cannot speak`;n===null?e.parentElement.removeAttribute(`title`):e.parentElement.title=n}fail(e){if(e===`room_closed`&&this.rematchAttempts>0){this.scheduleRematchRetry();return}e===`client_outdated`&&(this.reloadOnRetry=!0),this.dispatch({type:`fatal`,code:e}),this.rematchOpponent=null,this.voice.cancel(),this.network?.dispose(),this.network=null,this.session?.destroy(),this.session=null,this.renderer?.destroy(),this.renderer=null,this.input.setActive(!1),this.input.reset(),this.renderState()}destroy(){this.destroyed||(this.destroyed=!0,this.generation+=1,this.abort.abort(),this.network?.dispose(),this.session?.destroy(),this.reloading||this.authorizer.close(),this.renderer?.destroy(),window.removeEventListener(`keydown`,this.onCameraKey),this.input.destroy(),this.corner.destroy(),this.stylePicker.destroy(),this.audio.destroy(),this.voice.destroy(),this.settings.destroy(),this.retry.removeEventListener(`click`,this.onRetry),this.rematchButton.removeEventListener(`click`,this.onRematch),this.cpuPicker.removeEventListener(`click`,this.onCpuPick),this.clearRematchTimers(),this.diagnosticsTimer!==null&&window.clearInterval(this.diagnosticsTimer),this.resultRevealTimer!==null&&window.clearInterval(this.resultRevealTimer),this.resultWaitTimer!==null&&window.clearTimeout(this.resultWaitTimer),this.diagnosticsTimer=null,this.root.replaceChildren())}};(new URLSearchParams(window.location.search).get(`styles`)??``).split(`,`);var bz={broadcast:{position:[0,1.88,7.15],lookAt:[0,1.12,0]},side:{position:[7.4,1.6,0],lookAt:[0,1.1,0]},top:{position:[.01,8.5,.01],lookAt:[0,0,0]},ringside:{position:[1.6,1.5,4.1],lookAt:[.2,1.25,0]}},xz=[[`hips`,`spine`],[`spine`,`head`],[`chest`,`shoulderL`],[`shoulderL`,`elbowL`],[`elbowL`,`gloveL`],[`chest`,`shoulderR`],[`shoulderR`,`elbowR`],[`elbowR`,`gloveR`],[`hips`,`hipL`],[`hipL`,`kneeL`],[`kneeL`,`ankleL`],[`hips`,`hipR`],[`hipR`,`kneeR`],[`kneeR`,`ankleR`]],Sz=class{root;renderer=null;replay=null;raf=0;playing=!1;speed=1;virtualTick=0;carry=0;latencyMs=0;pendingQueue=[];destroyed=!1;overlays={skeleton:!1,hurtbox:!1,hitbox:!1};skeletonLines=[];hurtboxMeshes=[];hitboxMeshes=[];statusEl;metricsEl;canvas;constructor(e){this.root=e,e.innerHTML=`
HANDS LABloading replay…
@@ -4590,7 +4999,7 @@ void main() { -
`,this.canvas=e.querySelector(`canvas`),this.statusEl=e.querySelector(`[data-lab-status]`),this.metricsEl=e.querySelector(`[data-lab-metrics]`),this.bindControls()}async start(){let e=new URLSearchParams(window.location.search),t=e.get(`replay`)??`golden`,n=await fetch(`/replays/${t}.json`);if(n.ok||(n=await fetch(`/hands/replays/${t}.json`)),!n.ok)throw Error(`replay_load_failed:${n.status}`);this.replay=await n.json(),await fw(),this.renderer=new QE(this.canvas,{tick_rate:this.replay.tick_rate,ring_half_width:500,ring_half_height:500},()=>({volume:0,haptics:!1,reducedMotion:!1,blood:`full`}),{manualClock:!0});let r=Object.fromEntries(this.replay.ticks[0].snapshot.payload.fighters.map(e=>[e.player_id,{id:e.player_id,name:e.player_id===`one`?`Azure Vector`:`Crimson Geometry`,avatar:null,rating:1500,connected:!0}]));this.renderer.setPlayers(r,`one`),this.buildOverlays();let i=e.get(`tick`);if(e.get(`metrics`)===`1`){await this.renderer.ready,this.exportMetrics(),this.setStatus(`metrics done`);return}if(i!==null){let t=Number(i);await this.renderer.ready,this.seek(t);let n=_D[e.get(`camera`)??``]??_D.broadcast;this.renderer.labSetCameraOverride(n.position,n.lookAt),this.overlays.skeleton=e.get(`skeleton`)===`1`,this.overlays.hurtbox=e.get(`hurtbox`)===`1`,this.overlays.hitbox=e.get(`hitbox`)===`1`,this.syncOverlayVisibility(),this.renderCurrentTick(),window.__labDebug={roots:this.renderer.labRigs.map(e=>e.position.toArray().map(e=>Number(e.toFixed(3)))),heads:this.renderer.labRigs.map(e=>{let t=e.getObjectByName(rC.head??`head`);return t===void 0?null:t.getWorldPosition(new Z).toArray().map(e=>Number(e.toFixed(3)))}),gloves:this.renderer.labRigs.map(e=>{let t=e.getObjectByName(rC.gloveL??`gloveL`);return t===void 0?null:t.getWorldPosition(new Z).toArray().map(e=>Number(e.toFixed(3)))}),material:(()=>{let e=null;return this.renderer.labRigs[0]?.traverse(t=>{if(e===null&&t instanceof Sp){let n=t.material;e={colorHex:n.color.getHexString(),hasMap:n.map!==null,mapImage:n.map?.image?.width??`none`,mapVersion:n.map?.version,clearcoat:n.clearcoat,opacity:n.opacity}}}),e})()},this.setStatus(`filmstrip tick ${t}`);return}this.playing=!0;let a=()=>{this.destroyed||(this.advance(),this.raf=requestAnimationFrame(a))};this.raf=requestAnimationFrame(a)}bindControls(){this.root.querySelector(`[data-lab='play']`).addEventListener(`click`,e=>{this.playing=!this.playing,e.target.textContent=this.playing?`Pause`:`Play`}),this.root.querySelector(`[data-lab='step']`).addEventListener(`click`,()=>{this.playing=!1,this.root.querySelector(`[data-lab='play']`).textContent=`Play`,this.virtualTick=Math.min(this.virtualTick+1,(this.replay?.ticks.length??1)-1),this.pushUpTo(this.virtualTick),this.renderCurrentTick()}),this.root.querySelector(`[data-lab='speed']`).addEventListener(`change`,e=>{this.speed=Number(e.target.value)}),this.root.querySelector(`[data-lab='camera']`).addEventListener(`change`,e=>{let t=_D[e.target.value];this.renderer?.labSetCameraOverride(t.position,t.lookAt)});for(let e of[`skeleton`,`hurtbox`,`hitbox`])this.root.querySelector(`[data-lab='${e}']`).addEventListener(`change`,t=>{this.overlays[e]=t.target.checked,this.syncOverlayVisibility()});this.root.querySelector(`[data-lab='latency']`).addEventListener(`change`,e=>{this.latencyMs=Math.max(0,Math.min(500,Number(e.target.value)))}),this.root.querySelector(`[data-lab='metrics']`).addEventListener(`click`,()=>this.exportMetrics())}advance(){if(!this.playing||this.replay===null)return;let e=this.replay.ticks.length-1;if(!(this.virtualTick>=e)){for(this.carry+=this.speed;this.carry>=1&&this.virtualTick0?this.pendingQueue.push({at:performance.now()+this.latencyMs,tick:t}):this.renderer?.push(t.snapshot.payload))}drainLatencyQueue(){let e=performance.now();for(let t=this.pendingQueue.length-1;t>=0;--t){let n=this.pendingQueue[t];n.at<=e&&(this.renderer?.push(n.tick.snapshot.payload),this.pendingQueue.splice(t,1))}}seek(e,t=!0){if(this.replay===null||this.renderer===null)return;let n=Math.max(0,Math.min(e,this.replay.ticks.length-1));for(let e=0;e<=n;e+=1)this.renderer.push(this.replay.ticks[e].snapshot.payload),e>=n-15&&this.renderer.labEffects.update(1/30);this.virtualTick=n,this.renderOnSeek=t}renderOnSeek=!0;renderCurrentTick(e){if(this.replay===null||this.renderer===null)return;this.drainLatencyQueue(),this.renderer.labFrame(this.virtualTick/this.replay.tick_rate,e??this.renderOnSeek),this.renderer.labFrame((this.virtualTick+.5)/this.replay.tick_rate,e??this.renderOnSeek),this.updateOverlays();let t=this.replay.ticks[this.virtualTick]?.snapshot.payload.phase??``;this.setStatus(`tick ${this.virtualTick}/${this.replay.ticks.length-1} · ${t} · ${this.speed}×`)}buildOverlays(){let e=this.renderer;if(e===null)return;let t=new Vp({color:8257448,depthTest:!1,transparent:!0,opacity:.9});for(let n of e.labRigs){let r=new Lf;r.setAttribute(`position`,new Sf(new Float32Array(vD.length*6),3));let i=new $p(r,t);i.frustumCulled=!1,i.renderOrder=20,e.labScene.add(i),this.skeletonLines.push(i);let a=new up(new mm(.13,.16,4,8),new Qf({color:16735581,wireframe:!0,transparent:!0,opacity:.85,depthTest:!1}));a.renderOrder=21,a.position.y=.12,(n.getObjectByName(rC.head??`head`)??n).add(a),this.hurtboxMeshes.push(a);let o=new up(new mm(.21,.34,4,8),new Qf({color:16758877,wireframe:!0,transparent:!0,opacity:.7,depthTest:!1}));o.renderOrder=21,o.position.y=.16,(n.getObjectByName(rC.chest??`chest`)??n).add(o),this.hurtboxMeshes.push(o);for(let e of[`gloveL`,`gloveR`]){let t=new up(new Ym(.11,10,8),new Qf({color:6141183,wireframe:!0,transparent:!0,opacity:.85,depthTest:!1}));t.renderOrder=22,(n.getObjectByName(rC[e]??e)??n).add(t),this.hitboxMeshes.push(t)}}this.syncOverlayVisibility()}syncOverlayVisibility(){for(let e of this.skeletonLines)e.visible=this.overlays.skeleton;for(let e of this.hurtboxMeshes)e.visible=this.overlays.hurtbox;for(let e of this.hitboxMeshes)e.visible=this.overlays.hitbox}updateOverlays(){let e=this.renderer;if(e!==null)for(let[t,n]of e.labRigs.entries()){let e=this.skeletonLines[t];if(e===void 0||!e.visible)continue;let r=e.geometry.getAttribute(`position`),i=new Z,a=new Z;vD.forEach(([e,t],o)=>{let s=n.getObjectByName(e),c=n.getObjectByName(t);s===void 0||c===void 0||(s.getWorldPosition(i),c.getWorldPosition(a),r.setXYZ(o*2,i.x,i.y,i.z),r.setXYZ(o*2+1,a.x,a.y,a.z))}),r.needsUpdate=!0}}jointWorld(e,t){let n=(this.renderer?.labRigs[e])?.getObjectByName(rC[t]??t);return n==null?null:n.getWorldPosition(new Z)}exportMetrics(){if(this.replay===null)return;let e={contactAlignment:[],maxRootStepMeters:0,footSlideMeters:0,phaseDriftTicks:0};for(let t of this.replay.ticks){let n=t.snapshot.payload.events.find(e=>[`hit`,`counter_hit`,`block`].includes(e.kind)),r=t.snapshot.payload.fighters[0].action_hand;if(n===void 0||r===null)continue;let i=Math.min(t.tick+1,this.replay.ticks.length-1);this.seek(i,!1),this.renderCurrentTick(!1);let a=this.jointWorld(0,r===`left`?`gloveL`:`gloveR`),o=this.jointWorld(1,`head`),s=this.jointWorld(1,`chest`);a!==null&&o!==null&&s!==null&&e.contactAlignment.push({tick:t.tick,kind:n.kind,gloveToHeadMeters:Number(Math.max(0,a.distanceTo(o)-Cb.head.radius).toFixed(4)),gloveToTorsoMeters:Number(Math.max(0,a.distanceTo(s)-Cb.torso.radius).toFixed(4))})}this.seek(0,!1);let t=null,n=null;for(let r of this.replay.ticks){this.seek(r.tick,!1),this.renderCurrentTick(!1);let i=this.renderer.labRigs[0].position.x;n!==null&&(e.maxRootStepMeters=Math.max(e.maxRootStepMeters,Math.abs(i-n))),n=i;let a=r.snapshot.payload.fighters[0],o=Math.hypot(a.velocity_x,a.velocity_y),s=this.replay.ticks.slice(Math.max(0,r.tick-30),r.tick+1).some(e=>e.snapshot.payload.fighters[0].is_downed);if(o===0&&!a.is_downed&&!s&&r.snapshot.payload.phase===`fight`){let n=[this.jointWorld(0,`ankleL`),this.jointWorld(0,`ankleR`)].filter(e=>e!==null);t!==null&&n.length===2&&(e.footSlideMeters=Math.max(e.footSlideMeters,Number(Math.max(n[0].distanceTo(t[0]),n[1].distanceTo(t[1])).toFixed(4)))),t=n}else t=null}this.metricsEl.textContent=` contacts: ${e.contactAlignment.map(e=>`${e.kind}@t${e.tick} glove-head ${e.gloveToHeadMeters}m`).join(` · `)} · max root step ${e.maxRootStepMeters.toFixed(3)}m · foot slide ${e.footSlideMeters}m`,window.__labDebug={roots:this.renderer.labRigs.map(e=>e.position.toArray().map(e=>Number(e.toFixed(3)))),graphsActive:this.renderer.graphs!==null,heads:this.renderer.labRigs.map(e=>{let t=e.getObjectByName(rC.head??`head`);return t===void 0?null:t.getWorldPosition(new Z).toArray().map(e=>Number(e.toFixed(3)))})};let r=new Blob([JSON.stringify(e,null,1)],{type:`application/json`}),i=document.createElement(`a`);i.href=URL.createObjectURL(r),i.download=`hands-lab-metrics.json`,i.click(),URL.revokeObjectURL(i.href)}setStatus(e){this.statusEl.textContent!==e&&(this.statusEl.textContent=e)}destroy(){this.destroyed||(this.destroyed=!0,cancelAnimationFrame(this.raf),this.renderer?.destroy(),this.root.replaceChildren())}};function bD(e){let t=new yD(e);return t.start().catch(t=>{e.querySelector(`[data-lab-status]`).textContent=`lab failed: ${String(t)}`}),()=>t.destroy()}var xD=()=>({player_id:`lab`,x:0,y:0,facing:1,facing_x:0,facing_y:-1e3,velocity_x:0,velocity_y:0,stance:`orthodox`,defense:`none`,stamina:1e3,maximum_stamina:1e3,conditioning:1e3,guard:700,poise:600,trauma:{head:0,body:0,left_eye:0,right_eye:0,left_cut:0,right_cut:0,swelling:0,bleeding:0},knockdowns:0,warnings:0,deductions:0,stunned_ticks:0,is_downed:!1,action:null,action_hand:null,action_target:null,action_power:null,action_id:null,action_key:null,action_start_tick:0,action_startup_ticks:0,action_active_ticks:0,action_recovery_ticks:0,action_contact_tick:null,queued_actions:0,clinch_startup_ticks:0,clinch_ticks:0,is_foul_recovery_target:!1,taunt_ticks:0,get_up_prompt:null,get_up_meter:0,get_up_required:0,get_up_count:0,get_up_window_start_tick:0,get_up_window_end_tick:0,last_input_sequence:-1}),SD={face:[.25,1.55,1],front:[0,1.35,3.4],side:[3.4,1.3,.2],"three-quarter":[2.4,1.5,2.6],top:[.01,4.5,.6],back:[.2,1.4,-3.4],low:[1.8,.5,2.6]},CD=class{root;renderer;scene=new Hd;camera;graph=null;boxer=null;trails=[];trailGlove=new Z;skeletonHelper=null;raf=0;previous=performance.now();elapsed=0;params=new URLSearchParams(window.location.search);reactionFired=!1;statusEl;constructor(e){this.root=e,e.innerHTML=`
+
`,this.canvas=e.querySelector(`canvas`),this.statusEl=e.querySelector(`[data-lab-status]`),this.metricsEl=e.querySelector(`[data-lab-metrics]`),this.bindControls()}async start(){let e=new URLSearchParams(window.location.search),t=e.get(`replay`)??`golden`,n=await fetch(`/replays/${t}.json`);if(n.ok||(n=await fetch(`/hands/replays/${t}.json`)),!n.ok)throw Error(`replay_load_failed:${n.status}`);this.replay=await n.json(),await nP(),this.renderer=new QR(this.canvas,{tick_rate:this.replay.tick_rate,ring_half_width:500,ring_half_height:500},()=>({volume:0,haptics:!1,reducedMotion:!1,blood:`full`,commentary:!0,announcer:!1,camera:`broadcast`}),{manualClock:!0});let r=Object.fromEntries(this.replay.ticks[0].snapshot.payload.fighters.map(e=>[e.player_id,{id:e.player_id,name:e.player_id===`one`?`Azure Vector`:`Crimson Geometry`,avatar:null,rating:1500,connected:!0}]));this.renderer.setPlayers(r,`one`),this.buildOverlays();let i=e.get(`tick`);if(e.get(`metrics`)===`1`){await this.renderer.ready,this.exportMetrics(),this.setStatus(`metrics done`);return}if(i!==null){let t=Number(i);await this.renderer.ready,this.seek(t);let n=bz[e.get(`camera`)??``]??bz.broadcast;this.renderer.labSetCameraOverride(n.position,n.lookAt),this.overlays.skeleton=e.get(`skeleton`)===`1`,this.overlays.hurtbox=e.get(`hurtbox`)===`1`,this.overlays.hitbox=e.get(`hitbox`)===`1`,this.syncOverlayVisibility(),this.renderCurrentTick(),window.__labDebug={roots:this.renderer.labRigs.map(e=>e.position.toArray().map(e=>Number(e.toFixed(3)))),heads:this.renderer.labRigs.map(e=>{let t=e.getObjectByName(gA.head??`head`);return t===void 0?null:t.getWorldPosition(new J).toArray().map(e=>Number(e.toFixed(3)))}),gloves:this.renderer.labRigs.map(e=>{let t=e.getObjectByName(gA.gloveL??`gloveL`);return t===void 0?null:t.getWorldPosition(new J).toArray().map(e=>Number(e.toFixed(3)))}),material:(()=>{let e=null;return this.renderer.labRigs[0]?.traverse(t=>{if(e===null&&t instanceof Jg){let n=t.material;e={colorHex:n.color.getHexString(),hasMap:n.map!==null,mapImage:n.map?.image?.width??`none`,mapVersion:n.map?.version,clearcoat:n.clearcoat,opacity:n.opacity}}}),e})()},this.setStatus(`filmstrip tick ${t}`);return}this.playing=!0;let a=()=>{this.destroyed||(this.advance(),this.raf=requestAnimationFrame(a))};this.raf=requestAnimationFrame(a)}bindControls(){this.root.querySelector(`[data-lab='play']`).addEventListener(`click`,e=>{this.playing=!this.playing,e.target.textContent=this.playing?`Pause`:`Play`}),this.root.querySelector(`[data-lab='step']`).addEventListener(`click`,()=>{this.playing=!1,this.root.querySelector(`[data-lab='play']`).textContent=`Play`,this.virtualTick=Math.min(this.virtualTick+1,(this.replay?.ticks.length??1)-1),this.pushUpTo(this.virtualTick),this.renderCurrentTick()}),this.root.querySelector(`[data-lab='speed']`).addEventListener(`change`,e=>{this.speed=Number(e.target.value)}),this.root.querySelector(`[data-lab='camera']`).addEventListener(`change`,e=>{let t=bz[e.target.value];this.renderer?.labSetCameraOverride(t.position,t.lookAt)});for(let e of[`skeleton`,`hurtbox`,`hitbox`])this.root.querySelector(`[data-lab='${e}']`).addEventListener(`change`,t=>{this.overlays[e]=t.target.checked,this.syncOverlayVisibility()});this.root.querySelector(`[data-lab='latency']`).addEventListener(`change`,e=>{this.latencyMs=Math.max(0,Math.min(500,Number(e.target.value)))}),this.root.querySelector(`[data-lab='metrics']`).addEventListener(`click`,()=>this.exportMetrics())}advance(){if(!this.playing||this.replay===null)return;let e=this.replay.ticks.length-1;if(!(this.virtualTick>=e)){for(this.carry+=this.speed;this.carry>=1&&this.virtualTick0?this.pendingQueue.push({at:performance.now()+this.latencyMs,tick:t}):this.renderer?.push(t.snapshot.payload))}drainLatencyQueue(){let e=performance.now();for(let t=this.pendingQueue.length-1;t>=0;--t){let n=this.pendingQueue[t];n.at<=e&&(this.renderer?.push(n.tick.snapshot.payload),this.pendingQueue.splice(t,1))}}seek(e,t=!0){if(this.replay===null||this.renderer===null)return;let n=Math.max(0,Math.min(e,this.replay.ticks.length-1));for(let e=0;e<=n;e+=1)this.renderer.push(this.replay.ticks[e].snapshot.payload),e>=n-15&&this.renderer.labEffects.update(1/30);this.virtualTick=n,this.renderOnSeek=t}renderOnSeek=!0;renderCurrentTick(e){if(this.replay===null||this.renderer===null)return;this.drainLatencyQueue(),this.renderer.labFrame(this.virtualTick/this.replay.tick_rate,e??this.renderOnSeek),this.renderer.labFrame((this.virtualTick+.5)/this.replay.tick_rate,e??this.renderOnSeek),this.updateOverlays();let t=this.replay.ticks[this.virtualTick]?.snapshot.payload.phase??``;this.setStatus(`tick ${this.virtualTick}/${this.replay.ticks.length-1} · ${t} · ${this.speed}×`)}buildOverlays(){let e=this.renderer;if(e===null)return;let t=new p_({color:8257448,depthTest:!1,transparent:!0,opacity:.9});for(let n of e.labRigs){let r=new lg;r.setAttribute(`position`,new Jh(new Float32Array(xz.length*6),3));let i=new T_(r,t);i.frustumCulled=!1,i.renderOrder=20,e.labScene.add(i),this.skeletonLines.push(i);let a=new Ig(new B_(.13,.16,4,8),new wg({color:16735581,wireframe:!0,transparent:!0,opacity:.85,depthTest:!1}));a.renderOrder=21,a.position.y=.12,(n.getObjectByName(gA.head??`head`)??n).add(a),this.hurtboxMeshes.push(a);let o=new Ig(new B_(.21,.34,4,8),new wg({color:16758877,wireframe:!0,transparent:!0,opacity:.7,depthTest:!1}));o.renderOrder=21,o.position.y=.16,(n.getObjectByName(gA.chest??`chest`)??n).add(o),this.hurtboxMeshes.push(o);for(let e of[`gloveL`,`gloveR`]){let t=new Ig(new Sv(.11,10,8),new wg({color:6141183,wireframe:!0,transparent:!0,opacity:.85,depthTest:!1}));t.renderOrder=22,(n.getObjectByName(gA[e]??e)??n).add(t),this.hitboxMeshes.push(t)}}this.syncOverlayVisibility()}syncOverlayVisibility(){for(let e of this.skeletonLines)e.visible=this.overlays.skeleton;for(let e of this.hurtboxMeshes)e.visible=this.overlays.hurtbox;for(let e of this.hitboxMeshes)e.visible=this.overlays.hitbox}updateOverlays(){let e=this.renderer;if(e!==null)for(let[t,n]of e.labRigs.entries()){let e=this.skeletonLines[t];if(e===void 0||!e.visible)continue;let r=e.geometry.getAttribute(`position`),i=new J,a=new J;xz.forEach(([e,t],o)=>{let s=n.getObjectByName(e),c=n.getObjectByName(t);s===void 0||c===void 0||(s.getWorldPosition(i),c.getWorldPosition(a),r.setXYZ(o*2,i.x,i.y,i.z),r.setXYZ(o*2+1,a.x,a.y,a.z))}),r.needsUpdate=!0}}jointWorld(e,t){let n=(this.renderer?.labRigs[e])?.getObjectByName(gA[t]??t);return n==null?null:n.getWorldPosition(new J)}exportMetrics(){if(this.replay===null)return;let e={contactAlignment:[],maxRootStepMeters:0,footSlideMeters:0,phaseDriftTicks:0};for(let t of this.replay.ticks){let n=t.snapshot.payload.events.find(e=>[`hit`,`counter_hit`,`block`].includes(e.kind)),r=t.snapshot.payload.fighters[0].action_hand;if(n===void 0||r===null)continue;let i=Math.min(t.tick+1,this.replay.ticks.length-1);this.seek(i,!1),this.renderCurrentTick(!1);let o=this.jointWorld(0,r===`left`?`gloveL`:`gloveR`),s=this.jointWorld(1,`head`),c=this.jointWorld(1,`chest`);o!==null&&s!==null&&c!==null&&e.contactAlignment.push({tick:t.tick,kind:n.kind,gloveToHeadMeters:Number(Math.max(0,o.distanceTo(s)-a.head.radius).toFixed(4)),gloveToTorsoMeters:Number(Math.max(0,o.distanceTo(c)-a.torso.radius).toFixed(4))})}this.seek(0,!1);let t=null,n=null;for(let r of this.replay.ticks){this.seek(r.tick,!1),this.renderCurrentTick(!1);let i=this.renderer.labRigs[0].position.x;n!==null&&(e.maxRootStepMeters=Math.max(e.maxRootStepMeters,Math.abs(i-n))),n=i;let a=r.snapshot.payload.fighters[0],o=Math.hypot(a.velocity_x,a.velocity_y),s=this.replay.ticks.slice(Math.max(0,r.tick-30),r.tick+1).some(e=>e.snapshot.payload.fighters[0].is_downed);if(o===0&&!a.is_downed&&!s&&r.snapshot.payload.phase===`fight`){let n=[this.jointWorld(0,`ankleL`),this.jointWorld(0,`ankleR`)].filter(e=>e!==null);t!==null&&n.length===2&&(e.footSlideMeters=Math.max(e.footSlideMeters,Number(Math.max(n[0].distanceTo(t[0]),n[1].distanceTo(t[1])).toFixed(4)))),t=n}else t=null}this.metricsEl.textContent=` contacts: ${e.contactAlignment.map(e=>`${e.kind}@t${e.tick} glove-head ${e.gloveToHeadMeters}m`).join(` · `)} · max root step ${e.maxRootStepMeters.toFixed(3)}m · foot slide ${e.footSlideMeters}m`,window.__labDebug={roots:this.renderer.labRigs.map(e=>e.position.toArray().map(e=>Number(e.toFixed(3)))),graphsActive:this.renderer.graphs!==null,heads:this.renderer.labRigs.map(e=>{let t=e.getObjectByName(gA.head??`head`);return t===void 0?null:t.getWorldPosition(new J).toArray().map(e=>Number(e.toFixed(3)))})};let r=new Blob([JSON.stringify(e,null,1)],{type:`application/json`}),i=document.createElement(`a`);i.href=URL.createObjectURL(r),i.download=`hands-lab-metrics.json`,i.click(),URL.revokeObjectURL(i.href)}setStatus(e){this.statusEl.textContent!==e&&(this.statusEl.textContent=e)}destroy(){this.destroyed||(this.destroyed=!0,cancelAnimationFrame(this.raf),this.renderer?.destroy(),this.root.replaceChildren())}};function Cz(e){let t=new Sz(e);return t.start().catch(t=>{e.querySelector(`[data-lab-status]`).textContent=`lab failed: ${String(t)}`}),()=>t.destroy()}var wz={referee:{outfit:xA,look:_D.referee},corner_blue:{outfit:SA,look:_D.blueCorner},corner_red:{outfit:CA,look:_D.redCorner},cutman:{outfit:wA,look:_D.blueCutman}},Tz=()=>({player_id:`lab`,x:0,y:0,facing:1,facing_x:0,facing_y:-1e3,velocity_x:0,velocity_y:0,stance:`orthodox`,style:`balanced`,defense:`none`,stamina:1e3,maximum_stamina:1e3,conditioning:1e3,guard:700,poise:600,trauma:{head:0,body:0,left_eye:0,right_eye:0,left_cut:0,right_cut:0,swelling:0,bleeding:0},knockdowns:0,warnings:0,deductions:0,stunned_ticks:0,is_downed:!1,action:null,action_hand:null,action_target:null,action_power:null,action_id:null,action_key:null,action_start_tick:0,action_startup_ticks:0,action_active_ticks:0,action_recovery_ticks:0,action_contact_tick:null,queued_actions:0,clinch_startup_ticks:0,clinch_ticks:0,is_foul_recovery_target:!1,taunt_ticks:0,corner_choice:null,get_up_prompt:null,get_up_meter:0,get_up_required:0,get_up_count:0,get_up_window_start_tick:0,get_up_window_end_tick:0,last_input_sequence:-1}),Ez=.45,Dz=.3,Oz={face:[.25,1.55,1],portrait:[.12,1.62,.62],eyes:[-.2,1.65,.44],floor:[1.62,.3,.18],hand:[.55,1.05,.75],front:[0,1.35,3.4],side:[3.4,1.3,.2],"three-quarter":[2.4,1.5,2.6],top:[.01,4.5,.6],back:[.2,1.4,-3.4],low:[1.8,.5,2.6],wide:[3.4,2.7,3.6]},kz=class{root;renderer;scene=new hh;camera;graph=null;boxer=null;trails=[];trailGlove=new J;treatEye=new J(0,1.22,.78);treatFacing=new J(0,0,-1);treatTarget=new J;skeletonHelper=null;effects=null;shieldClock=1/0;shieldEvent=0;mouth=new J;headTurn=new cm;burstRim=new Float32Array;burstStump={position:new J,quaternion:new cm,across:new J,scratch:new J};raf=0;previous=performance.now();elapsed=0;params=new URLSearchParams(window.location.search);reactionFired=!1;statusEl;constructor(e){this.root=e,e.innerHTML=`
HANDS POSE LABloading…
-
`,this.statusEl=e.querySelector(`[data-status]`);let t=e.querySelector(`[data-canvas]`);this.renderer=new ab({canvas:t,antialias:!0,preserveDrawingBuffer:!0}),this.renderer.setPixelRatio(Math.min(2,window.devicePixelRatio||1)),this.renderer.shadowMap.enabled=!0,this.renderer.toneMapping=4,this.camera=new ag(38,1,.1,60);let n=SD[this.params.get(`cam`)??`three-quarter`]??SD[`three-quarter`];this.camera.position.set(...n),this.camera.lookAt(0,this.params.get(`cam`)===`face`?1.5:1,0),this.setupLighting()}setupLighting(){this.scene.background=new Q(`#101318`),this.scene.add(new qh(`#8fa0c8`,`#1a1c22`,1.4));let e=new fg(`#fff1dc`,2.6);e.position.set(2.5,5,3),e.castShadow=!0,e.shadow.mapSize.set(2048,2048),this.scene.add(e);let t=new fg(`#9fb8ff`,1.2);t.position.set(-3,3,-3),this.scene.add(t);let n=new up(new hm(4,48),new sh({color:`#3c4a7a`,roughness:.9}));n.rotation.x=-Math.PI/2,n.receiveShadow=!0,this.scene.add(n);let r=new zg(4,16,8952251,3358810);r.position.y=.002,this.scene.add(r)}synthesize(e){let t=this.params.get(`pose`)??`idle`,n=this.params.get(`stance`)===`southpaw`?`southpaw`:`orthodox`,r={...xD(),stance:n},i=this.params.get(`trauma`);i===`light`&&(r.trauma={head:220,body:260,left_eye:190,right_eye:60,left_cut:40,right_cut:0,swelling:120,bleeding:60}),i===`heavy`&&(r.trauma={head:900,body:700,left_eye:720,right_eye:380,left_cut:520,right_cut:190,swelling:620,bleeding:520}),i===`cut`&&(r.trauma={head:420,body:120,left_eye:380,right_eye:120,left_cut:300,right_cut:0,swelling:260,bleeding:380});let a={...xD(),player_id:`other`,x:0,y:-150,facing_x:0,facing_y:1e3},o=Math.floor(e*30),s=/^(jab|straight|hook|uppercut)_(left|right)(_body)?(_power)?$/.exec(t);if(s!==null){let e=s[1],t=s[2],n=s[3]===`_body`?`body`:`head`,i=s[4]===`_power`?`power`:`normal`,a={jab:[4,2,7],straight:[6,2,10],hook:[7,3,12],uppercut:[8,2,13]}[e],c=a[0]+a[1]+a[2],l=Math.floor(o/(c+12))*(c+12)+6;o>=l&&o{let i=Math.min(.05,(r-this.previous)/1e3);this.previous=r,t===null?(this.elapsed=e===null?this.elapsed+i:Number(e),this.step(e===null?i:1/60)):this.step(0),this.renderer.render(this.scene,this.camera),this.raf=requestAnimationFrame(n)};this.raf=requestAnimationFrame(n)}step(e){let t=this.graph;if(t===null)return;let n=this.renderer.domElement.clientWidth,r=this.renderer.domElement.clientHeight;n>0&&r>0&&(this.renderer.domElement.width!==n||this.renderer.domElement.height!==r)&&(this.renderer.setSize(n,r,!1),this.camera.aspect=n/r,this.camera.updateProjectionMatrix());let{fighter:i,opponent:a,sampledTick:o,head:s}=this.synthesize(this.elapsed),c=this.params.get(`pose`)??``;if(/^(hit|block)_/.test(c)){let n=this.elapsed%1.6;if(this.params.has(`freeze`)?!this.reactionFired&&this.elapsed>=.02:n0);window.__poseLab={head:this.boxer.bone(`head`).getWorldPosition(new Z).toArray().map(e=>Number(e.toFixed(3))),gloveL:this.boxer.bone(`gloveL`).getWorldPosition(new Z).toArray().map(e=>Number(e.toFixed(3))),gloveR:this.boxer.bone(`gloveR`).getWorldPosition(new Z).toArray().map(e=>Number(e.toFixed(3))),ankleL:this.boxer.bone(`ankleL`).getWorldPosition(new Z).toArray().map(e=>Number(e.toFixed(3))),ankleR:this.boxer.bone(`ankleR`).getWorldPosition(new Z).toArray().map(e=>Number(e.toFixed(3)))}}destroy(){cancelAnimationFrame(this.raf),this.graph?.dispose(),this.renderer.dispose(),this.root.replaceChildren()}};function wD(e){let t=new CD(e);return t.start().catch(()=>void 0),()=>t.destroy()}var TD=document.querySelector(`#app`);if(TD===null)throw Error(`app_root_missing`);var ED,DD=new URLSearchParams(window.location.search),OD=window.location.pathname.endsWith(`/hands/lab`)||DD.get(`lab`)===`1`;if(window.location.pathname.endsWith(`/hands/model-lab`)||DD.get(`model-lab`)===`1`)ED=wD(TD);else if(OD)ED=bD(TD);else{let e=new gD(TD);e.start(),ED=()=>e.destroy()}window.addEventListener(`pagehide`,ED,{once:!0}); \ No newline at end of file +`,this.statusEl=e.querySelector(`[data-status]`);let t=e.querySelector(`[data-canvas]`);this.renderer=new PC({canvas:t,antialias:!0,preserveDrawingBuffer:!0}),this.renderer.setPixelRatio(Math.min(2,window.devicePixelRatio||1)),this.renderer.shadowMap.enabled=!0,this.renderer.toneMapping=4,this.camera=new My(38,1,.1,60);let n=Oz[this.params.get(`cam`)??`three-quarter`]??Oz[`three-quarter`];this.camera.position.set(...n),this.params.get(`cam`)===`hand`?this.camera.lookAt(.27,.84,.12):this.params.get(`cam`)===`portrait`?this.camera.lookAt(0,1.6,.05):this.params.get(`cam`)===`eyes`?this.camera.lookAt(.01,1.6,.06):this.params.get(`cam`)===`floor`?this.camera.lookAt(1.3,0,-.24):this.params.get(`cam`)===`wide`?this.camera.lookAt(0,.45,-.2):this.camera.lookAt(0,this.params.get(`cam`)===`face`?1.5:1,0),this.setupLighting()}setupLighting(){this.scene.background=new Y(`#101318`),this.scene.add(new by(`#8fa0c8`,`#1a1c22`,1.4));let e=new zy(`#fff1dc`,2.6);e.position.set(2.5,5,3),e.castShadow=!0,e.shadow.mapSize.set(2048,2048),this.scene.add(e);let t=new zy(`#9fb8ff`,1.2);t.position.set(-3,3,-3),this.scene.add(t);let n=new Ig(new V_(4,48),new Pv({color:`#3c4a7a`,roughness:.9}));n.rotation.x=-Math.PI/2,n.receiveShadow=!0,this.scene.add(n);let r=new mb(4,16,8952251,3358810);r.position.y=.002,this.scene.add(r)}synthesize(e){let t=this.params.get(`pose`)??`idle`,n=this.params.get(`stance`)===`southpaw`?`southpaw`:`orthodox`,r={...Tz(),stance:n},i=this.params.get(`trauma`);i===`light`&&(r.trauma={head:220,body:260,left_eye:190,right_eye:60,left_cut:40,right_cut:0,swelling:120,bleeding:60}),i===`heavy`&&(r.trauma={head:900,body:700,left_eye:720,right_eye:380,left_cut:520,right_cut:190,swelling:620,bleeding:520}),i===`cut`&&(r.trauma={head:420,body:120,left_eye:380,right_eye:120,left_cut:300,right_cut:0,swelling:260,bleeding:380});let a=i?.split(`,`).map(Number)??[];if(a.length===8&&a.every(Number.isFinite)){let[e,t,n,i,o,s,c,l]=a;r.trauma={head:e,body:t,left_eye:n,right_eye:i,left_cut:o,right_cut:s,swelling:c,bleeding:l}}let o={...Tz(),player_id:`other`,x:0,y:-150,facing_x:0,facing_y:1e3},s=Math.floor(e*30),c=/^(jab|straight|hook|uppercut)_(left|right)(_body)?(_power)?$/.exec(t);if(c!==null){let e=c[1],t=c[2],n=c[3]===`_body`?`body`:`head`,i=c[4]===`_power`?`power`:`normal`,a={jab:[4,2,7],straight:[6,2,10],hook:[7,3,12],uppercut:[8,2,13]}[e],o=a[0]+a[1]+a[2],l=Math.floor(s/(o+12))*(o+12)+6;s>=l&&s=Ez&&t<3.4,r.stunned_ticks=t=Dz&&(t===null||e{let i=Math.min(.05,(r-this.previous)/1e3);this.previous=r,t===null?(this.elapsed=e===null?this.elapsed+i:Number(e),this.step(e===null?i:1/60)):this.step(0),this.renderer.render(this.scene,this.camera),this.raf=requestAnimationFrame(n)};this.raf=requestAnimationFrame(n)}step(e){let t=this.graph;if(t===null)return;let n=this.renderer.domElement.clientWidth,r=this.renderer.domElement.clientHeight;n>0&&r>0&&(this.renderer.domElement.width!==n||this.renderer.domElement.height!==r)&&(this.renderer.setSize(n,r,!1),this.camera.aspect=n/r,this.camera.updateProjectionMatrix());let{fighter:i,opponent:a,sampledTick:o,head:s}=this.synthesize(this.elapsed),c=this.params.get(`pose`)??``;if(/^(hit|block)_/.test(c)){let n=this.elapsed%1.6;if(this.params.has(`freeze`)?!this.reactionFired&&this.elapsed>=.02:n=Dz-.02){this.reactionFired=!0;let e=this.params.get(`hit`)??`straight`,n=e===`body`?`hook`:e;t.react(`hit`,e===`body`?`body`:`head`,1,n,this.params.get(`hand`)===`left`?`left`:`right`,Number(this.params.get(`amount`)??420))}this.params.get(`pose`)===`knockdown`&&this.params.get(`fall`)===`prone`&&this.elapsed%6=.02:this.elapsed%1.63)){this.shieldClock=0,this.shieldEvent+=1;let e=this.boxer.bone(`head`);e.getWorldQuaternion(this.headTurn),e.getWorldPosition(this.mouth).add(new J(0,.045,.12).applyQuaternion(this.headTurn)),this.effects.ejectMouthpiece(0,this.mouth,this.headTurn,1,this.shieldEvent,1920728,!0)}this.effects?.update(e);for(let[t,n]of[`gloveL`,`gloveR`].entries())this.boxer.rig.bones[n].getWorldPosition(this.trailGlove),this.trails[t]?.update(this.trailGlove,e,this.camera.position,e>0);window.__poseLab={head:this.boxer.bone(`head`).getWorldPosition(new J).toArray().map(e=>Number(e.toFixed(3))),gloveL:this.boxer.bone(`gloveL`).getWorldPosition(new J).toArray().map(e=>Number(e.toFixed(3))),gloveR:this.boxer.bone(`gloveR`).getWorldPosition(new J).toArray().map(e=>Number(e.toFixed(3))),ankleL:this.boxer.bone(`ankleL`).getWorldPosition(new J).toArray().map(e=>Number(e.toFixed(3))),ankleR:this.boxer.bone(`ankleR`).getWorldPosition(new J).toArray().map(e=>Number(e.toFixed(3))),kneeL:this.boxer.bone(`kneeL`).getWorldPosition(new J).toArray().map(e=>Number(e.toFixed(3))),kneeR:this.boxer.bone(`kneeR`).getWorldPosition(new J).toArray().map(e=>Number(e.toFixed(3))),metrics:this.boxer.rig.metrics,mouthpiece:this.effects!==null&&this.effects.mouthpiecePosition(0,this.mouth)?this.mouth.toArray().map(e=>Number(e.toFixed(3))):null}}destroy(){cancelAnimationFrame(this.raf),this.effects?.dispose(),this.graph?.dispose(),this.renderer.dispose(),this.root.replaceChildren()}};function Az(e){let t=new kz(e);return t.start().catch(()=>void 0),()=>t.destroy()}var jz=document.querySelector(`#app`);if(jz===null)throw Error(`app_root_missing`);var Mz,Nz=new URLSearchParams(window.location.search),Pz=window.location.pathname.endsWith(`/hands/lab`)||Nz.get(`lab`)===`1`;if(window.location.pathname.endsWith(`/hands/model-lab`)||Nz.get(`model-lab`)===`1`)Mz=Az(jz);else if(Pz)Mz=Cz(jz);else{let e=new yz(jz);e.start(),Mz=()=>e.destroy()}window.addEventListener(`pagehide`,Mz,{once:!0}); \ No newline at end of file diff --git a/src/intelstream/hands/types.py b/src/intelstream/hands/types.py index 0fbc8f26..e7417ff1 100644 --- a/src/intelstream/hands/types.py +++ b/src/intelstream/hands/types.py @@ -31,6 +31,14 @@ class Stance(StrEnum): SOUTHPAW = "southpaw" +class FighterStyle(StrEnum): + BALANCED = "balanced" + BOXER = "boxer" + SLUGGER = "slugger" + SWARMER = "swarmer" + COUNTER_PUNCHER = "counter_puncher" + + class DefensivePose(StrEnum): NONE = "none" GUARD_HIGH = "guard_high" @@ -58,6 +66,16 @@ class ActionKind(StrEnum): GET_UP_LEFT = "get_up_left" GET_UP_RIGHT = "get_up_right" TAUNT = "taunt" + CORNER_CUT = "corner_cut" + CORNER_SWELLING = "corner_swelling" + CORNER_BREATH = "corner_breath" + + +class CornerChoice(StrEnum): + CUT = "cut" + SWELLING = "swelling" + BREATH = "breath" + BALANCED = "balanced" class MatchPhase(StrEnum): @@ -143,6 +161,7 @@ class FighterSnapshot: velocity_x: int velocity_y: int stance: Stance + style: FighterStyle defense: DefensivePose stamina: int maximum_stamina: int @@ -171,6 +190,7 @@ class FighterSnapshot: clinch_ticks: int is_foul_recovery_target: bool taunt_ticks: int + corner_choice: CornerChoice | None get_up_prompt: ActionKind | None get_up_meter: int get_up_required: int diff --git a/tests/test_database.py b/tests/test_database.py index 9291e880..8bf4a6f4 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -1,8 +1,9 @@ import asyncio +import json from datetime import UTC, datetime, timedelta import pytest -from sqlalchemy import select, text +from sqlalchemy import select, text, update from sqlalchemy.exc import IntegrityError, OperationalError from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine @@ -29,7 +30,7 @@ SuckBoobsStats, ) from intelstream.database.repository import Repository -from intelstream.hands.types import FinishMethod, JudgeCard, MatchResult +from intelstream.hands.types import FighterStyle, FinishMethod, JudgeCard, MatchResult @pytest.fixture @@ -2272,6 +2273,68 @@ async def test_records_win_loss_ko_streak_best_and_zero_sum( assert match.player_two_rating_after == 984 assert repr(match) == "" + async def test_a_rated_bout_stores_both_fighters_styles(self, repository: Repository) -> None: + styled = await repository.record_hands_match( + self.result("styled"), styles=(FighterStyle.SWARMER, FighterStyle.COUNTER_PUNCHER) + ) + plain = await repository.record_hands_match(self.result("plain")) + + assert json.loads(styled.result_json) == { + "player_one_damage": 250, + "player_one_knockdowns": 0, + "player_one_style": "swarmer", + "player_two_damage": 100, + "player_two_knockdowns": 2, + "player_two_style": "counter_puncher", + } + assert "player_one_style" not in json.loads(plain.result_json) + + async def test_a_doctor_stoppage_counts_as_a_knockout_in_the_record( + self, repository: Repository + ) -> None: + await repository.record_hands_match( + self.result("cut-stoppage", finish_method=FinishMethod.DOCTOR_STOPPAGE) + ) + await repository.record_hands_match( + self.result("cards", finish_method=FinishMethod.DECISION) + ) + one = await repository.get_hands_rating("guild-a", "user-1") + + assert one is not None + assert (one.wins, one.knockouts) == (2, 1) + + async def test_knockouts_are_recounted_from_the_stored_bouts_at_start( + self, repository: Repository + ) -> None: + for match_id, method in ( + ("ko", FinishMethod.KO), + ("cut", FinishMethod.DOCTOR_STOPPAGE), + ("cards", FinishMethod.DECISION), + ): + await repository.record_hands_match(self.result(match_id, finish_method=method)) + await repository.record_hands_match( + self.result("other-guild", guild_id="guild-b", finish_method=FinishMethod.TKO) + ) + # A record stored before doctor stoppages counted, and one somebody else's bout left alone. + async with repository.session() as session: + await session.execute( + update(HandsRating) + .where(HandsRating.guild_id == "guild-a", HandsRating.user_id == "user-1") + .values(knockouts=1) + ) + await session.commit() + + await repository.initialize() + await repository.initialize() + + one = await repository.get_hands_rating("guild-a", "user-1") + two = await repository.get_hands_rating("guild-a", "user-2") + elsewhere = await repository.get_hands_rating("guild-b", "user-1") + assert one is not None and two is not None and elsewhere is not None + assert (one.wins, one.knockouts) == (3, 2) + assert two.knockouts == 0 + assert elsewhere.knockouts == 1 + async def test_draws_reset_streak_and_duplicate_match_is_idempotent( self, repository: Repository ) -> None: diff --git a/tests/test_hands/test_auth.py b/tests/test_hands/test_auth.py index 8fc09a56..955f990a 100644 --- a/tests/test_hands/test_auth.py +++ b/tests/test_hands/test_auth.py @@ -188,6 +188,23 @@ async def test_state_is_one_use_expiring_bounded_and_instance_bound() -> None: await auth.close() +async def test_a_live_state_names_its_instance_without_being_spent() -> None: + monotonic = Clock() + auth = make_auth(monotonic, Clock()) + state, _ = await auth.begin(INSTANCE) + + assert auth.instance_for_state(state) == INSTANCE + assert auth.instance_for_state(state) == INSTANCE + assert auth.instance_for_state("forged") is None + assert auth.instance_for_state(42) is None + await auth.exchange(code="sdk-code", state=state) + assert auth.instance_for_state(state) is None + expiring, _ = await auth.begin(INSTANCE) + monotonic.value += 11 + assert auth.instance_for_state(expiring) is None + await auth.close() + + @pytest.mark.parametrize( "overrides", [ diff --git a/tests/test_hands/test_balance.py b/tests/test_hands/test_balance.py new file mode 100644 index 00000000..0d2e2616 --- /dev/null +++ b/tests/test_hands/test_balance.py @@ -0,0 +1,446 @@ +from __future__ import annotations + +import pytest +from scripts.hands_balance import bout_styles, make_player + +from intelstream.hands.engine import BoxingEngine, EngineConfig +from intelstream.hands.rules import ( + BLOCK_POISE_PERCENT, + BODY_COLLAPSE_COOLDOWN_TICKS, + BODY_COLLAPSE_STAMINA, + BODY_COLLAPSE_TRAUMA, + BODY_TRAUMA_PER_DAMAGE_PERCENT, + CUT_PER_DAMAGE_PERCENT, + EYE_TRAUMA_PER_DAMAGE_PERCENT, + FLINCH_BASE_TICKS, + FLINCH_DAMAGE_DIVISOR, + GET_UP_BASE, + GET_UP_PER_KNOCKDOWN, + GET_UP_STAMINA, + GET_UP_TRAUMA_DIVISOR, + GUARD_BLOCK_MINIMUM, + GUARD_DAMAGE_PERCENT, + HEAD_TRAUMA_PER_DAMAGE_PERCENT, + JAB_CUT_PERCENT, + JAB_SWELLING_PERCENT, + MAX_POISE, + POISE_CEILING_FLOOR, + POISE_DAMAGE_PERCENT, + POISE_REGEN_EVERY_TICKS, + PUNCH_RULES, + ROCKED_BASE_TICKS, + ROCKED_HURT_POISE, + ROCKED_IMMUNITY_TICKS, + STUN_CHAIN_MAX_TICKS, + STUN_IMMUNITY_TICKS, + SWELLING_PER_DAMAGE_PERCENT, + poise_ceiling, +) +from intelstream.hands.types import ( + CombatEvent, + DefensivePose, + FinishMethod, + Hand, + MatchPhase, + Power, + PunchAction, + PunchClass, + Target, +) +from tests.test_hands.test_engine import command, make_engine, punch + + +def land( + engine: BoxingEngine, + action: PunchAction, + *, + defense: DefensivePose = DefensivePose.NONE, +) -> list[CombatEvent]: + """Throws `action` from one at two, two holding `defense`, and returns the contact tick's events.""" + engine.step( + { + "one": command(engine.fighter("one").last_sequence + 1, action=action), + "two": command(engine.fighter("two").last_sequence + 1, defense=defense), + } + ) + for _ in range(40): + events = list(engine.step().events) + if any(event.kind in ("hit", "counter_hit", "block", "perfect_block") for event in events): + return events + raise AssertionError("the punch never arrived") + + +def kinds(events: list[CombatEvent]) -> list[str]: + return [event.kind for event in events] + + +def hit_amount(events: list[CombatEvent]) -> int: + return next(event.amount for event in events if event.kind in ("hit", "counter_hit")) + + +def settled_guard(engine: BoxingEngine, pose: DefensivePose) -> None: + """Two raises a guard and holds it long enough that it is no longer a perfect block.""" + engine.step({"two": command(engine.fighter("two").last_sequence + 1, defense=pose)}) + for _ in range(10): + engine.step() + + +def test_a_clean_shot_makes_a_fresh_fighter_flinch_without_being_rocked() -> None: + engine = make_engine() + events = land(engine, punch(PunchClass.STRAIGHT)) + damage = hit_amount(events) + assert damage >= 36 + assert "stun" not in kinds(events) + assert ( + engine.fighter("two").stunned_ticks == FLINCH_BASE_TICKS + damage // FLINCH_DAMAGE_DIVISOR + ) + assert engine.fighter("two").stunned_ticks < ROCKED_BASE_TICKS + + +def test_a_big_counter_rocks_a_fighter_and_says_so() -> None: + engine = make_engine() + engine.fighter("one").counter_ticks = 30 + events = land(engine, punch(PunchClass.STRAIGHT)) + assert "counter_hit" in kinds(events) + assert "stun" in kinds(events) + assert engine.fighter("two").stunned_ticks >= ROCKED_BASE_TICKS + + +def test_a_big_power_shot_rocks_a_fresh_fighter() -> None: + engine = make_engine() + events = land(engine, punch(PunchClass.STRAIGHT, power=Power.POWER)) + assert "counter_hit" not in kinds(events) + assert "stun" in kinds(events) + + +def test_a_hurt_fighter_is_rocked_by_an_ordinary_shot() -> None: + engine = make_engine() + engine.fighter("two").poise = ROCKED_HURT_POISE + 20 + events = land(engine, punch(PunchClass.STRAIGHT)) + assert "stun" in kinds(events) + assert engine.fighter("two").stunned_ticks >= ROCKED_BASE_TICKS + + +def test_a_running_stun_is_not_started_again_and_no_stun_passes_the_chain_limit() -> None: + engine = make_engine() + fighter = engine.fighter("two") + fighter.stunned_ticks = 10 + fighter.stun_chain_ticks = STUN_CHAIN_MAX_TICKS - 25 + # However hard it lands, a punch on a man who is still stunned does not start his stun again. + assert not engine._stun(fighter, 8, rocked=False) + assert not engine._stun(fighter, 30, rocked=True) + assert (fighter.stunned_ticks, fighter.stun_chain_ticks) == (10, STUN_CHAIN_MAX_TICKS - 25) + fighter.stunned_ticks = 0 + fighter.stun_chain_ticks = 0 + assert engine._stun(fighter, STUN_CHAIN_MAX_TICKS + 20, rocked=True) + assert fighter.stunned_ticks == STUN_CHAIN_MAX_TICKS + + +def test_a_stun_keeps_counting_toward_the_chain_and_then_leaves_a_moment_of_clear_head() -> None: + engine = make_engine() + fighter = engine.fighter("two") + assert engine._stun(fighter, 6, rocked=True) + for _ in range(6): + engine.step() + assert fighter.stunned_ticks == 0 + assert fighter.stun_chain_ticks == 0 + # Clear-headed: not even a rocking shot stops him again for a few ticks, a flinch for longer. + assert not engine._stun(fighter, 8, rocked=False) + assert not engine._stun(fighter, 20, rocked=True) + for _ in range(ROCKED_IMMUNITY_TICKS - 1): + engine.step() + assert not engine._stun(fighter, 20, rocked=True) + engine.step() + assert not engine._stun(fighter, 8, rocked=False) + assert engine._stun(fighter, 20, rocked=True) + fighter.stunned_ticks = 0 + for _ in range(STUN_IMMUNITY_TICKS + 1): + engine.step() + assert engine._stun(fighter, 8, rocked=False) + + +@pytest.mark.parametrize("lands_after", range(-1, ROCKED_IMMUNITY_TICKS + 2)) +@pytest.mark.parametrize("stunned_first", [False, True]) +def test_a_power_shot_landing_as_a_stun_wears_off_cannot_start_another_at_once( + lands_after: int, stunned_first: bool +) -> None: + """A power straight thrown during a stun, timed to land the tick it wears off or just after, + with the stunned fighter taking his turn before or after the puncher on that tick.""" + engine = make_engine() + two = engine.fighter("two") + if stunned_first: + engine.step() + assert engine._stun(two, 30, rocked=True) + worn_off = engine.tick + 30 + straight = punch(PunchClass.STRAIGHT, power=Power.POWER) + startup = PUNCH_RULES[(PunchClass.STRAIGHT, Target.HEAD, Power.POWER)].startup + while engine.tick + 1 + startup < worn_off + lands_after: + engine.step() + engine.step({"one": command(1, action=straight)}) + attack = engine.fighter("one").attack + assert attack is not None and attack.start_tick + attack.rule.startup == worn_off + lands_after + events: list[CombatEvent] = [] + while not attack.resolved: + events.extend(engine.step().events) + assert "hit" in kinds(events) + assert hit_amount(events) >= 70 + rocked = "stun" in kinds(events) + assert rocked is (lands_after >= ROCKED_IMMUNITY_TICKS) + if not rocked: + assert two.stunned_ticks == 0 or lands_after < 0 + + +def test_a_guarded_punch_never_stuns_and_takes_a_quarter_of_its_poise() -> None: + open_engine = make_engine() + land(open_engine, punch(PunchClass.STRAIGHT)) + open_loss = MAX_POISE - open_engine.fighter("two").poise + + guarded = make_engine() + settled_guard(guarded, DefensivePose.GUARD_HIGH) + before = guarded.fighter("two").poise + events = land(guarded, punch(PunchClass.STRAIGHT), defense=DefensivePose.GUARD_HIGH) + assert "block" in kinds(events) + assert guarded.fighter("two").stunned_ticks == 0 + loss = before - guarded.fighter("two").poise + assert 0 < loss <= open_loss * BLOCK_POISE_PERCENT // 100 + 1 + + +def test_a_perfect_block_costs_no_poise() -> None: + engine = make_engine() + engine.step({"one": command(1, action=punch(PunchClass.STRAIGHT))}) + attack = engine.fighter("one").attack + assert attack is not None + while attack.age < attack.rule.startup - 2: + engine.step() + before = engine.fighter("two").poise + engine.step({"two": command(1, defense=DefensivePose.GUARD_HIGH)}) + for _ in range(10): + events = kinds(list(engine.step().events)) + if "perfect_block" in events: + break + else: + raise AssertionError("no perfect block") + defender = engine.fighter("two") + # A perfect block still lets a little through, which only lowers the ceiling poise returns to. + assert defender.poise >= min(before, poise_ceiling(defender.trauma.head)) + + +def test_a_worn_out_guard_stops_nothing_and_is_not_broken_again() -> None: + engine = make_engine() + settled_guard(engine, DefensivePose.GUARD_HIGH) + engine.fighter("two").guard = GUARD_BLOCK_MINIMUM - 20 + events = land(engine, punch(PunchClass.JAB, hand=Hand.LEFT), defense=DefensivePose.GUARD_HIGH) + assert "block" not in kinds(events) + assert "guard_break" not in kinds(events) + assert "hit" in kinds(events) + + +def test_a_block_that_wears_the_guard_too_thin_to_stop_a_punch_breaks_it() -> None: + engine = make_engine() + settled_guard(engine, DefensivePose.GUARD_HIGH) + two = engine.fighter("two") + two.guard = GUARD_BLOCK_MINIMUM + 15 + events = land(engine, punch(PunchClass.STRAIGHT), defense=DefensivePose.GUARD_HIGH) + assert "block" in kinds(events) + assert "guard_break" in kinds(events) + assert 0 < two.guard < GUARD_BLOCK_MINIMUM + assert two.stunned_ticks > 0 + while two.stunned_ticks: + engine.step() + # Still holding it up, but the next punch goes straight through. + events = land(engine, punch(PunchClass.STRAIGHT), defense=DefensivePose.GUARD_HIGH) + assert two.defense is DefensivePose.GUARD_HIGH + assert "hit" in kinds(events) + assert "block" not in kinds(events) and "guard_break" not in kinds(events) + # Once it has come back past the minimum, it blocks again. + while two.guard < GUARD_BLOCK_MINIMUM: + engine.step() + events = land(engine, punch(PunchClass.JAB, hand=Hand.LEFT), defense=DefensivePose.GUARD_HIGH) + assert "block" in kinds(events) + + +def test_a_blocked_punch_wears_the_guard_by_its_share() -> None: + engine = make_engine() + settled_guard(engine, DefensivePose.GUARD_HIGH) + before = engine.fighter("two").guard + land(engine, punch(PunchClass.STRAIGHT), defense=DefensivePose.GUARD_HIGH) + rule = PUNCH_RULES[(PunchClass.STRAIGHT, Target.HEAD, Power.NORMAL)] + worn = rule.guard_damage * GUARD_DAMAGE_PERCENT // 100 + assert before - engine.fighter("two").guard in range(worn - 3, worn + 1) + + +def test_a_held_guard_comes_back_but_slower_than_a_rested_one() -> None: + held = make_engine() + held.fighter("two").guard = 300 + held.step({"two": command(1, defense=DefensivePose.GUARD_HIGH)}) + rested = make_engine() + rested.fighter("two").guard = 300 + for _ in range(24): + held.step() + rested.step() + assert 300 < held.fighter("two").guard < rested.fighter("two").guard + + +def test_a_beating_lowers_the_poise_a_fighter_comes_back_to() -> None: + assert poise_ceiling(0) == MAX_POISE + assert poise_ceiling(1000) < poise_ceiling(400) < MAX_POISE + assert poise_ceiling(1400) == POISE_CEILING_FLOOR + engine = make_engine() + fighter = engine.fighter("two") + fighter.trauma.head = 1000 + fighter.poise = 100 + for _ in range(MAX_POISE * POISE_REGEN_EVERY_TICKS): + engine.step() + assert fighter.poise == poise_ceiling(1000) + + +def test_poise_comes_back_one_point_every_few_ticks() -> None: + engine = make_engine() + fighter = engine.fighter("two") + fighter.poise = 300 + for _ in range(POISE_REGEN_EVERY_TICKS * 10): + engine.step() + assert fighter.poise == 310 + + +def test_a_punch_takes_its_share_of_poise() -> None: + engine = make_engine() + land(engine, punch(PunchClass.JAB, hand=Hand.LEFT)) + rule = PUNCH_RULES[(PunchClass.JAB, Target.HEAD, Power.NORMAL)] + taken = MAX_POISE - engine.fighter("two").poise + share = rule.poise_damage * POISE_DAMAGE_PERCENT // 100 + assert share - 1 <= taken <= share + + +def test_only_a_clean_punch_marks_the_face() -> None: + clean = make_engine() + events = land(clean, punch(PunchClass.STRAIGHT)) + damage = hit_amount(events) + trauma = clean.fighter("two").trauma + assert trauma.head == damage * HEAD_TRAUMA_PER_DAMAGE_PERCENT // 100 + assert trauma.left_eye == damage * EYE_TRAUMA_PER_DAMAGE_PERCENT // 100 + assert trauma.swelling == damage * SWELLING_PER_DAMAGE_PERCENT // 100 + + guarded = make_engine() + settled_guard(guarded, DefensivePose.GUARD_HIGH) + events = land(guarded, punch(PunchClass.STRAIGHT), defense=DefensivePose.GUARD_HIGH) + assert "block" in kinds(events) + trauma = guarded.fighter("two").trauma + assert trauma.head > 0 + assert (trauma.left_eye, trauma.right_eye, trauma.swelling, trauma.left_cut) == (0, 0, 0, 0) + + +def test_a_cut_opens_by_half_the_damage_once_the_eye_is_marked() -> None: + engine = make_engine() + engine.fighter("two").trauma.left_eye = 300 + damage = hit_amount(land(engine, punch(PunchClass.STRAIGHT))) + assert engine.fighter("two").trauma.left_cut == damage * CUT_PER_DAMAGE_PERCENT // 100 + + +def test_a_jab_seldom_splits_the_skin_and_swells_the_face_less() -> None: + engine = make_engine() + engine.fighter("two").trauma.right_eye = 300 + damage = hit_amount(land(engine, punch(PunchClass.JAB, hand=Hand.LEFT))) + trauma = engine.fighter("two").trauma + cut = damage * CUT_PER_DAMAGE_PERCENT // 100 * JAB_CUT_PERCENT // 100 + swelling = damage * SWELLING_PER_DAMAGE_PERCENT // 100 * JAB_SWELLING_PERCENT // 100 + assert trauma.right_cut == cut < damage * CUT_PER_DAMAGE_PERCENT // 100 + assert trauma.swelling == swelling < damage * SWELLING_PER_DAMAGE_PERCENT // 100 + + +def test_by_default_the_doctor_lets_a_bad_cut_and_a_swollen_face_box_on() -> None: + defaults = EngineConfig() + engine = make_engine( + doctor_cut_threshold=defaults.doctor_cut_threshold, + doctor_swelling_threshold=defaults.doctor_swelling_threshold, + ) + trauma = engine.fighter("two").trauma + trauma.left_eye, trauma.left_cut, trauma.swelling = 500, 740, 850 + land(engine, punch(PunchClass.STRAIGHT)) + assert trauma.left_cut > 740 and trauma.swelling > 850 + assert engine.result is None + + +@pytest.mark.parametrize("seed", [1, 2]) +def test_a_one_sided_three_rounder_leaves_a_visible_cut( + seed: int, monkeypatch: pytest.MonkeyPatch +) -> None: + """Three rounds of a skilled player outboxing the rookie mark the loser's face, short of the + doctor: a brow cut the client draws in full (220), an eye past where it starts to close (330) + and swollen shut at least once. Marking slowed so far once that faces stayed clean.""" + # Nothing here reads a checksum, and hashing every tick is most of what a bout costs. + monkeypatch.setattr(BoxingEngine, "_checksum", lambda _engine, _fighters: "") + styles = bout_styles("skilled", "rookie", seed) + engine = BoxingEngine( + match_id=f"cut-{seed}", + activity_instance_id="balance", + guild_id="balance", + player_one_id="one", + player_two_id="two", + seed=seed, + config=EngineConfig(), + styles=styles, + ) + players = ( + ("one", make_player("skilled", "one", "two", seed * 2 + 1, styles[0])), + ("two", make_player("rookie", "two", "one", seed * 2 + 2, styles[1])), + ) + worst_cut = worst_eye = eyes_shut = 0 + while engine.result is None: + for player_id, player in players: + command = player.decide(engine) + if command is not None: + engine.submit_input(player_id, command) + snapshot = engine.step() + trauma = engine.fighter("two").trauma + worst_cut = max(worst_cut, trauma.left_cut, trauma.right_cut) + worst_eye = max(worst_eye, trauma.left_eye, trauma.right_eye) + eyes_shut += sum(event.kind == "eye_shut" for event in snapshot.events) + + assert engine.result.winner_id == "one" + assert engine.result.finish_method is not FinishMethod.DOCTOR_STOPPAGE + assert worst_cut >= 220 + assert worst_eye >= 330 + assert eyes_shut >= 1 + + +def test_body_trauma_counts_the_damage_once() -> None: + engine = make_engine() + damage = hit_amount(land(engine, punch(PunchClass.HOOK, hand=Hand.LEFT, target=Target.BODY))) + assert engine.fighter("two").trauma.body == damage * BODY_TRAUMA_PER_DAMAGE_PERCENT // 100 + + +def broken_body(engine: BoxingEngine) -> None: + fighter = engine.fighter("two") + fighter.trauma.body = BODY_COLLAPSE_TRAUMA + 100 + fighter.stamina = BODY_COLLAPSE_STAMINA - 100 + fighter.conditioning = 500 + + +def test_a_body_collapse_cannot_follow_another_inside_its_cooldown() -> None: + body_hook = punch(PunchClass.HOOK, hand=Hand.LEFT, target=Target.BODY, power=Power.POWER) + fresh = make_engine() + broken_body(fresh) + assert "body_collapse" in kinds(land(fresh, body_hook)) + + recent = make_engine() + broken_body(recent) + recent.fighter("two").body_collapse_at_tick = recent.tick - BODY_COLLAPSE_COOLDOWN_TICKS // 2 + assert "body_collapse" not in kinds(land(recent, body_hook)) + + +def test_getting_up_leaves_enough_stamina_to_stand_and_is_harder_for_a_battered_man() -> None: + engine = make_engine() + downed = engine.fighter("two") + downed.stamina = 0 + engine._knock_down(downed, engine.fighter("one")) + assert engine._get_up_required(downed) == GET_UP_BASE + GET_UP_PER_KNOCKDOWN + downed.trauma.head = 900 + assert engine._get_up_required(downed) == ( + GET_UP_BASE + GET_UP_PER_KNOCKDOWN + 900 // GET_UP_TRAUMA_DIVISOR + ) + downed.get_up_meter = 1_000 + while engine.phase is MatchPhase.KNOCKDOWN: + engine.step() + assert downed.stamina >= min(downed.maximum_stamina, GET_UP_STAMINA) + assert downed.stamina > BODY_COLLAPSE_STAMINA diff --git a/tests/test_hands/test_cpu.py b/tests/test_hands/test_cpu.py new file mode 100644 index 00000000..2dbe82d0 --- /dev/null +++ b/tests/test_hands/test_cpu.py @@ -0,0 +1,1323 @@ +from __future__ import annotations + +from collections import Counter +from dataclasses import replace +from itertools import pairwise +from math import cos, hypot, radians, sin + +import pytest +from scripts.hands_balance import ( + HUMAN_REACTION_TICKS, + Lag, + LaggedPlayer, + ScriptedHuman, + bout_styles, + make_player, + play, + style_matrix, +) + +from intelstream.hands.cpu import ( + CPU_STYLES, + PROFILES, + CpuBrain, + CpuLevel, + _evades, + _rope_room, + cpu_player_id, + cpu_style, + styled_profile, +) +from intelstream.hands.engine import EVASION_TICKS, BoxingEngine, EngineConfig +from intelstream.hands.rules import ( + BLIND_SIDE_EYE_THRESHOLD, + BODY_COLLAPSE_STAMINA, + BODY_COLLAPSE_TRAUMA, + COMPATIBLE_COMBO_CHAINS, + PUNCH_RULES, + poise_ceiling, +) +from intelstream.hands.types import ( + ActionKind, + DefensivePose, + FighterStyle, + Hand, + InputCommand, + MatchPhase, + MovementAction, + Power, + PunchAction, + PunchClass, + Target, +) + + +def engine_at(distance: int, *, seed: int = 3, **config: int) -> BoxingEngine: + engine = BoxingEngine( + match_id="match", + activity_instance_id="instance", + guild_id="guild", + player_one_id="human", + player_two_id="cpu", + seed=seed, + config=EngineConfig(countdown_ticks=0, flash_ko_enabled=False, **config), + ) + engine.fighter("human").x = -(distance // 2) + engine.fighter("cpu").x = distance - distance // 2 + return engine + + +def always(brain: CpuBrain) -> CpuBrain: + """Every dice roll succeeds and every reaction is the quickest, so a test sees the brain's best + answer.""" + brain._roll = lambda _percent: True # type: ignore[method-assign] + brain.profile = replace(brain.profile, reaction_spread=0) + return brain + + +def never(brain: CpuBrain) -> CpuBrain: + brain._roll = lambda _percent: False # type: ignore[method-assign] + return brain + + +def throw(engine: BoxingEngine, action: PunchAction, sequence: int = 1) -> None: + engine.submit_input("human", InputCommand(sequence, engine.tick, actions=(action,))) + + +def run(engine: BoxingEngine, brain: CpuBrain, ticks: int) -> list[InputCommand]: + commands: list[InputCommand] = [] + for _ in range(ticks): + command = brain.decide(engine) + if command is not None: + commands.append(command) + engine.submit_input(brain.player_id, command) + engine.step() + return commands + + +def punches(commands: list[InputCommand]) -> list[PunchAction]: + return [ + action + for command in commands + for action in command.actions + if isinstance(action, PunchAction) + ] + + +def kinds(commands: list[InputCommand]) -> list[ActionKind]: + return [action.kind for command in commands for action in command.actions] + + +@pytest.mark.parametrize("punch_class", list(PunchClass)) +@pytest.mark.parametrize("hand", list(Hand)) +@pytest.mark.parametrize("target", list(Target)) +@pytest.mark.parametrize("distance", [100, 140]) +def test_the_evasion_it_picks_is_one_the_engine_accepts( + punch_class: PunchClass, hand: Hand, target: Target, distance: int +) -> None: + action = PunchAction(hand, punch_class, target) + rule = PUNCH_RULES[(punch_class, target, Power.NORMAL)] + pose = _evades(action, rule, distance, 0) + engine = engine_at(distance) + defender = engine.fighter("cpu") + if pose is None: + for candidate in ( + DefensivePose.SLIP_LEFT, + DefensivePose.SLIP_RIGHT, + DefensivePose.WEAVE, + DefensivePose.PULL, + ): + defender.defense = candidate + defender.evasion_ticks = 5 + assert not engine._evades(defender, action, distance * distance, rule.reach, 0) + return + defender.defense = pose + defender.evasion_ticks = 5 + assert engine._evades(defender, action, distance * distance, rule.reach, 0) + + +def test_a_punch_is_not_answered_before_the_reaction_delay() -> None: + engine = engine_at(150) + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 1)) + brain._attack = lambda *_args: None # type: ignore[method-assign] + throw(engine, PunchAction(Hand.RIGHT, PunchClass.STRAIGHT, Target.HEAD, Power.POWER)) + engine.step() + started = engine.fighter("human").attack + assert started is not None + answered_at = None + for _ in range(12): + command = brain.decide(engine) + assert command is not None + if command.actions: + answered_at = engine.tick + break + engine.submit_input("cpu", command) + engine.step() + assert answered_at == started.start_tick + PROFILES[CpuLevel.CHAMPION].reaction_ticks + + +@pytest.mark.parametrize("level", list(CpuLevel)) +def test_each_punch_is_noticed_after_a_reaction_of_its_own(level: CpuLevel) -> None: + profile = PROFILES[level] + delays = [] + for seed in range(24): + engine = engine_at(150, seed=seed) + engine.checksums = False + # A worn-out puncher's straight is slow enough to answer after any reaction. + engine.fighter("human").conditioning = 0 + brain = CpuBrain("cpu", "human", level, seed) + brain._roll = lambda _percent: True # type: ignore[method-assign] + brain._attack = lambda *_args: None # type: ignore[method-assign] + throw(engine, PunchAction(Hand.RIGHT, PunchClass.STRAIGHT, Target.HEAD, Power.POWER)) + engine.step() + attack = engine.fighter("human").attack + assert attack is not None + for _ in range(30): + command = brain.decide(engine) + assert command is not None + if brain._read_attack is not None: + delays.append(engine.tick - attack.start_tick) + break + engine.submit_input("cpu", command) + engine.step() + assert len(delays) == 24 + assert profile.reaction_ticks == min(delays) + assert max(delays) == profile.reaction_ticks + profile.reaction_spread + assert len(set(delays)) == profile.reaction_spread + 1 + + +@pytest.mark.parametrize( + ("action", "distance", "expected"), + [ + (PunchAction(Hand.RIGHT, PunchClass.STRAIGHT, Target.HEAD, Power.POWER), 120, "slip_left"), + (PunchAction(Hand.LEFT, PunchClass.HOOK, Target.HEAD, Power.POWER), 105, "weave"), + (PunchAction(Hand.RIGHT, PunchClass.UPPERCUT, Target.HEAD), 95, "slip_left"), + (PunchAction(Hand.RIGHT, PunchClass.STRAIGHT, Target.HEAD, Power.POWER), 160, "pull"), + ], +) +def test_a_champion_evades_a_punch_it_has_seen( + action: PunchAction, distance: int, expected: str +) -> None: + engine = engine_at(distance) + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 2)) + # Standing still with its hands down, so the punch arrives where it was aimed. + brain._movement = lambda *_args: (0, 0) # type: ignore[method-assign] + brain._attack = lambda *_args: None # type: ignore[method-assign] + throw(engine, action) + evaded = False + sent: list[ActionKind] = [] + for _ in range(25): + command = brain.decide(engine) + assert command is not None + sent.extend(item.kind for item in command.actions) + engine.submit_input("cpu", command) + snapshot = engine.step() + evaded = evaded or any( + event.kind == "evade" and event.actor_id == "cpu" for event in snapshot.events + ) + if evaded: + break + assert sent[0].value == expected + assert evaded + + +def test_a_body_hook_it_cannot_duck_is_met_with_a_perfect_low_block() -> None: + engine = engine_at(100) + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 4)) + brain._movement = lambda *_args: (0, 0) # type: ignore[method-assign] + brain._attack = lambda *_args: None # type: ignore[method-assign] + # Its hands are down when the punch comes: only a guard let down a moment can parry. + brain._hold_guard = False + brain._guard_window_until = 1_000_000 + throw(engine, PunchAction(Hand.RIGHT, PunchClass.HOOK, Target.BODY, Power.POWER)) + events: list[str] = [] + for _ in range(20): + command = brain.decide(engine) + assert command is not None + assert not any(isinstance(item, MovementAction) for item in command.actions) + engine.submit_input("cpu", command) + events.extend(event.kind for event in engine.step().events if event.actor_id == "cpu") + if "perfect_block" in events: + break + assert "perfect_block" in events + + +def body_hook_outcome( + style: FighterStyle, *, puncher_conditioning: int = 1000, perfect_roll: bool = False +) -> str: + """How a champion who reads a power body hook 6 ticks after it starts, and fails or passes the + perfect roll, meets it.""" + engine = engine_at(100) + engine.checksums = False + engine.fighter("cpu").style = style + engine.fighter("human").conditioning = puncher_conditioning + brain = CpuBrain("cpu", "human", CpuLevel.CHAMPION, 4, style) + # Read and perfect-block chances no other roll uses, so each can be passed or failed alone. + brain.profile = replace( + brain.profile, reaction_ticks=6, reaction_spread=0, read_percent=91, perfect_percent=37 + ) + + def roll(percent: int) -> bool: + return percent == 91 or (perfect_roll and percent == 37) + + brain._roll = roll # type: ignore[method-assign] + brain._movement = lambda *_args: (0, 0) # type: ignore[method-assign] + brain._attack = lambda *_args: None # type: ignore[method-assign] + throw(engine, PunchAction(Hand.RIGHT, PunchClass.HOOK, Target.BODY, Power.POWER)) + for _ in range(40): + command = brain.decide(engine) + assert command is not None + engine.submit_input("cpu", command) + for event in engine.step().events: + if event.kind in ("hit", "counter_hit", "block", "perfect_block"): + return "hit" if event.kind == "counter_hit" else event.kind + raise AssertionError("the hook never arrived") + + +@pytest.mark.parametrize("style", [FighterStyle.BALANCED, FighterStyle.COUNTER_PUNCHER]) +def test_a_guard_raised_too_late_for_a_plain_block_is_only_a_parry_when_the_roll_says_so( + style: FighterStyle, +) -> None: + # A fresh puncher's hook, read 4 ticks before contact, leaves no time to raise a guard that is + # not still fresh when it lands: without the perfect roll it gets through, not parried anyway. + assert body_hook_outcome(style) == "hit" + assert body_hook_outcome(style, perfect_roll=True) == "perfect_block" + # A worn-out puncher is slow enough for an ordinary block. + assert body_hook_outcome(style, puncher_conditioning=0) == "block" + + +def parry_share( + level: CpuLevel, + style: FighterStyle, + *, + perfect_percent: int | None = None, + guard_percent: int = 0, + trials: int = 120, + outcome: str = "perfect_block", +) -> float: + """Share of power body hooks thrown at random moments that the computer, standing still, meets + with `outcome`; by default its hands are down, so a late guard can be a parry.""" + met = 0 + for trial in range(trials): + engine = engine_at(100, seed=trial) + engine.checksums = False + engine.fighter("cpu").style = style + brain = CpuBrain("cpu", "human", level, 1000 + trial, style) + brain.profile = replace(brain.profile, guard_percent=guard_percent) + if perfect_percent is not None: + brain.profile = replace(brain.profile, perfect_percent=perfect_percent) + brain._movement = lambda *_args: (0, 0) # type: ignore[method-assign] + brain._attack = lambda *_args: None # type: ignore[method-assign] + brain._head_movement = lambda *_args: None # type: ignore[method-assign] + wait = 20 + trial % 40 + for tick in range(wait + 30): + command = brain.decide(engine) + if command is not None: + engine.submit_input("cpu", command) + if tick == wait: + throw(engine, PunchAction(Hand.LEFT, PunchClass.HOOK, Target.BODY, Power.POWER)) + kinds = {event.kind for event in engine.step().events} + met_by = kinds & {"perfect_block", "block", "hit", "counter_hit"} + if met_by: + met += outcome in met_by and (outcome != "hit" or "block" not in met_by) + break + return met / trials + + +def test_how_often_the_computer_parries_follows_its_reads_and_its_perfect_blocks() -> None: + """A guard raised late enough to parry is the perfect-block roll's to give, at every level.""" + for level, style in ( + (CpuLevel.CONTENDER, FighterStyle.BALANCED), + (CpuLevel.CHAMPION, FighterStyle.BALANCED), + (CpuLevel.CHAMPION, FighterStyle.COUNTER_PUNCHER), + ): + assert parry_share(level, style, perfect_percent=0) == 0 + champion = styled_profile(PROFILES[CpuLevel.CHAMPION], FighterStyle.COUNTER_PUNCHER) + expected = champion.read_percent * champion.perfect_percent / 10_000 + share = parry_share(CpuLevel.CHAMPION, FighterStyle.COUNTER_PUNCHER) + assert abs(share - expected) <= 0.1 + assert parry_share(CpuLevel.CONTENDER, FighterStyle.BALANCED) < share + + +def test_a_computer_holding_its_guard_blocks_rather_than_dropping_it_for_a_parry_it_cannot_get() -> ( + None +): + """Only a guard let down a while parries, so with its hands up the computer meets a body + uppercut it reads, even with every roll going its way, with an ordinary low block: it never + lets its guard down first.""" + engine = engine_at(90) + engine.checksums = False + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 4)) + brain.profile = replace(brain.profile, guard_percent=100) + brain._movement = lambda *_args: (0, 0) # type: ignore[method-assign] + brain._attack = lambda *_args: None # type: ignore[method-assign] + for _ in range(12): + command = brain.decide(engine) + assert command is not None and command.defense is DefensivePose.GUARD_HIGH + engine.submit_input("cpu", command) + engine.step() + throw( + engine, PunchAction(Hand.RIGHT, PunchClass.UPPERCUT, Target.BODY, Power.POWER), sequence=2 + ) + defenses: list[DefensivePose] = [] + for _ in range(20): + command = brain.decide(engine) + assert command is not None + defenses.append(command.defense) + engine.submit_input("cpu", command) + kinds = {event.kind for event in engine.step().events} + if kinds & {"block", "perfect_block", "hit"}: + break + assert "block" in kinds and "perfect_block" not in kinds + assert DefensivePose.NONE not in defenses + + +def test_a_misread_punch_gets_no_answer() -> None: + engine = engine_at(120) + brain = never(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 5)) + brain._movement = lambda *_args: (0, 0) # type: ignore[method-assign] + throw(engine, PunchAction(Hand.RIGHT, PunchClass.STRAIGHT, Target.HEAD, Power.POWER)) + landed = [] + for _ in range(14): + command = brain.decide(engine) + assert command is not None + assert command.actions == () + engine.submit_input("cpu", command) + landed.extend(event.kind for event in engine.step().events if event.target_id == "cpu") + assert "hit" in landed + + +def test_it_never_throws_a_punch_it_cannot_pay_for() -> None: + engine = engine_at(120) + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 6)) + # No reserve, so it is not resting: only the price of each punch holds it back. + brain.profile = replace(brain.profile, stamina_reserve=0) + cpu = engine.fighter("cpu") + thrown: list[PunchAction] = [] + for _ in range(120): + cpu.stamina = 40 + command = brain.decide(engine) + assert command is not None + thrown.extend(punches([command])) + engine.submit_input("cpu", command) + engine.step() + assert all( + PUNCH_RULES[(punch.punch_class, punch.target, punch.power)].stamina_cost <= 40 + for punch in thrown + ) + assert not any(event.kind == "exhausted" for event in engine.events) + + +def test_it_only_follows_up_with_the_full_price_of_the_next_punch_in_hand() -> None: + """The engine charges a combination's next punch in full before its discount, so a follow-up + queued on the discounted price waits out the recovery or comes as a slow arm punch.""" + engine = engine_at(100) + engine.checksums = False + brain = CpuBrain("cpu", "human", CpuLevel.CHAMPION, 9) + cpu, human = engine.fighter("cpu"), engine.fighter("human") + engine.submit_input( + "cpu", + InputCommand( + 1, engine.tick, actions=(PunchAction(Hand.LEFT, PunchClass.JAB, Target.HEAD),) + ), + ) + while cpu.attack is None or not cpu.attack.resolved: + engine.step() + assert cpu.attack.landed + # The first follow-up on offer after a jab is the hook; make it a power hook to the body. + brain._rng.randrange = lambda _stop: 0 # type: ignore[method-assign] + brain._shaped = lambda action, _them, _power: replace( # type: ignore[method-assign] + action, target=Target.BODY, power=Power.POWER + ) + price = brain._rule( + PunchAction(Hand.RIGHT, PunchClass.HOOK, Target.BODY, Power.POWER) + ).stamina_cost + + def follow_up(stamina: int) -> PunchAction | None: + brain._followed_start = -1 + brain._combo_left = 1 + cpu.stamina = stamina + return brain._follow_up(cpu, human, False) + + assert follow_up(price * 90 // 100) is None + assert follow_up(price - 1) is None + hook = follow_up(price) + assert hook is not None + attack = cpu.attack + engine.submit_input("cpu", InputCommand(2, engine.tick, actions=(hook,))) + events = [] + while cpu.attack is attack: + events.extend(engine.step().events) + # Paid in full, it cuts the jab's recovery short at the cancel point and is no arm punch. + assert engine.tick == attack.start_tick + attack.cancel_age + assert not any(event.kind == "exhausted" for event in events) + + +def test_with_stamina_in_hand_it_lets_its_hands_go() -> None: + engine = engine_at(120) + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 6)) + commands = run(engine, brain, 120) + assert punches(commands) + + +@pytest.mark.parametrize("distance", [150, 165]) +def test_it_only_throws_what_reaches(distance: int) -> None: + engine = engine_at(distance) + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 7)) + human = engine.fighter("human") + for _ in range(60): + command = brain.decide(engine) + assert command is not None + for punch in punches([command]): + gap = hypot(human.x - engine.fighter("cpu").x, human.y - engine.fighter("cpu").y) + assert PUNCH_RULES[(punch.punch_class, punch.target, punch.power)].reach >= gap + engine.submit_input("cpu", command) + engine.step() + + +def test_it_only_throws_a_punch_that_reaches_along_the_way_it_faces() -> None: + """The engine judges a punch along the way its thrower faces, which turns toward the other man + only so fast and holds through the punch, so a man who has stepped round off that line is out + of reach until the computer has turned to him.""" + engine = engine_at(120) + engine.checksums = False + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 7)) + cpu, human = engine.fighter("cpu"), engine.fighter("human") + jab = PunchAction(Hand.LEFT, PunchClass.JAB, Target.HEAD) + assert (cpu.facing_x, cpu.facing_y) == (-1000, 0) + assert brain._reaches(cpu, human, jab) + # He steps 40 degrees round the computer, much further than it turns in a tick. + human.x = cpu.x - round(120 * cos(radians(40))) + human.y = round(120 * sin(radians(40))) + assert not brain._reaches(cpu, human, jab) + engine.submit_input("cpu", InputCommand(1, engine.tick, actions=(jab,))) + kinds = [event.kind for _ in range(12) for event in engine.step().events] + assert "whiff" in kinds and "hit" not in kinds + # Facing him again, it has him in reach. + cpu.facing_x = round(1000 * (human.x - cpu.x) / 120) + cpu.facing_y = round(1000 * (human.y - cpu.y) / 120) + assert brain._reaches(cpu, human, jab) + + +def test_a_combination_follows_up_only_where_the_turning_facing_still_reaches() -> None: + """A follow-up goes at the cancel point along a facing that has only turned since the punch + landed, so a man who has stepped well round the computer gets none.""" + + def follows_up(off_degrees: int) -> bool: + engine = engine_at(100) + engine.checksums = False + brain = CpuBrain("cpu", "human", CpuLevel.CHAMPION, 9) + cpu, human = engine.fighter("cpu"), engine.fighter("human") + jab = PunchAction(Hand.LEFT, PunchClass.JAB, Target.HEAD) + engine.submit_input("cpu", InputCommand(1, engine.tick, actions=(jab,))) + while cpu.attack is None or not cpu.attack.resolved: + engine.step() + assert cpu.attack.landed + human.x = cpu.x - round(100 * cos(radians(off_degrees))) + human.y = round(100 * sin(radians(off_degrees))) + human.velocity_x = human.velocity_y = 0 + brain._combo_left = 1 + brain._rng.randrange = lambda _stop: 1 # type: ignore[method-assign] + brain._shaped = lambda action, _them, _power: action # type: ignore[method-assign] + return brain._follow_up(cpu, human, False) is not None + + assert follows_up(0) + assert not follows_up(50) + + +def test_it_walks_toward_an_opponent_out_of_range_and_backs_off_one_too_close() -> None: + far = engine_at(360) + brain = never(CpuBrain("cpu", "human", CpuLevel.CONTENDER, 8)) + command = brain.decide(far) + assert command is not None and command.move_x < 0 + close = engine_at(90) + brain = never(CpuBrain("cpu", "human", CpuLevel.CONTENDER, 8)) + command = brain.decide(close) + assert command is not None and command.move_x > 0 + + +def rope_room_after(level: CpuLevel, ticks: int) -> float: + engine = engine_at(150) + human = engine.fighter("human") + cpu = engine.fighter("cpu") + human.x, human.y = 300, 0 + cpu.x, cpu.y = 450, 0 + brain = CpuBrain("cpu", "human", level, 9) + brain._attack = lambda *_args: None # type: ignore[method-assign] + run(engine, brain, ticks) + return _rope_room(cpu.x, cpu.y) + + +def test_off_the_ropes_a_champion_circles_out_and_a_rookie_is_slow_to() -> None: + assert _rope_room(450, 0) < 15 + champion = rope_room_after(CpuLevel.CHAMPION, 60) + rookie = rope_room_after(CpuLevel.ROOKIE, 60) + assert champion > 80 + assert rookie < champion / 2 + + +def test_hurt_and_close_it_ties_him_up() -> None: + engine = engine_at(100) + brain = always(CpuBrain("cpu", "human", CpuLevel.CONTENDER, 10)) + engine.fighter("cpu").poise = 80 + commands = run(engine, brain, 20) + assert ActionKind.CLINCH in kinds(commands) + assert any(event.kind == "clinch_start" for event in engine.events) + + +def test_fresh_it_does_not_clinch() -> None: + engine = engine_at(100) + brain = always(CpuBrain("cpu", "human", CpuLevel.CONTENDER, 10)) + assert ActionKind.CLINCH not in kinds(run(engine, brain, 90)) + + +def knocked_down(level: CpuLevel, seed: int, *, head: int = 0) -> tuple[BoxingEngine, CpuBrain]: + engine = engine_at(120, seed=seed) + cpu = engine.fighter("cpu") + cpu.trauma.head = head + engine._knock_down(cpu, engine.fighter("human")) + assert engine.phase is MatchPhase.KNOCKDOWN + return engine, CpuBrain("cpu", "human", level, seed) + + +def test_down_it_answers_each_prompt_once_inside_its_window() -> None: + engine, brain = knocked_down(CpuLevel.CHAMPION, 11) + always(brain) + cpu = engine.fighter("cpu") + presses: list[tuple[int, ActionKind, int, int, ActionKind | None]] = [] + while engine.phase is MatchPhase.KNOCKDOWN: + command = brain.decide(engine) + assert command is not None + for action in command.actions: + presses.append( + ( + engine.tick + 1, + action.kind, + cpu.get_up_window_start_tick, + cpu.get_up_window_end_tick, + cpu.get_up_prompt, + ) + ) + engine.submit_input("cpu", command) + engine.step() + assert presses + for tick, kind, start, end, prompt in presses: + assert start <= tick <= end + assert kind is prompt + assert len({start for _tick, _kind, start, _end, _prompt in presses}) == len(presses) + assert engine.phase is MatchPhase.FIGHT + assert any(event.kind == "get_up" and event.actor_id == "cpu" for event in engine.events) + + +def test_a_badly_hurt_rookie_can_stay_down() -> None: + outcomes = Counter() + for seed in range(12): + engine, brain = knocked_down(CpuLevel.ROOKIE, seed, head=1400) + engine.fighter("cpu").knockdowns = 2 + while engine.phase is MatchPhase.KNOCKDOWN: + command = brain.decide(engine) + if command is not None: + engine.submit_input("cpu", command) + engine.step() + outcomes["up" if engine.result is None else "out"] += 1 + assert outcomes["out"] >= 6 + + +def test_the_standing_fighter_sends_nothing_during_the_count() -> None: + engine = engine_at(120) + engine._knock_down(engine.fighter("human"), engine.fighter("cpu")) + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 12)) + for _ in range(30): + command = brain.decide(engine) + assert command is not None and command.actions == () + engine.submit_input("cpu", command) + engine.step() + + +def test_between_rounds_it_stays_put_and_only_talks_to_its_corner() -> None: + engine = engine_at(120, rounds=2, round_ticks=5, rest_ticks=40) + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 13)) + run(engine, brain, 5) + assert engine.phase is MatchPhase.REST + command = brain.decide(engine) + assert command is not None + assert (command.move_x, command.move_y) == (0, 0) + assert [action.kind for action in command.actions] == [ActionKind.CORNER_BREATH] + engine.complete_forfeit("human") + assert brain.decide(engine) is None + + +def resting(cpu_trauma: dict[str, int]) -> tuple[BoxingEngine, CpuBrain]: + engine = engine_at(120, rounds=3, round_ticks=5, rest_ticks=40) + for name, value in cpu_trauma.items(): + setattr(engine.fighter("cpu").trauma, name, value) + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 21)) + while engine.phase is not MatchPhase.REST: + engine.step() + return engine, brain + + +@pytest.mark.parametrize( + ("trauma", "instruction"), + [ + ({"left_cut": 450}, ActionKind.CORNER_CUT), + ({"bleeding": 260}, ActionKind.CORNER_CUT), + ({"right_eye": 560}, ActionKind.CORNER_SWELLING), + ({"swelling": 480}, ActionKind.CORNER_SWELLING), + ({}, ActionKind.CORNER_BREATH), + ], +) +def test_its_corner_works_on_what_is_worst(trauma: dict[str, int], instruction: ActionKind) -> None: + engine, brain = resting(trauma) + command = brain.decide(engine) + assert command is not None + assert [action.kind for action in command.actions] == [instruction] + + +def test_it_gives_one_instruction_a_rest_and_one_again_the_next_rest() -> None: + engine, brain = resting({}) + sent: list[ActionKind] = [] + rounds_seen = set() + while engine.result is None and engine.round_number <= 3: + command = brain.decide(engine) + if command is not None: + sent.extend( + action.kind for action in command.actions if engine.phase is MatchPhase.REST + ) + if engine.phase is MatchPhase.REST: + rounds_seen.add(engine.round_number) + engine.submit_input("cpu", command) + engine.step() + if engine.round_number == 3 and engine.phase is MatchPhase.FIGHT: + break + assert len(rounds_seen) == 2 + assert sent == [ActionKind.CORNER_BREATH, ActionKind.CORNER_BREATH] + assert engine.fighter("cpu").corner_choice is None or engine.phase is MatchPhase.FIGHT + + +def test_a_corner_that_forgets_does_not_keep_asking_through_the_rest() -> None: + engine, brain = resting({}) + rolls = iter([False]) + brain._roll = lambda _percent: next(rolls, True) # type: ignore[method-assign] + sent = [] + for _ in range(10): + command = brain.decide(engine) + assert command is not None + sent.extend(command.actions) + engine.submit_input("cpu", command) + engine.step() + assert sent == [] + + +def test_a_forgetful_corner_sends_nothing() -> None: + engine, brain = resting({}) + never(brain) + command = brain.decide(engine) + assert command is not None + assert command.actions == () + + +def test_it_cannot_read_a_punch_on_the_side_of_its_shut_eye() -> None: + def answers(left_eye: int) -> list[ActionKind]: + engine = engine_at(120) + engine.fighter("cpu").trauma.left_eye = left_eye + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 2)) + brain._movement = lambda *_args: (0, 0) # type: ignore[method-assign] + brain._attack = lambda *_args: None # type: ignore[method-assign] + throw(engine, PunchAction(Hand.RIGHT, PunchClass.STRAIGHT, Target.HEAD, Power.POWER)) + return kinds(run(engine, brain, 12)) + + assert ActionKind.SLIP_LEFT in answers(0) + assert not any( + kind in (ActionKind.SLIP_LEFT, ActionKind.SLIP_RIGHT, ActionKind.PULL) + for kind in answers(BLIND_SIDE_EYE_THRESHOLD) + ) + + +def test_it_works_the_side_of_the_opponents_shut_eye() -> None: + def hook_hands(right_eye: int) -> Counter[Hand]: + hands: Counter[Hand] = Counter() + for seed in range(40): + engine = engine_at(100, seed=seed) + engine.fighter("human").trauma.right_eye = right_eye + brain = CpuBrain("cpu", "human", CpuLevel.CHAMPION, seed) + brain._tick = engine.tick + action = brain._choose(engine.fighter("cpu"), engine.fighter("human"), 100, 0) + if action.punch_class in (PunchClass.HOOK, PunchClass.UPPERCUT): + hands[action.hand] += 1 + return hands + + shut = hook_hands(BLIND_SIDE_EYE_THRESHOLD) + open_ = hook_hands(0) + assert shut[Hand.LEFT] > shut[Hand.RIGHT] + assert shut[Hand.LEFT] > open_[Hand.LEFT] + + +def test_it_keeps_power_back_from_a_guard_that_could_still_parry_it() -> None: + engine = engine_at(110) + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 41)) + human = engine.fighter("human") + hook = PunchAction(Hand.LEFT, PunchClass.HOOK, Target.HEAD) + brain._tick = engine.tick + assert brain._shaped(hook, human, 100).power is Power.NORMAL + human.defense = DefensivePose.GUARD_HIGH + human.guard_held_tick = engine.tick + human.defense_started_tick = engine.tick - 30 + assert brain._shaped(hook, human, 100).power is Power.POWER + # A guard raised a moment ago is up too long before a power punch lands to parry it. + human.defense_started_tick = engine.tick - 1 + assert brain._shaped(hook, human, 100).power is Power.POWER + human.defense = DefensivePose.NONE + human.stunned_ticks = 10 + assert brain._shaped(hook, human, 100).power is Power.POWER + + +def test_with_its_body_broken_down_it_keeps_more_stamina_back() -> None: + engine = engine_at(110) + brain = CpuBrain("cpu", "human", CpuLevel.ROOKIE, 42) + cpu = engine.fighter("cpu") + fresh = brain._reserve(cpu) + cpu.trauma.body = BODY_COLLAPSE_TRAUMA + assert fresh < BODY_COLLAPSE_STAMINA < brain._reserve(cpu) + + +def test_it_never_fouls() -> None: + engine = engine_at(110) + brain = CpuBrain("cpu", "human", CpuLevel.ROOKIE, 14) + assert not any(kind is ActionKind.FOUL for kind in kinds(run(engine, brain, 900))) + + +def test_the_same_seed_plays_the_same_bout() -> None: + def play(seed: int) -> list[InputCommand]: + engine = engine_at(200) + return run(engine, CpuBrain("cpu", "human", CpuLevel.CONTENDER, seed), 300) + + assert play(21) == play(21) + assert play(21) != play(22) + + +def test_every_command_is_valid_input() -> None: + engine = engine_at(130) + commands = run(engine, CpuBrain("cpu", "human", CpuLevel.CHAMPION, 15), 600) + sequences = [command.sequence for command in commands] + assert sequences == sorted(set(sequences)) + for command in commands: + assert command.move_x**2 + command.move_y**2 <= 1_000_000 + 2_000 + assert len(command.actions) <= 1 + assert command.defense in ( + DefensivePose.NONE, + DefensivePose.GUARD_HIGH, + DefensivePose.GUARD_LOW, + ) + + +def bout( + one: CpuLevel, two: CpuLevel, seed: int, *, round_ticks: int = 90 * 30, checksums: bool = False +) -> BoxingEngine: + engine = BoxingEngine( + match_id="sim", + activity_instance_id="instance", + guild_id="guild", + player_one_id="one", + player_two_id="two", + seed=seed, + config=EngineConfig(rounds=1, round_ticks=round_ticks, countdown_ticks=0), + ) + engine.checksums = checksums + brains = (CpuBrain("one", "two", one, seed * 2), CpuBrain("two", "one", two, seed * 2 + 1)) + while engine.result is None: + for brain in brains: + command = brain.decide(engine) + if command is not None: + engine.submit_input(brain.player_id, command) + engine.step() + return engine + + +def test_a_bout_without_checksums_plays_out_the_same() -> None: + hashed = bout(CpuLevel.CHAMPION, CpuLevel.CONTENDER, 8, round_ticks=600, checksums=True) + fast = bout(CpuLevel.CHAMPION, CpuLevel.CONTENDER, 8, round_ticks=600) + assert hashed.events == fast.events and hashed.result == fast.result + assert hashed.snapshot().checksum and fast.snapshot().checksum == "" + + +@pytest.mark.parametrize("level", list(CpuLevel)) +def test_two_computers_box_a_round_to_a_result(level: CpuLevel) -> None: + engine = bout(level, level, 31) + assert engine.result is not None + landed = Counter( + event.actor_id for event in engine.events if event.kind in ("hit", "counter_hit") + ) + assert landed["one"] > 0 and landed["two"] > 0 + + +def test_a_champion_beats_a_rookie() -> None: + winners = Counter( + bout(CpuLevel.CHAMPION, CpuLevel.ROOKIE, seed).result.winner_id for seed in range(5) + ) # type: ignore[union-attr] + assert winners["one"] >= 4 + + +def test_a_newcomer_mashing_every_punch_button_can_beat_the_rookie() -> None: + """Never guarding and never stopping, he out-lands the rookie and wins more bouts than he loses + (three in four over 48 bouts, draws counted half); a man who never stops punching never gets + his poise back, though, so the rookie drops him now and then.""" + bouts = [play("mash", "rookie", seed, EngineConfig()) for seed in range(1, 9)] + assert all(bout.landed[0] > bout.landed[1] for bout in bouts) + wins = sum(bout.winner_seat == 0 for bout in bouts) + losses = sum(bout.winner_seat == 1 for bout in bouts) + assert wins > losses + + +def test_the_rookie_lands_punches_on_a_turtle_every_round_and_the_turtle_still_wins() -> None: + """A newcomer who covers up still has to defend: the rookie pecks at his guard every round.""" + for seed in (1, 2, 6): + bout = play("turtle", "rookie", seed, EngineConfig(), bout_styles("turtle", "rookie", seed)) + assert bout.winner_seat == 0 + assert bout.rounds == 3 and min(bout.landed_by_round[1]) >= 1 + + +def test_a_rookie_pecks_at_a_guard_or_a_quiet_man_and_mostly_waits_out_a_busy_open_one() -> None: + def leads(level: CpuLevel, defense: DefensivePose, *, busy: bool) -> int: + """How many of 300 looks become a lead (a punch now, or a step in to throw one).""" + engine = engine_at(110) + brain = CpuBrain("cpu", "human", level, 3) + brain.profile = replace(brain.profile, aggression_percent=100) + human = engine.fighter("human") + human.defense = defense + leads = 0 + for look in range(300): + brain._next_attack_tick = 0 + brain._step_in = None + brain._opponent_punches = [look - 10] if busy else [] + cpu = engine.fighter("cpu") + punch = brain._attack(look, cpu, human, 110.0, False, False, False) + leads += punch is not None or brain._step_in is not None + return leads + + assert leads(CpuLevel.ROOKIE, DefensivePose.GUARD_HIGH, busy=True) == 300 + assert leads(CpuLevel.ROOKIE, DefensivePose.NONE, busy=False) == 300 + rookie = PROFILES[CpuLevel.ROOKIE].busy_lead_percent + assert abs(leads(CpuLevel.ROOKIE, DefensivePose.NONE, busy=True) - 3 * rookie) <= 30 + assert leads(CpuLevel.CONTENDER, DefensivePose.NONE, busy=True) == 300 + + +def test_a_skilled_player_beats_the_rookie_on_the_cards_and_not_by_cutting_him_up() -> None: + """Sixty-odd clean jabs on one eye used to split it open before the final bell in every bout.""" + bout = play("skilled", "rookie", 1, EngineConfig()) + assert (bout.winner_seat, bout.method) == (0, "decision") + + +def test_the_champion_can_be_outboxed_or_outcountered_but_not_mashed() -> None: + """A skilled player and a counter-puncher each win some bouts against the champion and lose + others; a button masher next to never wins.""" + config = EngineConfig() + + def winner(human: str, seed: int, style: FighterStyle) -> int | None: + return play(human, "champion", seed, config, (FighterStyle.BALANCED, style)).winner_seat + + assert winner("skilled", 1, FighterStyle.COUNTER_PUNCHER) == 0 + assert winner("skilled", 1, FighterStyle.SWARMER) == 1 + assert winner("counter", 3, FighterStyle.BOXER) == 0 + assert winner("counter", 1, FighterStyle.SWARMER) == 1 + assert winner("mash", 1, FighterStyle.BOXER) == 1 + + +def test_over_a_connection_the_champion_is_harder_to_beat_than_the_contender() -> None: + """At 50 ms, reacting to each punch as a person does, the scripted skilled player beats the + contender in most bouts and the champion in few (57% and 31% over 48 bouts; the scripted + counter-puncher 23% and 10%).""" + lag = Lag.over(50, (6, 9)) + + def wins(level: str) -> int: + return sum( + play( + "skilled", level, seed, EngineConfig(), bout_styles("skilled", level, seed), lag + ).winner_seat + == 0 + for seed in range(1, 7) + ) + + contender, champion = wins("contender"), wins("champion") + assert contender >= 3 + assert champion < contender + + +def test_against_a_scripted_player_the_computer_boxes_in_its_room_style() -> None: + assert bout_styles("skilled", "champion", 5) == ( + FighterStyle.BALANCED, + cpu_style(CpuLevel.CHAMPION, 5), + ) + assert bout_styles("champion", "rookie", 5) == (FighterStyle.BALANCED, FighterStyle.BALANCED) + + +def test_the_scripted_counter_puncher_minds_its_breath_guards_low_and_talks_to_its_corner() -> None: + def wants_to_throw(stamina: int) -> set[PunchClass]: + """What it would throw in range of a jab that has landed, given the breath it has.""" + engine = engine_at(140) + engine.fighter("human").stamina = stamina + counter = ScriptedHuman("counter", "human", "cpu") + jab = PunchAction(Hand.LEFT, PunchClass.JAB, Target.HEAD) + engine.submit_input("cpu", InputCommand(1, engine.tick, actions=(jab,))) + wanted: set[PunchClass] = set() + for _ in range(HUMAN_REACTION_TICKS + 4): + engine.step() + command = counter.decide(engine) + assert command is not None + wanted.update(punch.punch_class for punch in punches([command])) + return wanted + + assert wants_to_throw(1000) == {PunchClass.JAB, PunchClass.STRAIGHT} + assert wants_to_throw(200) == {PunchClass.STRAIGHT} + assert wants_to_throw(60) == set() + + engine = engine_at(130) + engine.fighter("human").stamina = 250 + body_hook = PunchAction(Hand.LEFT, PunchClass.HOOK, Target.BODY, Power.POWER) + engine.submit_input("cpu", InputCommand(1, engine.tick, actions=(body_hook,))) + for _ in range(HUMAN_REACTION_TICKS + 1): + engine.step() + attack = engine.fighter("cpu").attack + assert attack is not None and not attack.resolved + command = ScriptedHuman("counter", "human", "cpu").decide(engine) + assert command is not None and command.defense is DefensivePose.GUARD_LOW + + engine.phase = MatchPhase.REST + command = ScriptedHuman("counter", "human", "cpu").decide(engine) + assert command is not None and kinds([command]) == [ActionKind.CORNER_BREATH] + + +def first_guard_tick(human: str, lag: Lag | None, seed: int) -> int: + """Ticks from the start of a slow body hook to the first low guard the server hears for it. + + Thrown from across the ring, so nothing the human does on the way in can stop it first.""" + engine = engine_at(300, seed=seed) + # Worn out, the puncher is slow enough that every reaction sees the hook before it lands. + engine.fighter("cpu").conditioning = 0 + player = make_player(human, "human", "cpu", seed, lag=lag) + hook = PunchAction(Hand.LEFT, PunchClass.HOOK, Target.BODY, Power.POWER) + engine.submit_input("cpu", InputCommand(1, engine.tick, actions=(hook,))) + started = engine.tick + 1 + for _ in range(30): + command = player.decide(engine) + if command is not None: + engine.submit_input("human", command) + if command.defense is DefensivePose.GUARD_LOW: + return engine.tick + 1 - started + engine.step() + raise AssertionError("the hook was never guarded") + + +@pytest.mark.parametrize("human", ["skilled", "counter"]) +def test_a_scripted_human_on_a_connection_sees_each_punch_late_and_is_heard_late( + human: str, +) -> None: + assert {first_guard_tick(human, None, seed) for seed in range(6)} == {HUMAN_REACTION_TICKS + 1} + lag = Lag(reaction=(6, 9), latency_ticks=2, uplink_ticks=1) + delays = {first_guard_tick(human, lag, seed) for seed in range(12)} + # His reaction to each punch is his own, then the connection shows it late and carries the + # guard back late: the server hears it between 6+2+1 and 9+2+1 ticks after the punch starts. + assert delays <= set(range(6 + 2 + 1 + 1, 9 + 2 + 1 + 2)) + assert len(delays) >= 3 + + +def test_a_lagged_player_delivers_every_command_in_order_and_late() -> None: + class Counting: + def __init__(self) -> None: + self.sequence = 0 + + def decide(self, engine: BoxingEngine) -> InputCommand: + self.sequence += 1 + return InputCommand(self.sequence, engine.tick) + + engine = engine_at(150) + lagged = LaggedPlayer(Counting(), 2) + heard = [lagged.decide(engine) for _ in range(5)] + assert [None if command is None else command.sequence for command in heard] == [ + None, + None, + 1, + 2, + 3, + ] + assert Lag.over(50, (6, 9)) == Lag((6, 9), 1, 1) + assert Lag.over(100, (6, 9)) == Lag((6, 9), 2, 2) + + +def test_a_style_matrix_plays_two_strategies_with_each_style_on_each_side() -> None: + config = EngineConfig(rounds=1, round_ticks=240, countdown_ticks=0) + table = style_matrix( + "skilled:brawler", + [FighterStyle.BALANCED, FighterStyle.SWARMER], + range(101, 102), + config, + Lag(reaction=(6, 9), uplink_ticks=1), + ) + assert table.splitlines()[0].startswith("skilled:brawler: win % of the row's style") + assert "2 bouts a pair, seeds 101-101" in table + assert "+-" in table.splitlines()[2] + + +def test_profiles_get_better_with_the_level() -> None: + rookie, contender, champion = (PROFILES[level] for level in CpuLevel) + assert rookie.reaction_ticks > contender.reaction_ticks > champion.reaction_ticks + assert rookie.read_percent < contender.read_percent < champion.read_percent + assert rookie.rating < contender.rating < champion.rating + assert cpu_player_id(CpuLevel.CHAMPION) == "cpu:champion" + + +def test_a_landed_punch_is_followed_by_a_combination_that_chains() -> None: + engine = engine_at(110) + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 16)) + started: list[tuple[int, PunchClass, int]] = [] + for _ in range(240): + command = brain.decide(engine) + assert command is not None + engine.submit_input("cpu", command) + snapshot = engine.step() + attack = engine.fighter("cpu").attack + if attack is not None and any( + event.kind == "punch_start" and event.actor_id == "cpu" for event in snapshot.events + ): + started.append((attack.start_tick, attack.action.punch_class, attack.total_ticks)) + # A punch that starts before the one before it has recovered was a follow-up cut in early. + follow_ups = [ + (before[1], after[1]) + for before, after in pairwise(started) + if after[0] < before[0] + before[2] + ] + assert follow_ups + assert all(pair in COMPATIBLE_COMBO_CHAINS for pair in follow_ups) + + +def test_a_rookie_lets_a_hurt_man_off_and_a_champion_does_not() -> None: + def gap_after_landing(level: CpuLevel) -> int: + engine = engine_at(110) + brain = always(CpuBrain("cpu", "human", level, 17)) + brain.profile = replace(brain.profile, combo_length=1) + starts = [] + for _ in range(240): + command = brain.decide(engine) + assert command is not None + engine.submit_input("cpu", command) + snapshot = engine.step() + starts.extend( + event.tick + for event in snapshot.events + if event.kind == "punch_start" and event.actor_id == "cpu" + ) + if len(starts) >= 2: + break + return starts[1] - starts[0] + + rookie = PROFILES[CpuLevel.ROOKIE] + assert gap_after_landing(CpuLevel.ROOKIE) >= rookie.admire_ticks + assert gap_after_landing(CpuLevel.CHAMPION) < rookie.admire_ticks + + +def test_against_a_busy_opponent_it_moves_its_head() -> None: + engine = engine_at(140) + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 18)) + brain._attack = lambda *_args: None # type: ignore[method-assign] + brain._opponent_punches = [0, 1] + command = brain.decide(engine) + assert command is not None + assert [action.kind for action in command.actions] in ( + [ActionKind.SLIP_LEFT], + [ActionKind.SLIP_RIGHT], + [ActionKind.WEAVE], + ) + quiet = engine_at(140) + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 18)) + brain._attack = lambda *_args: None # type: ignore[method-assign] + command = brain.decide(quiet) + assert command is not None and command.actions == () + + +def test_well_ahead_and_out_of_range_it_may_showboat_but_never_up_close() -> None: + far = engine_at(300) + far.fighter("human").knockdowns = 1 + brain = always(CpuBrain("cpu", "human", CpuLevel.ROOKIE, 19)) + command = brain.decide(far) + assert command is not None and [action.kind for action in command.actions] == [ActionKind.TAUNT] + close = engine_at(120) + close.fighter("human").knockdowns = 1 + brain = always(CpuBrain("cpu", "human", CpuLevel.ROOKIE, 19)) + assert ActionKind.TAUNT not in kinds(run(close, brain, 60)) + + +def test_a_high_guard_is_attacked_to_the_body() -> None: + engine = engine_at(110) + brain = always(CpuBrain("cpu", "human", CpuLevel.CONTENDER, 20)) + human = engine.fighter("human") + thrown: list[PunchAction] = [] + for sequence in range(1, 120): + engine.submit_input( + "human", InputCommand(sequence, engine.tick, defense=DefensivePose.GUARD_HIGH) + ) + command = brain.decide(engine) + assert command is not None + thrown.extend(punches([command])) + engine.submit_input("cpu", command) + engine.step() + assert human.defense in (DefensivePose.GUARD_HIGH, DefensivePose.NONE) + assert thrown + assert all(punch.target is Target.BODY for punch in thrown) + + +def _fight_a_man_standing_still(level: CpuLevel, prepare, ticks: int = 900) -> Counter[str]: # type: ignore[no-untyped-def] + """The computer against an opponent who stands in front of it and never punches.""" + engine = engine_at(220, seed=5, round_ticks=100_000) + brain = CpuBrain("cpu", "human", level, 5) + seen: Counter[str] = Counter() + for sequence in range(1, ticks + 1): + cpu, human = engine.fighter("cpu"), engine.fighter("human") + prepare(cpu) + human.poise, human.stamina, human.guard = 600, 1000, 700 + command = brain.decide(engine) + if command is not None: + engine.submit_input("cpu", command) + engine.submit_input("human", InputCommand(sequence, engine.tick)) + for event in engine.step().events: + if event.actor_id == "cpu": + seen[event.kind] += 1 + return seen + + +@pytest.mark.parametrize("level", [CpuLevel.CONTENDER, CpuLevel.CHAMPION]) +def test_a_battered_computer_at_the_poise_it_can_still_have_keeps_fighting(level: CpuLevel) -> None: + def battered(cpu) -> None: # type: ignore[no-untyped-def] + cpu.trauma.head = 1200 + cpu.poise = poise_ceiling(1200) + + seen = _fight_a_man_standing_still(level, battered) + assert seen["punch_start"] >= 20 + + +@pytest.mark.parametrize("level", [CpuLevel.CONTENDER, CpuLevel.CHAMPION]) +def test_a_computer_whose_body_is_broken_down_fights_on_the_breath_it_can_still_hold( + level: CpuLevel, +) -> None: + """Saving stamina against a body knockdown must not ask for more than a battered body holds.""" + + def broken_down(cpu) -> None: # type: ignore[no-untyped-def] + cpu.trauma.body = 800 + cpu.conditioning = 100 + + # Gassed, with a third of his breath, he still throws a punch every few seconds (none before). + seen = _fight_a_man_standing_still(level, broken_down) + assert seen["punch_start"] >= 5 + for level_ in CpuLevel: + brain = CpuBrain("cpu", "human", level_, 1) + cpu = engine_at(110).fighter("cpu") + for body in range(0, 1201, 100): + for conditioning in range(0, 1001, 100): + cpu.trauma.body, cpu.conditioning = body, conditioning + assert brain._reserve(cpu) < cpu.maximum_stamina + + +@pytest.mark.parametrize("level", [CpuLevel.CONTENDER, CpuLevel.CHAMPION]) +def test_with_swollen_eyes_it_steps_into_its_shorter_reach(level: CpuLevel) -> None: + def swollen(cpu) -> None: # type: ignore[no-untyped-def] + cpu.trauma.left_eye = cpu.trauma.right_eye = 800 + cpu.trauma.swelling = 700 + + seen = _fight_a_man_standing_still(level, swollen) + assert seen["punch_start"] >= 20 + assert seen["whiff"] * 10 <= seen["punch_start"] + + +def test_the_computer_picks_its_style_from_the_match_seed() -> None: + for level in CpuLevel: + picks = {cpu_style(level, seed) for seed in range(200)} + assert picks == set(CPU_STYLES[level]) + assert cpu_style(level, 99) is cpu_style(level, 99) + + +@pytest.mark.parametrize("level", list(CpuLevel)) +def test_each_style_boxes_its_own_way(level: CpuLevel) -> None: + base = PROFILES[level] + assert styled_profile(base, FighterStyle.BALANCED) == base + boxer = styled_profile(base, FighterStyle.BOXER) + assert boxer.outside_distance > base.outside_distance + assert boxer.jab_bias > base.jab_bias + assert boxer.footwork_percent > base.footwork_percent + slugger = styled_profile(base, FighterStyle.SLUGGER) + assert slugger.power_percent > base.power_percent + assert slugger.footwork_percent < base.footwork_percent + # The swarmer presses in close and moves his head; his body work is in his punches' body + # damage, so he picks body shots as often as his level does. + swarmer = styled_profile(base, FighterStyle.SWARMER) + assert swarmer.outside_distance < base.outside_distance + assert swarmer.body_percent == base.body_percent + assert swarmer.aggression_percent == base.aggression_percent + assert swarmer.head_movement_percent > base.head_movement_percent + counter = styled_profile(base, FighterStyle.COUNTER_PUNCHER) + assert counter.counter_percent > base.counter_percent + assert counter.aggression_percent < base.aggression_percent + for styled in (boxer, slugger, swarmer, counter): + for name in ("aggression_percent", "power_percent", "counter_percent", "body_percent"): + assert 0 <= getattr(styled, name) <= 100 + + +def test_it_prices_and_times_its_punches_by_its_style() -> None: + engine = engine_at(120) + cpu = engine.fighter("cpu") + hook = PunchAction(Hand.LEFT, PunchClass.HOOK, Target.HEAD, Power.POWER) + cost = PUNCH_RULES[(PunchClass.HOOK, Target.HEAD, Power.POWER)].stamina_cost + cpu.stamina = cost + balanced = CpuBrain("cpu", "human", CpuLevel.CHAMPION, 6) + slugger = CpuBrain("cpu", "human", CpuLevel.CHAMPION, 6, FighterStyle.SLUGGER) + assert balanced._affordable(cpu, hook, 0) + assert not slugger._affordable(cpu, hook, 0) + swarmer = CpuBrain("cpu", "human", CpuLevel.CHAMPION, 6, FighterStyle.SWARMER) + assert swarmer._timing(cpu, hook)[0] == balanced._timing(cpu, hook)[0] - 1 + jab = PunchAction(Hand.LEFT, PunchClass.JAB, Target.HEAD) + boxer = CpuBrain("cpu", "human", CpuLevel.CHAMPION, 6, FighterStyle.BOXER) + assert boxer._rule(jab).reach > balanced._rule(jab).reach + + +def test_a_counter_puncher_raises_its_guard_earlier_for_the_perfect_block() -> None: + def guard_from(style: FighterStyle) -> int: + engine = engine_at(120) + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 6, style)) + throw(engine, PunchAction(Hand.RIGHT, PunchClass.STRAIGHT, Target.HEAD, Power.POWER)) + engine.step() + human = engine.fighter("human") + cpu = engine.fighter("cpu") + attack = human.attack + assert attack is not None + # Force a guard answer: an evasion is out of reach. + cpu.stamina = 0 + while engine.tick - attack.start_tick < brain.profile.reaction_ticks: + engine.step() + brain._read(engine.tick, cpu, human) + return brain._guard_from + + assert guard_from(FighterStyle.COUNTER_PUNCHER) == guard_from(FighterStyle.BALANCED) - 1 + + +def test_the_brain_boxes_in_the_style_it_is_given() -> None: + for style in FighterStyle: + brain = CpuBrain("cpu", "human", CpuLevel.CONTENDER, 1, style) + assert brain.profile == styled_profile(PROFILES[CpuLevel.CONTENDER], style) + assert (brain.profile == PROFILES[CpuLevel.CONTENDER]) is (style is FighterStyle.BALANCED) + + +def test_a_counter_puncher_still_slips_a_slow_punch_it_reads_early() -> None: + def answer(style: FighterStyle) -> ActionKind | None: + engine = engine_at(120) + human = engine.fighter("human") + # A worn-out puncher is slow enough that the punch is read long before it lands. + human.conditioning = 0 + brain = always(CpuBrain("cpu", "human", CpuLevel.CHAMPION, 2, style)) + throw(engine, PunchAction(Hand.RIGHT, PunchClass.STRAIGHT, Target.HEAD, Power.POWER)) + engine.step() + attack = human.attack + assert attack is not None + while attack.start_tick + attack.rule.startup - engine.tick > EVASION_TICKS + 1: + engine.step() + return brain._read(engine.tick, engine.fighter("cpu"), human) + + assert answer(FighterStyle.BALANCED) is None + assert answer(FighterStyle.COUNTER_PUNCHER) is ActionKind.SLIP_LEFT diff --git a/tests/test_hands/test_engine.py b/tests/test_hands/test_engine.py index b1559bd8..ef9f1852 100644 --- a/tests/test_hands/test_engine.py +++ b/tests/test_hands/test_engine.py @@ -1,34 +1,66 @@ from __future__ import annotations +import dataclasses import json import math +import random import re +from enum import Enum +from importlib import resources from math import hypot +from typing import TYPE_CHECKING import pytest from intelstream.hands import rules from intelstream.hands.engine import ( ACTION_BUFFER_TICKS, + COUNT_TICK_INTERVAL, MAX_PENDING_ACTIONS, + PERFECT_BLOCK_TICKS, + AttackState, BoxingEngine, EngineConfig, + FighterState, ) from intelstream.hands.protocol import encode_snapshot from intelstream.hands.rules import ( + BLIND_SIDE_EYE_THRESHOLD, + BODY_COLLAPSE_DELAY_TICKS, + BODY_WIND_PERCENT, + BOX_PAUSE_TICKS, CLINCH_HOLD_DISTANCE, + CORNER_TREATMENTS, FIGHTER_RADIUS, - KNOCKDOWN_NEUTRAL_SEPARATION, + FLINCH_MINIMUM_DAMAGE, + FOUL_SEPARATION, + GET_UP_STUN_TICKS, + GUARD_BLOCK_MINIMUM, + MANDATORY_COUNT, MINIMUM_SEPARATION, + PARRY_STAGGER_TICKS, + PERFECT_BLOCK_REARM_TICKS, PUNCH_RULES, + REFEREE_WALK_SPEED, REST_CORNER_OFFSET, RING_CORNER_REACH, RING_HALF_HEIGHT, RING_HALF_WIDTH, + ROCKED_HURT_POISE, + ROCKED_IMMUNITY_TICKS, + STUN_CHAIN_MAX_TICKS, + STUN_IMMUNITY_TICKS, + STUNNED_SPEED_PERCENT, + TIRED_IMPACT_PERCENT, + TIRED_RECOVERY_TICKS, + TIRED_STARTUP_TICKS, ) from intelstream.hands.types import ( ActionKind, + CombatEvent, + CornerChoice, DefensivePose, + FighterStyle, FinishMethod, Foul, FoulAction, @@ -43,6 +75,9 @@ Target, ) +if TYPE_CHECKING: + from collections.abc import Callable + def make_engine( seed: int = 7, @@ -379,6 +414,8 @@ def test_corner_posts_keep_fighters_out_of_the_corner_pad() -> None: ("name", "value", "key"), [ ("FACING_SCALE", 1024, "facing.scale"), + ("FACING_TURN_DEGREES_PER_SECOND", 0, "facing.turn_degrees_per_second"), + ("FACING_TURN_DEGREES_PER_SECOND", 2700, "facing.turn_degrees_per_second"), ("REST_CORNER_OFFSET", RING_CORNER_REACH // 2 + 1, "rest.corner_offset"), ], ) @@ -420,6 +457,9 @@ def in_ring(fighter: object) -> bool: ((286, -229, 413, -209), (1000, -500), (1000, -500)), ((272, 304, 194, 326), (0, 1000), (707, 707)), ((-351, -314, -206, -413), (-1000, 0), (-1000, 0)), + # The man on the pad walks out at the other, who walks him back onto it. + ((-214, 328, -306, 364), (-931, 364), (981, 193)), + ((-162, -269, -251, -315), (-707, -707), (707, -707)), ], ) def test_fighters_pressing_into_a_corner_come_to_rest( @@ -446,6 +486,51 @@ def test_fighters_pressing_into_a_corner_come_to_rest( assert max(steps[-60:]) <= 3 +def test_a_fighter_walked_back_onto_a_corner_pad_settles_there_without_a_wobble() -> None: + def against_the_drift(values: list[int]) -> int: + """The largest step a coordinate takes back against where it has gone in the window.""" + net = values[-1] - values[0] + steps = [values[index] - values[index - 1] for index in range(1, len(values))] + return max((abs(step) for step in steps if net == 0 or (step > 0) != (net > 0)), default=0) + + def toward(walker: FighterState, target: FighterState) -> tuple[int, int]: + distance = max(1.0, hypot(target.x - walker.x, target.y - walker.y)) + return ( + round((target.x - walker.x) / distance * 1000), + round((target.y - walker.y) / distance * 1000), + ) + + rng = random.Random(2) + for _ in range(24): + engine = make_engine(seed=97, round_ticks=5000) + one, two = engine.fighter("one"), engine.fighter("two") + sign_x, sign_y = rng.choice((-1, 1)), rng.choice((-1, 1)) + across = rng.randrange(RING_CORNER_REACH - 462, 463) + two.x = sign_x * across + two.y = sign_y * min(462, RING_CORNER_REACH - across - rng.randrange(80)) + one.x = two.x - sign_x * rng.randrange(30, 100) + one.y = two.y - sign_y * rng.randrange(30, 100) + into_the_corner = rng.choice((True, False)) + trail = [] + for sequence in range(1, 161): + press = (sign_x * 707, sign_y * 707) if into_the_corner else toward(one, two) + out = toward(two, one) + engine.step( + { + "one": command(sequence, move_x=press[0], move_y=press[1]), + # Walking out at him behind a high guard, slower than the man pressing him. + "two": command( + sequence, move_x=out[0], move_y=out[1], defense=DefensivePose.GUARD_HIGH + ), + } + ) + assert in_ring(one) and in_ring(two) + assert (one.x - two.x) ** 2 + (one.y - two.y) ** 2 >= MINIMUM_SEPARATION**2 + trail.append((one.x, one.y, two.x, two.y)) + settled = trail[-48:] + assert max(against_the_drift([state[i] for state in settled]) for i in range(4)) <= 2 + + def test_fighters_meeting_at_an_angle_are_not_thrown_apart() -> None: engine = make_engine(seed=98, round_ticks=5000) one, two = engine.fighter("one"), engine.fighter("two") @@ -499,7 +584,7 @@ def slide(start: tuple[int, int], move: tuple[int, int]) -> list[tuple[int, int, ("winner_at", "downed_at"), [((367, 366), (300, 300)), ((333, 400), (265, 328)), ((-440, -293), (-370, -230))], ) -def test_cornered_winner_still_reaches_neutral_distance( +def test_cornered_winner_still_reaches_a_neutral_corner( winner_at: tuple[int, int], downed_at: tuple[int, int] ) -> None: engine = make_engine(seed=100, round_ticks=5000) @@ -510,14 +595,13 @@ def test_cornered_winner_still_reaches_neutral_distance( engine._knock_down(downed, winner) assert engine.phase is MatchPhase.KNOCKDOWN - for _ in range(150): + for _ in range(8 * COUNT_TICK_INTERVAL): engine.step() assert in_ring(winner) assert hypot(winner.x - downed.x, winner.y - downed.y) >= MINIMUM_SEPARATION - 8 - if engine.phase is not MatchPhase.KNOCKDOWN: - break - assert hypot(winner.x - downed.x, winner.y - downed.y) >= KNOCKDOWN_NEUTRAL_SEPARATION + corners = [(-REST_CORNER_OFFSET, REST_CORNER_OFFSET), (REST_CORNER_OFFSET, -REST_CORNER_OFFSET)] + assert min(hypot(winner.x - x, winner.y - y) for x, y in corners) <= REFEREE_WALK_SPEED assert (winner.velocity_x, winner.velocity_y) == (0, 0) @@ -622,7 +706,7 @@ def test_combo_window_rewards_chaining_different_punches() -> None: def test_high_low_guard_guard_wear_break_and_perfect_block() -> None: engine = make_engine() defender = engine.fighter("two") - defender.guard = 20 + defender.guard = GUARD_BLOCK_MINIMUM + 40 engine.step({"one": command(1, action=punch(target=Target.HEAD, power=Power.POWER))}) attack = engine.fighter("one").attack assert attack is not None @@ -631,8 +715,19 @@ def test_high_low_guard_guard_wear_break_and_perfect_block() -> None: engine.step({"two": command(1, defense=DefensivePose.GUARD_HIGH)}) assert advance_until(engine, {"perfect_block"}) == "perfect_block" - assert defender.guard < 20 - assert any(event.kind == "guard_break" for event in engine.events) + assert defender.guard < GUARD_BLOCK_MINIMUM + 40 + + worn = make_engine() + worn.fighter("two").guard = GUARD_BLOCK_MINIMUM + worn.step( + { + "one": command(1, action=punch(PunchClass.HOOK, target=Target.HEAD, power=Power.POWER)), + "two": command(1, defense=DefensivePose.GUARD_HIGH), + } + ) + assert advance_until(worn, {"block", "perfect_block"}) == "block" + assert worn.fighter("two").guard == 0 + assert any(event.kind == "guard_break" for event in worn.events) body_engine = make_engine() body_engine.step( @@ -731,6 +826,23 @@ def test_clinch_draws_the_fighters_to_the_hold_distance_before_the_break() -> No assert gap >= MINIMUM_SEPARATION +@pytest.mark.parametrize(("rounds", "rest_ticks"), [(2, 10), (2, 0), (1, 10)]) +def test_the_referee_breaks_a_clinch_still_on_at_the_bell(rounds: int, rest_ticks: int) -> None: + engine = make_engine(round_ticks=30, rounds=rounds, rest_ticks=rest_ticks) + engine.step({"one": command(1, action=MovementAction(ActionKind.CLINCH))}) + assert advance_until(engine, {"clinch"}, limit=12) == "clinch" + one, two = engine.fighter("one"), engine.fighter("two") + assert one.clinch_ticks > 30 - engine.tick + + snapshot = engine.step() + while not any(event.detail == "round_end" for event in snapshot.events): + snapshot = engine.step() + + assert "referee_break" in [event.kind for event in snapshot.events] + assert [fighter.clinch_ticks for fighter in snapshot.fighters] == [0, 0] + assert hypot(two.x - one.x, two.y - one.y) >= MINIMUM_SEPARATION + + def test_a_clinch_stops_both_fighters_for_the_whole_hold() -> None: engine = make_engine(round_ticks=2000) one = engine.fighter("one") @@ -775,6 +887,7 @@ def test_out_of_range_clinch_is_denied_and_still_costs_stamina() -> None: def test_fouls_warn_deduct_recover_and_disqualify() -> None: engine = make_engine(round_ticks=2000) for sequence, foul in enumerate((Foul.LOW_BLOW, Foul.HEADBUTT, Foul.LOW_BLOW), start=1): + engine.fighter("one").x, engine.fighter("two").x = -45, 45 engine.step({"one": command(sequence, action=FoulAction(foul))}) if sequence < 3: while engine.phase is MatchPhase.FOUL_RECOVERY: @@ -842,7 +955,8 @@ def test_rest_walks_both_fighters_to_their_corners_and_seats_them_facing_the_rin assert (two.x, two.y) == (REST_CORNER_OFFSET, REST_CORNER_OFFSET) assert (one.velocity_x, one.velocity_y) == (0, 0) assert (two.velocity_x, two.velocity_y) == (0, 0) - for step in range(12): + # Arrived facing his corner, he turns round to face the ring at the turn rate. + for step in range(math.ceil(180 / TURN_PER_TICK)): engine.step({"one": command(300 + step, move_x=1000, move_y=1000)}) assert (one.x, one.y) == (-REST_CORNER_OFFSET, -REST_CORNER_OFFSET) assert one.facing_x > 0 and one.facing_y > 0 @@ -930,7 +1044,7 @@ def test_knockdown_seeded_get_up_and_repeated_knockdown_tko() -> None: sequence = 2 used_window = -1 - while engine.phase is MatchPhase.KNOCKDOWN and engine.tick < 240: + while engine.phase is MatchPhase.KNOCKDOWN and engine.tick < 400: snapshot = engine.snapshot() fighter = next(item for item in snapshot.fighters if item.player_id == "two") if ( @@ -951,7 +1065,10 @@ def test_knockdown_seeded_get_up_and_repeated_knockdown_tko() -> None: else: engine.step() assert engine.phase is MatchPhase.FIGHT - assert any(event.kind == "get_up" for event in engine.events) + assert [event.kind for event in engine.events if event.kind in {"get_up", "box"}] == [ + "get_up", + "box", + ] tko = make_engine(round_ticks=2000) tko.fighter("two").knockdowns = 2 @@ -1271,7 +1388,7 @@ def test_facing_vector_turns_toward_the_opponent_and_the_hit_test_follows() -> N two.x = 0 two.y = 100 assert (one.facing_x, one.facing_y) == (1000, 0) - for _ in range(12): + for _ in range(math.ceil(90 / TURN_PER_TICK) + 1): engine.step() assert one.facing_y > 950 assert abs(one.facing_x) < 200 @@ -1288,36 +1405,21 @@ def test_facing_vector_turns_toward_the_opponent_and_the_hit_test_follows() -> N assert payload["fighters"][1]["last_input_sequence"] == -1 -def test_knockdown_walks_the_standing_fighter_to_neutral_distance_without_teleport() -> None: +def test_the_standing_fighter_walks_to_the_far_neutral_corner_without_teleporting() -> None: engine = make_engine(round_ticks=2000) - defender = engine.fighter("two") - attacker = engine.fighter("one") - defender.poise = 1 - engine.step({"one": command(1, action=punch(PunchClass.UPPERCUT, power=Power.POWER))}) - advance_until(engine, {"knockdown"}) - downed_at = (defender.x, defender.y) - for _ in range(40): + one, two = engine.fighter("one"), engine.fighter("two") + _down_two(engine) + downed_at = (two.x, two.y) + neutral = [(-REST_CORNER_OFFSET, REST_CORNER_OFFSET), (REST_CORNER_OFFSET, -REST_CORNER_OFFSET)] + far = max(neutral, key=lambda corner: hypot(corner[0] - two.x, corner[1] - two.y)) + while engine._knockdown_count_ticks < MANDATORY_COUNT * COUNT_TICK_INTERVAL: + before = (one.x, one.y) engine.step() - assert (defender.x, defender.y) == downed_at - assert hypot(attacker.x - defender.x, attacker.y - defender.y) >= 230 - assert attacker.velocity_x == 0 and attacker.velocity_y == 0 - - sequence = 2 - used_window = -1 - while engine.phase is MatchPhase.KNOCKDOWN and engine.tick < 300: - fighter = next(item for item in engine.snapshot().fighters if item.player_id == "two") - if ( - fighter.get_up_prompt is not None - and engine.tick >= fighter.get_up_window_start_tick - and fighter.get_up_window_end_tick != used_window - ): - engine.step({"two": command(sequence, action=MovementAction(fighter.get_up_prompt))}) - used_window = fighter.get_up_window_end_tick - sequence += 1 - else: - engine.step() - assert engine.phase is MatchPhase.FIGHT - assert (defender.x, defender.y) == downed_at + assert hypot(one.x - before[0], one.y - before[1]) <= REFEREE_WALK_SPEED + 1 + assert hypot(one.x - two.x, one.y - two.y) >= MINIMUM_SEPARATION - 8 + assert (two.x, two.y) == downed_at + assert hypot(one.x - far[0], one.y - far[1]) <= REFEREE_WALK_SPEED + assert (one.velocity_x, one.velocity_y) == (0, 0) def test_short_buffered_combo_only_rewards_authored_compatible_chain() -> None: @@ -1609,6 +1711,7 @@ def test_completed_close_round_applies_live_foul_deduction() -> None: engine.step({"one": command(1, action=FoulAction(Foul.LOW_BLOW))}) while engine.phase is MatchPhase.FOUL_RECOVERY: engine.step() + engine.fighter("one").x, engine.fighter("two").x = -45, 45 engine.step({"one": command(2, action=FoulAction(Foul.HEADBUTT))}) while engine.phase is MatchPhase.FOUL_RECOVERY: engine.step() @@ -1996,6 +2099,467 @@ def test_identical_class_actions_get_distinct_fallback_ids() -> None: assert first != second +def rest_engine(**trauma: int) -> BoxingEngine: + """An engine one tick into the rest after round one, with fighter one carrying `trauma`.""" + engine = make_engine(round_ticks=20, rounds=2, rest_ticks=90) + one = engine.fighter("one") + for name, value in trauma.items(): + setattr(one.trauma, name, value) + while engine.phase is not MatchPhase.REST: + engine.step() + return engine + + +def corner(sequence: int, kind: ActionKind) -> InputCommand: + return command(sequence, action=MovementAction(kind)) + + +def test_corner_closes_the_worse_cut_once_per_rest() -> None: + engine = rest_engine(left_cut=600, right_cut=200, bleeding=300) + one = engine.fighter("one") + assert one.corner_choice is None + snapshot = engine.step({"one": corner(1, ActionKind.CORNER_CUT)}) + + treatment = CORNER_TREATMENTS[CornerChoice.CUT] + assert [(event.kind, event.detail) for event in snapshot.events] == [("corner", "cut")] + assert one.trauma.left_cut == 600 - treatment.worse_cut + assert one.trauma.right_cut == 200 - treatment.other_cut + assert one.trauma.bleeding <= 300 * treatment.bleeding_kept_percent // 100 + assert snapshot.fighters[0].corner_choice is CornerChoice.CUT + + cut_after = (one.trauma.left_cut, one.trauma.right_cut) + snapshot = engine.step({"one": corner(2, ActionKind.CORNER_BREATH)}) + assert not [event for event in snapshot.events if event.kind == "corner"] + assert one.corner_choice is CornerChoice.CUT + while engine.phase is MatchPhase.REST: + engine.step() + assert (one.trauma.left_cut, one.trauma.right_cut) == cut_after + assert one.corner_choice is CornerChoice.CUT + + +def test_corner_treats_the_right_cut_when_it_is_the_worse() -> None: + engine = rest_engine(left_cut=100, right_cut=500) + engine.step({"one": corner(1, ActionKind.CORNER_CUT)}) + one = engine.fighter("one") + treatment = CORNER_TREATMENTS[CornerChoice.CUT] + assert one.trauma.right_cut == 500 - treatment.worse_cut + assert one.trauma.left_cut == max(0, 100 - treatment.other_cut) + + +def test_corner_brings_down_the_swelling_and_can_open_a_shut_eye() -> None: + engine = rest_engine(right_eye=BLIND_SIDE_EYE_THRESHOLD + 50, left_eye=120, swelling=500) + engine.step({"one": corner(1, ActionKind.CORNER_SWELLING)}) + one = engine.fighter("one") + treatment = CORNER_TREATMENTS[CornerChoice.SWELLING] + assert one.trauma.right_eye == BLIND_SIDE_EYE_THRESHOLD + 50 - treatment.eyes + assert one.trauma.right_eye < BLIND_SIDE_EYE_THRESHOLD + assert one.trauma.left_eye == max(0, 120 - treatment.eyes) + assert one.trauma.swelling == 500 - treatment.swelling + + +def test_corner_catches_the_breath() -> None: + engine = rest_engine(body=600) + one = engine.fighter("one") + one.conditioning = 500 + one.stamina = 100 + one.poise = 50 + engine.step({"one": corner(1, ActionKind.CORNER_BREATH)}) + treatment = CORNER_TREATMENTS[CornerChoice.BREATH] + assert one.conditioning == 500 + treatment.conditioning + assert one.trauma.body == 600 - treatment.body + assert one.stamina == one.maximum_stamina + assert one.poise == 600 + + +def test_no_corner_instruction_gets_the_balanced_treatment_at_the_bell() -> None: + engine = rest_engine(left_cut=400, swelling=300, right_eye=500) + one = engine.fighter("one") + one.conditioning = 700 + while engine.phase is MatchPhase.REST: + snapshot = engine.step() + treatment = CORNER_TREATMENTS[CornerChoice.BALANCED] + assert ("corner", "balanced") in [(event.kind, event.detail) for event in snapshot.events] + assert one.corner_choice is CornerChoice.BALANCED + assert one.trauma.left_cut == 400 - treatment.worse_cut + assert one.trauma.swelling == 300 - treatment.swelling + assert one.trauma.right_eye == 500 - treatment.eyes + assert one.conditioning == 700 + + +def test_corner_choice_resets_for_each_rest() -> None: + engine = make_engine(round_ticks=20, rounds=3, rest_ticks=40) + while engine.phase is not MatchPhase.REST: + engine.step() + engine.step({"one": corner(1, ActionKind.CORNER_CUT)}) + while engine.phase is MatchPhase.REST: + engine.step() + while engine.phase is not MatchPhase.REST: + engine.step() + assert engine.fighter("one").corner_choice is None + assert engine.snapshot().fighters[0].corner_choice is None + + +def test_corner_instructions_outside_the_rest_do_nothing() -> None: + engine = make_engine(round_ticks=200) + one = engine.fighter("one") + one.trauma.left_cut = 500 + for sequence, kind in enumerate( + (ActionKind.CORNER_CUT, ActionKind.CORNER_SWELLING, ActionKind.CORNER_BREATH), start=1 + ): + engine.step({"one": corner(sequence, kind)}) + for _ in range(8): + engine.step() + assert not [event for event in engine.events if event.kind == "corner"] + assert one.trauma.left_cut == 500 + assert one.corner_choice is None + + +def test_a_bout_without_a_rest_has_no_corner() -> None: + engine = make_engine(round_ticks=20, rounds=2, rest_ticks=0) + while engine.round_number == 1: + engine.step() + assert not [event for event in engine.events if event.kind == "corner"] + + +def blind_engine(right_eye: int) -> BoxingEngine: + engine = make_engine(round_ticks=2000) + engine.fighter("two").trauma.right_eye = right_eye + return engine + + +def test_a_shut_eye_cannot_see_the_punch_coming() -> None: + lead_straight = punch(PunchClass.STRAIGHT, hand=Hand.LEFT) + slip = MovementAction(ActionKind.SLIP_RIGHT) + + seeing = blind_engine(BLIND_SIDE_EYE_THRESHOLD - 1) + seeing.step({"one": command(1, action=lead_straight), "two": command(1, action=slip)}) + assert advance_until(seeing, {"evade", "hit"}) == "evade" + + blind = blind_engine(BLIND_SIDE_EYE_THRESHOLD) + blind.step({"one": command(1, action=lead_straight), "two": command(1, action=slip)}) + assert advance_until(blind, {"evade", "hit"}) == "hit" + landed = [event for event in blind.events if event.kind == "blind_side"] + assert [(event.actor_id, event.target_id, event.detail) for event in landed] == [ + ("one", "two", "right") + ] + + +def test_a_punch_on_the_blind_side_lands_harder() -> None: + seeing = blind_engine(0) + blind = blind_engine(BLIND_SIDE_EYE_THRESHOLD) + for engine in (seeing, blind): + engine.step({"one": command(1, action=punch(PunchClass.STRAIGHT, hand=Hand.LEFT))}) + advance_until(engine, {"hit"}) + seen = next(event for event in seeing.events if event.kind == "hit") + unseen = next(event for event in blind.events if event.kind == "hit") + assert unseen.amount == seen.amount * 120 // 100 + assert unseen.amount > seen.amount + + +def test_the_open_side_is_still_seen() -> None: + engine = blind_engine(1000) + engine.step( + { + "one": command(1, action=punch(PunchClass.STRAIGHT, hand=Hand.RIGHT)), + "two": command(1, action=MovementAction(ActionKind.SLIP_LEFT)), + } + ) + assert advance_until(engine, {"evade", "hit"}) == "evade" + assert not [event for event in engine.events if event.kind == "blind_side"] + + +def perfect_guard_engine(power: Power, right_eye: int = 0) -> BoxingEngine: + """Fighter two raises the high guard two ticks before fighter one's left straight lands.""" + engine = make_engine(round_ticks=2000) + engine.fighter("two").trauma.right_eye = right_eye + engine.step({"one": command(1, action=punch(PunchClass.STRAIGHT, hand=Hand.LEFT, power=power))}) + attack = engine.fighter("one").attack + assert attack is not None + while attack.age < attack.rule.startup - 2: + engine.step() + engine.step({"two": command(1, defense=DefensivePose.GUARD_HIGH)}) + return engine + + +def test_no_perfect_block_on_the_blind_side() -> None: + assert advance_until(perfect_guard_engine(Power.NORMAL), {"block", "perfect_block"}) == ( + "perfect_block" + ) + blind = perfect_guard_engine(Power.NORMAL, right_eye=BLIND_SIDE_EYE_THRESHOLD) + assert advance_until(blind, {"block", "perfect_block"}) == "block" + + +def test_an_eye_swelling_shut_is_announced_once() -> None: + engine = blind_engine(BLIND_SIDE_EYE_THRESHOLD - 60) + engine.step({"one": command(1, action=punch(PunchClass.HOOK, hand=Hand.LEFT))}) + advance_until(engine, {"hit"}) + shut = [event for event in engine.events if event.kind == "eye_shut"] + assert [(event.target_id, event.detail) for event in shut] == [("two", "right")] + while engine.fighter("one").attack is not None: + engine.step() + for _ in range(30): + engine.step() + engine.step({"one": command(2, action=punch(PunchClass.HOOK, hand=Hand.LEFT))}) + advance_until(engine, {"hit"}) + assert len([event for event in engine.events if event.kind == "eye_shut"]) == 1 + + +def test_parrying_a_power_punch_staggers_the_puncher() -> None: + engine = perfect_guard_engine(Power.POWER) + assert advance_until(engine, {"block", "perfect_block"}) == "perfect_block" + parries = [event for event in engine.events if event.kind == "parry"] + assert [(event.actor_id, event.target_id) for event in parries] == [("two", "one")] + one = engine.fighter("one") + assert one.stunned_ticks == PARRY_STAGGER_TICKS + assert one.attack is None + + engine.step({"two": command(2, action=punch(PunchClass.JAB, hand=Hand.LEFT))}) + assert advance_until(engine, {"counter_hit", "hit", "block", "perfect_block"}) == "counter_hit" + + +@pytest.mark.parametrize("gap", [110, 130, 150]) +@pytest.mark.parametrize("answer_after", [4, 8]) +@pytest.mark.parametrize( + ("counter", "hand"), [(PunchClass.JAB, Hand.LEFT), (PunchClass.STRAIGHT, Hand.RIGHT)] +) +def test_a_parried_fighter_cannot_walk_out_of_the_counter_it_opens( + gap: int, answer_after: int, counter: PunchClass, hand: Hand +) -> None: + engine = make_engine(round_ticks=2000) + one = engine.fighter("one") + one.x, engine.fighter("two").x = -(gap // 2), gap - gap // 2 + # He throws the power straight holding back, ready to walk away from whatever comes back. + straight = punch(PunchClass.STRAIGHT, power=Power.POWER) + engine.step({"one": command(1, action=straight, move_x=-1000)}) + attack = one.attack + assert attack is not None + while attack.age < attack.rule.startup - 2: + engine.step() + engine.step({"two": command(1, defense=DefensivePose.GUARD_HIGH)}) + assert advance_until(engine, {"parry", "block", "hit"}) == "parry" + parried_at = (one.x, one.y) + for _ in range(answer_after - 1): + engine.step() + engine.step({"two": command(2, action=punch(counter, hand=hand))}) + assert advance_until(engine, {"counter_hit", "hit", "whiff", "block"}) == "counter_hit" + assert (one.x, one.y) == parried_at + # Once the stagger is over he walks away at full speed again. + while one.stunned_ticks: + engine.step() + for _ in range(8): + engine.step() + assert one.velocity_x == -7 + + +def test_parrying_an_ordinary_punch_does_not_stagger() -> None: + engine = perfect_guard_engine(Power.NORMAL) + assert advance_until(engine, {"block", "perfect_block"}) == "perfect_block" + assert not [event for event in engine.events if event.kind == "parry"] + assert engine.fighter("one").stunned_ticks == 0 + + +def _power_straight_into(guard_at: Callable[[int], DefensivePose], start: int = 30) -> str: + """How a power straight thrown at tick `start` meets a defender holding `guard_at(tick)`.""" + engine = make_engine(round_ticks=2000) + one = engine.fighter("one") + while engine.tick + 1 < start: + tick = engine.tick + 1 + engine.step({"two": command(tick, defense=guard_at(tick))}) + straight = punch(PunchClass.STRAIGHT, power=Power.POWER) + engine.step( + {"one": command(1, action=straight), "two": command(start, defense=guard_at(start))} + ) + assert one.attack is not None and one.attack.start_tick == start + for _ in range(30): + tick = engine.tick + 1 + for event in engine.step({"two": command(tick, defense=guard_at(tick))}).events: + if event.kind in ("hit", "counter_hit", "block", "perfect_block"): + return event.kind + raise AssertionError("the straight never arrived") + + +@pytest.mark.parametrize(("up", "down"), [(3, 2), (4, 1), (5, 1), (6, 2)]) +def test_a_guard_flicked_up_and_down_blocks_but_never_parries(up: int, down: int) -> None: + outcomes = [] + for phase in range(up + down): + + def flicker(tick: int, phase: int = phase) -> DefensivePose: + up_now = (tick + phase) % (up + down) < up + return DefensivePose.GUARD_HIGH if up_now else DefensivePose.NONE + + outcomes.append(_power_straight_into(flicker)) + assert "perfect_block" not in outcomes + assert "block" in outcomes + + +@pytest.mark.parametrize( + ("down_for", "lead", "expected"), + [ + (PERFECT_BLOCK_REARM_TICKS, 2, "perfect_block"), + (PERFECT_BLOCK_REARM_TICKS, 4, "perfect_block"), + (30, 3, "perfect_block"), + (PERFECT_BLOCK_REARM_TICKS - 1, 3, "block"), + (1, 2, "block"), + ], +) +def test_a_guard_raised_late_after_being_let_down_a_moment_still_parries( + down_for: int, lead: int, expected: str +) -> None: + contact = 30 + PUNCH_RULES[(PunchClass.STRAIGHT, Target.HEAD, Power.POWER)].startup + raised = contact - lead + + def guard_at(tick: int) -> DefensivePose: + down = raised - down_for <= tick < raised + return DefensivePose.NONE if down else DefensivePose.GUARD_HIGH + + assert _power_straight_into(guard_at) == expected + + +def test_switching_guards_or_coming_back_from_a_stun_is_no_parry() -> None: + contact = 30 + PUNCH_RULES[(PunchClass.STRAIGHT, Target.HEAD, Power.POWER)].startup + + def switched(tick: int) -> DefensivePose: + return DefensivePose.GUARD_LOW if tick < contact - 2 else DefensivePose.GUARD_HIGH + + assert _power_straight_into(switched) == "block" + # Held through a stun, the guard comes back up as the stun ends: that is no raise either. + engine = make_engine(round_ticks=2000) + two = engine.fighter("two") + engine.step({"two": command(1, defense=DefensivePose.GUARD_HIGH)}) + for _ in range(10): + engine.step() + two.stunned_ticks = 6 + engine.step({"one": command(1, action=punch(PunchClass.STRAIGHT, power=Power.POWER))}) + assert advance_until(engine, {"hit", "block", "perfect_block"}) == "block" + assert engine.tick - two.defense_started_tick <= PERFECT_BLOCK_TICKS + + +def body_shot_engine( + *, body: int, stamina: int, guard: DefensivePose = DefensivePose.NONE, guard_meter: int = 700 +) -> BoxingEngine: + engine = make_engine(round_ticks=2000) + two = engine.fighter("two") + two.trauma.body = body + two.stamina = stamina + two.guard = guard_meter + engine.step( + { + "one": command( + 1, + action=PunchAction( + Hand.LEFT, PunchClass.HOOK, Target.BODY, client_action_id="liver" + ), + ), + "two": command(1, defense=guard), + } + ) + return engine + + +def test_a_broken_body_goes_down_after_a_moment() -> None: + engine = body_shot_engine(body=700, stamina=300) + advance_until(engine, {"hit"}) + collapse = [event for event in engine.events if event.kind == "body_collapse"] + assert [(event.actor_id, event.target_id, event.action_id) for event in collapse] == [ + ("one", "two", "liver") + ] + two = engine.fighter("two") + assert engine.phase is MatchPhase.FIGHT + assert two.stunned_ticks > 0 + for _ in range(BODY_COLLAPSE_DELAY_TICKS - 1): + snapshot = engine.step() + assert engine.phase is MatchPhase.FIGHT + assert not [event for event in snapshot.events if event.kind == "knockdown"] + snapshot = engine.step() + knockdowns = [event for event in snapshot.events if event.kind == "knockdown"] + assert [(event.target_id, event.detail, event.action_id) for event in knockdowns] == [ + ("two", "body", "liver") + ] + assert engine.phase is MatchPhase.KNOCKDOWN + assert two.knockdowns == 1 + + +@pytest.mark.parametrize( + ("body", "stamina", "guard", "guard_meter"), + [ + (500, 300, DefensivePose.NONE, 700), + (700, 900, DefensivePose.NONE, 700), + # A worn guard leaks most of the shot, but a blocked shot still never drops him. + (700, 300, DefensivePose.GUARD_LOW, 100), + ], +) +def test_a_body_shot_only_drops_a_tired_broken_unguarded_fighter( + body: int, stamina: int, guard: DefensivePose, guard_meter: int +) -> None: + engine = body_shot_engine(body=body, stamina=stamina, guard=guard, guard_meter=guard_meter) + contact = advance_until(engine, {"hit", "block", "perfect_block"}) + if guard is not DefensivePose.NONE: + assert contact == "block" + blocked = next(event for event in engine.events if event.kind == "hit") + assert blocked.amount >= 40 + for _ in range(BODY_COLLAPSE_DELAY_TICKS + 5): + engine.step() + assert not [event for event in engine.events if event.kind in ("body_collapse", "knockdown")] + + +def test_the_bell_saves_a_fighter_who_is_going_down_from_a_body_shot() -> None: + engine = body_shot_engine(body=700, stamina=300) + advance_until(engine, {"hit"}) + assert engine.fighter("two").body_collapse_ticks > 0 + engine.phase_ticks_remaining = 2 + while engine.phase is MatchPhase.FIGHT: + engine.step() + assert engine.phase is MatchPhase.REST + for _ in range(BODY_COLLAPSE_DELAY_TICKS + 5): + engine.step() + assert not [event for event in engine.events if event.kind == "knockdown"] + assert engine.fighter("two").body_collapse_ticks == 0 + + +def test_a_knockdown_from_a_body_shot_says_so() -> None: + engine = make_engine(round_ticks=2000) + engine.fighter("two").poise = 1 + engine.step( + { + "one": command( + 1, + action=PunchAction( + Hand.RIGHT, PunchClass.UPPERCUT, Target.BODY, client_action_id="dig" + ), + ) + } + ) + advance_until(engine, {"knockdown"}) + knockdown = next(event for event in engine.events if event.kind == "knockdown") + assert (knockdown.detail, knockdown.action_id) == ("body", "dig") + + head = make_engine(round_ticks=2000) + head.fighter("two").poise = 1 + head.step({"one": command(1, action=punch(PunchClass.UPPERCUT))}) + advance_until(head, {"knockdown"}) + knockdown = next(event for event in head.events if event.kind == "knockdown") + assert (knockdown.detail, knockdown.action_id) == ("", None) + + +def test_a_clean_body_shot_takes_the_wind_and_a_blocked_one_does_not() -> None: + for guard, winded in ((DefensivePose.NONE, True), (DefensivePose.GUARD_LOW, False)): + engine = body_shot_engine(body=0, stamina=500, guard=guard) + attack = engine.fighter("one").attack + assert attack is not None + while attack.age + 1 < attack.rule.startup: + engine.step() + before = engine.fighter("two").stamina + snapshot = engine.step() + contact = next(event for event in snapshot.events if event.kind in ("hit", "block")) + lost = before - engine.fighter("two").stamina + if winded: + assert contact.kind == "hit" + assert lost >= contact.amount * BODY_WIND_PERCENT // 100 - 4 + else: + assert contact.kind == "block" + assert lost <= 0 + + def test_recovery_is_only_cut_short_for_a_follow_up_the_fighter_can_afford() -> None: engine = make_engine(round_ticks=2000) one = engine.fighter("one") @@ -2089,6 +2653,109 @@ def test_an_early_buffered_hook_uppercut_chain_cannot_hold_a_stunned_fighter( assert free_ticks > 0 +@dataclasses.dataclass +class _ChainAgainstAStillDefender: + stuns: list[int] = dataclasses.field(default_factory=list) + longest_chain: int = 0 + free_ticks: int = 0 + hits_on_a_running_stun: int = 0 + flinching_hits_in_the_clear_moment: int = 0 + rocked_in_the_clear_moment: int = 0 + + +def _chain_into_a_still_defender(poise: int | None, power: Power) -> _ChainAgainstAStillDefender: + """A straight, then a hook and uppercut chain with one early press per punch, into a defender + who stands still until poise puts him down. Asserts the stun rules on every tick.""" + engine = make_engine(round_ticks=2000) + one = engine.fighter("one") + two = engine.fighter("two") + if poise is not None: + two.poise = poise + follow_ups = { + PunchClass.STRAIGHT: punch(PunchClass.HOOK, hand=Hand.LEFT, power=power), + PunchClass.HOOK: punch(PunchClass.UPPERCUT, power=power), + PunchClass.UPPERCUT: punch(PunchClass.HOOK, hand=Hand.LEFT, power=power), + } + engine.step({"one": command(1, action=punch(PunchClass.STRAIGHT, power=power))}) + seen = _ChainAgainstAStillDefender() + pressed_during = None + sequence = 1 + worn_off_at: int | None = None + while engine.phase is MatchPhase.FIGHT and engine.tick < 600: + inputs = {} + attack = one.attack + if attack is not None and attack is not pressed_during: + sequence += 1 + inputs["one"] = command(sequence, action=follow_ups[attack.action.punch_class]) + pressed_during = attack + running = two.stunned_ticks + before = one.attack + snapshot = engine.step(inputs) + if engine.phase is not MatchPhase.FIGHT: + break + events = snapshot.events + flinching_hit = any( + event.kind in ("hit", "counter_hit") and event.amount >= FLINCH_MINIMUM_DAMAGE + for event in events + ) + rocked = any(event.kind == "stun" and event.target_id == "two" for event in events) + assert two.stun_chain_ticks <= STUN_CHAIN_MAX_TICKS + seen.longest_chain = max(seen.longest_chain, two.stun_chain_ticks) + if running > 1: + assert two.stunned_ticks == running - 1, "a punch started a running stun again" + cut_short = before is not None and one.attack is not None and one.attack is not before + assert not cut_short, "a punch was cut short into a stunned fighter" + seen.hits_on_a_running_stun += flinching_hit + continue + # A stun that wears off this tick opens the clear moment, whichever fighter moved first. + clear_moment = running == 1 or ( + worn_off_at is not None and engine.tick - worn_off_at < STUN_IMMUNITY_TICKS + ) + if running == 1: + worn_off_at = engine.tick + if two.stunned_ticks > 0: + seen.stuns.append(two.stunned_ticks) + assert two.stunned_ticks <= STUN_CHAIN_MAX_TICKS + assert worn_off_at is None or engine.tick - worn_off_at >= ROCKED_IMMUNITY_TICKS, ( + "a stun started again before he could raise a guard" + ) + if clear_moment: + assert rocked, "a flinch landed in the clear moment after a stun" + seen.rocked_in_the_clear_moment += 1 + else: + seen.free_ticks += bool(seen.stuns) + if running == 0 and clear_moment: + seen.flinching_hits_in_the_clear_moment += flinching_hit + return seen + + +def test_a_still_defender_gets_free_ticks_from_a_chain_of_combinations_inside_the_stun_cap() -> ( + None +): + # Fresh, he flinches. A shot hard enough to flinch him does not stop him again in the clear + # moment after a stun, and a punch that lands on a running stun does not start it again. + fresh = _chain_into_a_still_defender(None, Power.NORMAL) + assert len(fresh.stuns) >= 3 + assert fresh.free_ticks > 0 + assert fresh.flinching_hits_in_the_clear_moment > 0 + assert fresh.hits_on_a_running_stun > 0 + + # Hurt, every clean shot rocks him, and still the chain cannot hold him. + hurt = _chain_into_a_still_defender(ROCKED_HURT_POISE - 1, Power.NORMAL) + assert hurt.stuns and hurt.hits_on_a_running_stun > 0 + assert hurt.free_ticks > 0 + + # Power shots rock him even in the clear moment, but never past the chain cap. + power = _chain_into_a_still_defender(None, Power.POWER) + assert power.rocked_in_the_clear_moment > 0 + assert power.hits_on_a_running_stun > 0 + assert power.free_ticks > 0 + + for seen in (fresh, hurt, power): + assert max(seen.stuns) <= STUN_CHAIN_MAX_TICKS + assert seen.longest_chain <= STUN_CHAIN_MAX_TICKS + + def test_fighters_walking_to_their_corners_go_round_each_other() -> None: engine = make_engine(round_ticks=3, rounds=2, rest_ticks=300) one = engine.fighter("one") @@ -2121,6 +2788,27 @@ def test_the_bell_clears_evasion_counter_and_combo_windows(rest_ticks: int) -> N assert (one.evasion_ticks, one.counter_ticks, one.combo_ticks) == (0, 0, 0) +@pytest.mark.parametrize("rest_ticks", [150, 0]) +def test_the_bell_ends_a_stun_and_its_clear_moment_along_with_the_held_input( + rest_ticks: int, +) -> None: + engine = make_engine(round_ticks=30, rounds=2, rest_ticks=rest_ticks) + two = engine.fighter("two") + engine.step({"two": command(1, move_x=-1000, defense=DefensivePose.GUARD_HIGH)}) + while engine.phase_ticks_remaining > 2: + engine.step() + # Stunned as the bell goes, still inside the clear moment of an earlier stun. + two.stunned_ticks = 20 + two.stun_chain_ticks = 5 + two.stun_immune_until_tick = engine.tick + STUN_IMMUNITY_TICKS + while engine.round_number == 1: + engine.step() + + assert (two.stunned_ticks, two.stun_chain_ticks, two.stun_immune_until_tick) == (0, 0, -1) + held = two.held_input + assert (held.move_x, held.move_y, held.defense) == (0, 0, DefensivePose.NONE) + + @pytest.mark.parametrize("rest_ticks", [150, 0]) def test_a_walk_and_guard_held_at_the_bell_do_not_carry_into_the_next_round( rest_ticks: int, @@ -2139,14 +2827,554 @@ def test_a_walk_and_guard_held_at_the_bell_do_not_carry_into_the_next_round( assert (one.velocity_x, one.velocity_y) == (0, 0) -def test_the_facing_blends_toward_the_opponent_before_and_after_footwork_each_fight_tick() -> None: +TURN_PER_TICK = rules.FACING_TURN_DEGREES_PER_SECOND / rules.TICKS_PER_SECOND + + +def off_line_degrees(viewer: FighterState, other: FighterState) -> float: + """How far off the way `viewer` faces `other` stands.""" + bearing = math.atan2(other.y - viewer.y, other.x - viewer.x) + facing = math.atan2(viewer.facing_y, viewer.facing_x) + return abs(math.degrees((bearing - facing + math.pi) % (2 * math.pi) - math.pi)) + + +def test_a_standing_fighter_turns_toward_the_other_at_a_fixed_rate() -> None: engine = make_engine(round_ticks=2000) one = engine.fighter("one") two = engine.fighter("two") one.x = one.y = 0 two.x, two.y = 0, 300 + turned = [] + for _ in range(math.ceil(90 / TURN_PER_TICK)): + engine.step() + turned.append(math.degrees(math.atan2(one.facing_y, one.facing_x))) + + # The same few degrees every tick, not a share of what is left: it used to close 58% of the + # gap a tick, so a quarter turn took two ticks and nobody could be outflanked. + assert turned[0] == pytest.approx(TURN_PER_TICK, abs=0.1) + assert turned[len(turned) // 2 - 1] == pytest.approx(TURN_PER_TICK * len(turned) // 2, abs=0.3) + assert turned[-1] == pytest.approx(90, abs=0.1) + assert 999_000 <= one.facing_x**2 + one.facing_y**2 <= 1_001_000 + + +@pytest.mark.parametrize("stepping_with_him", [False, True]) +def test_circling_takes_an_angle_on_a_man_who_stands_but_not_on_one_who_steps_with_him( + stepping_with_him: bool, +) -> None: + def circled(then: dict[str, InputCommand]) -> tuple[float, float, str]: + engine = make_engine(round_ticks=2000) + one, two = engine.fighter("one"), engine.fighter("two") + one.x, two.x = -60, 60 + for sequence in range(1, 31): + # One circles the other a second at full speed, hands down, keeping 120 from him. + dx, dy = two.x - one.x, two.y - one.y + distance = hypot(dx, dy) + across = (-dy / distance, dx / distance) + radial = (distance - 120) / 20 + move = (across[0] + dx / distance * radial, across[1] + dy / distance * radial) + size = hypot(*move) + inputs = { + "one": command( + sequence, + move_x=round(move[0] / size * 1000), + move_y=round(move[1] / size * 1000), + ) + } + if stepping_with_him: + inputs["two"] = command( + sequence, move_x=round(across[0] * 1000), move_y=round(across[1] * 1000) + ) + engine.step(inputs) + angles = (off_line_degrees(one, two), off_line_degrees(two, one)) + engine.step(then) + return *angles, advance_until(engine, {"hit", "counter_hit", "whiff"}) + + straight = punch(PunchClass.STRAIGHT) + circler, other, circler_punch = circled({"one": command(31, action=straight)}) + *_, other_punch = circled({"two": command(31, action=straight)}) + + if stepping_with_him: + # Cutting the ring off, each turns with his own steps: both stay square. + assert circler < TURN_PER_TICK * 2 and other < TURN_PER_TICK * 2 + assert (circler_punch, other_punch) == ("hit", "hit") + else: + # His own steps turn the circler with them. Standing, the other turns only at the turn + # rate and falls behind: off the line of his own straight, it goes past the circler, + # whose straight lands. + assert circler < 1 + assert other > 15 + assert (circler_punch, other_punch) == ("hit", "whiff") + + +def test_the_facing_holds_through_each_punch_but_follows_between_a_combinations_punches() -> None: + engine = make_engine(round_ticks=2000) + one = engine.fighter("one") + jab = punch(PunchClass.JAB, hand=Hand.LEFT) + straight = punch(PunchClass.STRAIGHT, hand=Hand.RIGHT) + # One throws a jab and lets the straight go off its recovery, while two steps round him. + engine.step({"one": command(1, action=jab), "two": command(1, move_y=1000)}) + first = one.attack + assert first is not None + engine.step({"one": command(2, action=straight)}) + held: dict[str, set[tuple[int, int]]] = {"jab": set(), "between": set(), "straight": set()} + for _ in range(30): + attack = one.attack + if attack is not None and attack.age < attack.rule.startup + attack.rule.active: + held[attack.action.punch_class.value].add((one.facing_x, one.facing_y)) + elif attack is not None and attack.action.punch_class is PunchClass.JAB: + held["between"].add((one.facing_x, one.facing_y)) + engine.step() + if one.attack is not None and one.attack is not first and one.attack.age == 0: + assert one.attack.start_tick - first.start_tick == first.cancel_age + + # Each punch keeps the line it was thrown along, so a slip or weave is judged against it, but + # the cancel chain no longer freezes the facing from the jab to the end of the straight. + assert len(held["jab"]) == 1 + assert len(held["straight"]) == 1 + assert len(held["between"]) > 1 + assert held["jab"] != held["straight"] + + +def _manifest() -> dict[str, dict[str, int]]: + resource = resources.files("intelstream.hands").joinpath("combat-manifest.json") + return json.loads(resource.read_text()) # type: ignore[no-any-return] + + +def test_the_most_get_up_presses_asked_for_is_the_bound_the_client_derives() -> None: + # web/hands/src/manifest.ts derives GET_UP_REQUIRED_MAX from the manifest with this formula: + # three knockdowns on a head beaten to the cap. A different engine formula must change both. + engine = make_engine() + two = engine.fighter("two") + two.knockdowns = 3 + two.trauma.head = 1400 + knockdown = _manifest()["knockdown"] + assert engine._get_up_required(two) == ( + knockdown["get_up_base"] + + 3 * knockdown["get_up_per_knockdown"] + + 1400 // knockdown["get_up_trauma_divisor"] + ) + + +def test_a_clean_hit_through_a_guard_too_worn_to_block_lands_and_can_flash() -> None: + engine = make_engine(seed=11, flash=True) + one, two = engine.fighter("one"), engine.fighter("two") + two.guard = GUARD_BLOCK_MINIMUM - 1 + two.trauma.head = 300 + one.counter_ticks = 30 + engine.step({"one": command(1, action=punch(PunchClass.HOOK, power=Power.POWER))}) + attack = one.attack + assert attack is not None + while attack.age < attack.rule.startup - 2: + two.guard = GUARD_BLOCK_MINIMUM - 1 + engine.step() + kinds: list[str] = [] + two.guard = GUARD_BLOCK_MINIMUM - 1 + snapshot = engine.step({"two": command(1, defense=DefensivePose.GUARD_HIGH)}) + kinds.extend(event.kind for event in snapshot.events) + while not attack.resolved: + two.guard = GUARD_BLOCK_MINIMUM - 1 + kinds.extend(event.kind for event in engine.step().events) + assert "block" not in kinds and "perfect_block" not in kinds + assert "hit" in kinds or "counter_hit" in kinds + assert attack.landed is True + assert "flash_roll" in kinds + + +def test_checksum_covers_every_number_a_fighter_carries() -> None: + base = make_engine(seed=303).snapshot().checksum + for state_field in dataclasses.fields(FighterState): + changed = make_engine(seed=303) + fighter = changed.fighter("one") + value = getattr(fighter, state_field.name) + # An expiry with nothing pending decides nothing, so the checksum leaves it out on purpose. + if state_field.name == "pending_action_expires_tick": + continue + if isinstance(value, bool) or not isinstance(value, int): + continue + setattr(fighter, state_field.name, value + 1) + assert changed.snapshot().checksum != base, state_field.name + + +def test_checksum_covers_every_choice_a_fighter_carries() -> None: + base = make_engine(seed=303).snapshot().checksum + checked = [] + for state_field in dataclasses.fields(FighterState): + changed = make_engine(seed=303) + fighter = changed.fighter("one") + value = getattr(fighter, state_field.name) + if isinstance(value, bool): + setattr(fighter, state_field.name, not value) + elif isinstance(value, Enum): + other = next(member for member in type(value) if member is not value) + setattr(fighter, state_field.name, other) + else: + continue + checked.append(state_field.name) + assert changed.snapshot().checksum != base, state_field.name + assert {"stance", "style", "defense"} <= set(checked) + + +def test_engines_that_differ_only_in_the_styles_never_share_a_checksum() -> None: + def checksums(styles: tuple[FighterStyle, FighterStyle]) -> list[str]: + engine = BoxingEngine( + match_id="styles", + activity_instance_id="instance-1", + guild_id="guild-1", + player_one_id="one", + player_two_id="two", + seed=5, + styles=styles, + ) + return [engine.step().checksum for _ in range(20)] + + balanced = checksums((FighterStyle.BALANCED, FighterStyle.BALANCED)) + styled = checksums((FighterStyle.COUNTER_PUNCHER, FighterStyle.SLUGGER)) + assert all(one != two for one, two in zip(balanced, styled, strict=True)) + + +def test_a_bout_ended_on_the_punch_leaves_no_poise_below_zero() -> None: + engine = make_engine(seed=7, flash=True) + engine.fighter("two").poise = 40 + engine.fighter("two").trauma.head = 300 + engine.fighter("one").counter_ticks = 30 + engine._flash_chance = lambda *_args: 10_000 # type: ignore[method-assign] + snapshot = engine.step({"one": command(1, action=punch(PunchClass.HOOK, power=Power.POWER))}) + while engine.result is None: + snapshot = engine.step() + assert engine.result.finish_method is FinishMethod.FLASH_KO + assert all(fighter.poise >= 0 for fighter in snapshot.fighters) + + +def test_bleeding_never_leaves_more_stamina_than_the_fighter_can_hold() -> None: + engine = make_engine(seed=3, doctor_cut_threshold=10_000, doctor_swelling_threshold=10_000) + engine.fighter("one").x, engine.fighter("two").x = -300, 300 + engine.fighter("two").trauma.bleeding = 1000 + engine.fighter("two").conditioning = 900 + for _ in range(300): + for fighter in engine.step().fighters: + assert fighter.stamina <= fighter.maximum_stamina + + +def test_a_stunned_fighter_stumbles_at_a_share_of_his_footwork() -> None: + engine = make_engine(seed=13) + one = engine.fighter("one") + engine.fighter("two").x = 400 + one.stunned_ticks = 30 + engine.step({"one": command(1, move_x=-1000)}) + for _ in range(8): + engine.step() + speed = hypot(one.velocity_x, one.velocity_y) + assert 0 < speed <= max(2, 7 * STUNNED_SPEED_PERCENT // 100) + one.stunned_ticks = 0 + for _ in range(8): + engine.step() + assert hypot(one.velocity_x, one.velocity_y) == 7 + + +def test_a_stun_ends_the_weave_it_lands_on_so_he_stumbles_instead_of_standing_frozen() -> None: + engine = make_engine(seed=13) + two = engine.fighter("two") + # A weave does not take a straight: it lands mid-weave, walking away. + engine.step( + { + "one": command(1, action=punch(PunchClass.STRAIGHT)), + "two": command(1, action=MovementAction(ActionKind.WEAVE), move_x=1000), + } + ) + assert two.evasion_ticks > 0 + advance_until(engine, {"hit"}) + stunned = two.stunned_ticks + assert stunned > 0 and two.evasion_ticks > 0 engine.step() - turned = math.degrees(math.atan2(one.facing_y, one.facing_x)) - single_blend = math.degrees(math.atan2(350, 650)) - assert turned > single_blend + 15 - assert 46 < turned < 52 + assert (two.evasion_ticks, two.defense) == (0, DefensivePose.NONE) + start = two.x + while two.stunned_ticks > 0: + engine.step() + assert two.x - start >= (stunned - 2) * 2 + engine.step() + assert two.defense is DefensivePose.NONE and two.evasion_ticks == 0 + + +def test_a_fighter_folding_over_a_body_shot_stands_frozen_until_he_drops() -> None: + engine = make_engine(seed=7) + two = engine.fighter("two") + two.trauma.body = 800 + two.stamina = 300 + engine.step( + { + "one": command( + 1, + action=punch( + PunchClass.HOOK, hand=Hand.LEFT, target=Target.BODY, power=Power.POWER + ), + ) + } + ) + collapsed_at = None + for _ in range(30): + if any(event.kind == "body_collapse" for event in engine.step().events): + collapsed_at = (two.x, two.y) + break + assert collapsed_at is not None + sequence = 1 + while engine.phase is MatchPhase.FIGHT: + engine.step({"two": command(sequence, move_x=1000)}) + sequence += 1 + assert engine.phase is MatchPhase.KNOCKDOWN + assert (two.x, two.y) == collapsed_at + + +@pytest.mark.parametrize(("ticks_before", "first_down"), [(0, "two"), (1, "one")]) +def test_two_body_collapses_on_one_tick_favour_neither_seat( + ticks_before: int, first_down: str +) -> None: + engine = make_engine(seed=17) + for _ in range(ticks_before): + engine.step() + for fighter in (engine.fighter("one"), engine.fighter("two")): + fighter.body_collapse_ticks = 1 + fighter.body_collapse_action_id = "trade" + knockdowns = [event for event in engine.step().events if event.kind == "knockdown"] + assert [event.target_id for event in knockdowns] == [first_down] + + +def _down_two(engine: BoxingEngine) -> None: + engine.fighter("two").poise = 1 + engine.step({"one": command(1, action=punch(PunchClass.UPPERCUT, power=Power.POWER))}) + advance_until(engine, {"knockdown"}) + assert engine.phase is MatchPhase.KNOCKDOWN + + +def _rise_at(engine: BoxingEngine, count: int) -> list[CombatEvent]: + """Let the referee reach `count`, then fill the get-up meter: two rises on the next tick.""" + while engine._knockdown_count_ticks < count * COUNT_TICK_INTERVAL: + engine.step() + two = engine.fighter("two") + two.get_up_meter = engine._get_up_required(two) + return list(engine.step().events) + + +def test_a_fighter_who_beats_the_count_takes_the_mandatory_eight_before_they_box() -> None: + engine = make_engine(round_ticks=2000) + _down_two(engine) + clock = engine._paused_fight_ticks + two = engine.fighter("two") + risen_at = (two.x, two.y) + events = _rise_at(engine, 2) + assert [event.amount for event in events if event.kind == "get_up"] == [2] + + counts: list[int] = [] + shown: list[int] = [] + sequence = 1 + box_tick = engine.tick - engine._knockdown_count_ticks + MANDATORY_COUNT * COUNT_TICK_INTERVAL + box_tick += BOX_PAUSE_TICKS + while engine.phase is MatchPhase.KNOCKDOWN: + view = next(item for item in engine.snapshot().fighters if item.player_id == "two") + assert not view.is_downed and view.get_up_prompt is None + assert engine.phase_ticks_remaining == box_tick - engine.tick + shown.append(view.get_up_count) + # Nothing he presses counts until the referee waves them on. + snapshot = engine.step({"two": command(sequence, action=punch(), move_x=1000)}) + sequence += 1 + counts += [event.amount for event in snapshot.events if event.kind == "count"] + assert (two.x, two.y) == risen_at + assert engine.tick == box_tick + assert counts == [3, 4, 5, 6, 7, 8] + assert shown == sorted(shown) and shown[0] == 2 and shown[-1] == MANDATORY_COUNT + assert [event.target_id for event in engine.events if event.kind == "box"] == ["two"] + assert engine.phase is MatchPhase.FIGHT + assert engine.phase_ticks_remaining == clock + assert two.stunned_ticks == GET_UP_STUN_TICKS + assert {view.get_up_count for view in engine.snapshot().fighters} == {MANDATORY_COUNT} + + +def test_a_late_get_up_still_gets_the_referee_s_look_before_the_box() -> None: + engine = make_engine(round_ticks=2000) + _down_two(engine) + events = _rise_at(engine, 9) + assert [event.amount for event in events if event.kind == "get_up"] == [9] + for _ in range(BOX_PAUSE_TICKS - 1): + snapshot = engine.step() + assert engine.phase is MatchPhase.KNOCKDOWN and engine.result is None + assert not [event for event in snapshot.events if event.kind in {"count", "box"}] + assert {view.get_up_count for view in snapshot.fighters} == {9} + assert [event.kind for event in engine.step().events if event.kind == "box"] == ["box"] + assert engine.phase is MatchPhase.FIGHT + # The count the referee reached stays on the snapshot, inside what the client accepts. + for _ in range(3 * COUNT_TICK_INTERVAL): + assert {view.get_up_count for view in engine.step().fighters} == {9} + + +def test_a_meter_filled_as_the_count_reaches_ten_is_too_late() -> None: + engine = make_engine(round_ticks=2000) + _down_two(engine) + two = engine.fighter("two") + while engine._knockdown_count_ticks < 10 * COUNT_TICK_INTERVAL - 1: + engine.step() + two.get_up_meter = engine._get_up_required(two) + events = engine.step().events + assert "get_up" not in [event.kind for event in events] + assert engine.result is not None + assert engine.result.finish_method is FinishMethod.KO + assert engine.phase is MatchPhase.COMPLETE + + # A tick earlier, at nine and a bit, he still beats it. + engine = make_engine(round_ticks=2000) + _down_two(engine) + while engine._knockdown_count_ticks < 10 * COUNT_TICK_INTERVAL - 2: + engine.step() + engine.fighter("two").get_up_meter = engine._get_up_required(engine.fighter("two")) + assert [event.amount for event in engine.step().events if event.kind == "get_up"] == [9] + assert engine.result is None + + +def test_round_one_opens_with_the_introductions_and_later_rounds_with_the_bell() -> None: + opening = _manifest()["countdown"]["opening_ticks"] + assert opening >= 8 * COUNT_TICK_INTERVAL + config = dataclasses.replace(EngineConfig(), rounds=2, round_ticks=30, rest_ticks=10) + assert config.countdown_ticks == opening + engine = BoxingEngine( + match_id="intro", + activity_instance_id="instance-1", + guild_id="guild-1", + player_one_id="one", + player_two_id="two", + seed=3, + config=config, + ) + phases = [] + while engine.result is None: + phases.append(engine.phase) + engine.step() + assert phases[:opening] == [MatchPhase.COUNTDOWN] * opening + assert MatchPhase.COUNTDOWN not in phases[opening:] + assert [event.detail for event in engine.events if event.kind == "bell"] == [ + "round_start", + "round_end", + "round_start", + "round_end", + ] + + +def test_the_referee_sends_both_fighters_back_after_a_foul() -> None: + engine = make_engine(round_ticks=2000) + one, two = engine.fighter("one"), engine.fighter("two") + apart = hypot(two.x - one.x, two.y - one.y) + engine.step({"one": command(1, action=FoulAction(Foul.LOW_BLOW))}) + assert engine.phase is MatchPhase.FOUL_RECOVERY + assert hypot(two.x - one.x, two.y - one.y) >= apart + 2 * FOUL_SEPARATION - 2 + assert (one.velocity_x, one.velocity_y, two.velocity_x, two.velocity_y) == (0, 0, 0, 0) + while engine.phase is MatchPhase.FOUL_RECOVERY: + engine.step() + # The two seconds were the fouled man's recovery: both box on clear-headed. + assert (one.stunned_ticks, two.stunned_ticks) == (0, 0) + # A second foul straight after the restart is thrown from too far to land. + events = engine.step({"one": command(2, action=FoulAction(Foul.HEADBUTT))}).events + assert [event.kind for event in events if event.kind.startswith("foul")] == ["foul_miss"] + + +def test_the_fouler_gets_no_free_shot_at_the_restart() -> None: + engine = make_engine(round_ticks=2000) + one, two = engine.fighter("one"), engine.fighter("two") + # Fouled in the middle of his own jab, which the referee's break ends. + engine.step({"two": command(1, action=punch(PunchClass.JAB, hand=Hand.LEFT))}) + engine.step({"one": command(1, action=FoulAction(Foul.LOW_BLOW))}) + assert engine.phase is MatchPhase.FOUL_RECOVERY + assert two.attack is None + engine.step({"two": command(2, defense=DefensivePose.GUARD_HIGH)}) + while engine.phase is MatchPhase.FOUL_RECOVERY: + engine.step() + sequence = 2 + # The fouler walks straight in and throws a power straight at the man he fouled. + while hypot(two.x - one.x, two.y - one.y) > 150: + engine.step({"one": command(sequence, move_x=1000)}) + sequence += 1 + engine.step({"one": command(sequence, action=punch(PunchClass.STRAIGHT, power=Power.POWER))}) + assert advance_until(engine, {"hit", "block", "perfect_block", "whiff"}) == "block" + assert two.stunned_ticks == 0 + + +def _straight_from(stamina: int) -> tuple[BoxingEngine, int, list[CombatEvent]]: + engine = make_engine(round_ticks=2000) + one = engine.fighter("one") + one.stamina = stamina + events = list(engine.step({"one": command(1, action=punch(PunchClass.STRAIGHT))}).events) + left = one.stamina + attack = one.attack + assert attack is not None + while not attack.resolved: + events.extend(engine.step().events) + return engine, left, events + + +def test_a_fighter_out_of_breath_still_throws_a_slow_weak_arm_punch() -> None: + cost = PUNCH_RULES[(PunchClass.STRAIGHT, Target.HEAD, Power.NORMAL)].stamina_cost + fresh, fresh_left, fresh_events = _straight_from(cost) + tired, tired_left, tired_events = _straight_from(cost - 1) + assert "exhausted" not in [event.kind for event in fresh_events] + assert "exhausted" in [event.kind for event in tired_events] + # He pays with what breath he has left. + assert tired_left == fresh_left + fresh_attack, tired_attack = fresh.fighter("one").attack, tired.fighter("one").attack + assert fresh_attack is not None and tired_attack is not None + assert tired_attack.rule.startup == fresh_attack.rule.startup + TIRED_STARTUP_TICKS + assert tired_attack.rule.recovery == fresh_attack.rule.recovery + TIRED_RECOVERY_TICKS + assert tired_attack.rule.impact == fresh_attack.rule.impact * TIRED_IMPACT_PERCENT // 100 + assert tired_attack.landed and fresh_attack.landed + fresh_two, tired_two = fresh.fighter("two"), tired.fighter("two") + assert 0 < tired_two.trauma.head < fresh_two.trauma.head + assert fresh_two.poise < tired_two.poise < 600 + + +def _hook_after_a_jab(stamina: int) -> tuple[AttackState, int]: + engine = make_engine(round_ticks=2000) + one = engine.fighter("one") + engine.step({"one": command(1, action=punch(PunchClass.JAB, hand=Hand.LEFT))}) + jab = one.attack + assert jab is not None + while not jab.resolved: + engine.step() + assert jab.landed + one.stamina = stamina + hook = punch(PunchClass.HOOK, hand=Hand.LEFT) + engine.step({"one": command(2, action=hook)}) + for _ in range(40): + if one.attack is not None and one.attack.action == hook: + return one.attack, one.attack.start_tick - jab.start_tick + engine.step() + raise AssertionError("the hook was never thrown") + + +def test_a_tired_punch_is_no_combination() -> None: + fresh, fresh_gap = _hook_after_a_jab(1000) + tired, tired_gap = _hook_after_a_jab(10) + assert fresh.combo_bonus > 0 and tired.combo_bonus == 0 + # It cannot cut the jab's recovery short either: it waits for the jab to finish. + assert tired_gap > fresh_gap + + +def _counter_hook(stamina: int) -> list[str]: + engine = make_engine(seed=11, flash=True) + one, two = engine.fighter("one"), engine.fighter("two") + two.trauma.head = 300 + one.counter_ticks = 30 + one.stamina = stamina + kinds = [ + event.kind + for event in engine.step( + {"one": command(1, action=punch(PunchClass.HOOK, power=Power.POWER))} + ).events + ] + attack = one.attack + assert attack is not None + # His breath back by the time it lands, so only the punch itself decides. + one.stamina = 1000 + while not attack.resolved: + kinds.extend(event.kind for event in engine.step().events) + assert attack.landed + return kinds + + +def test_a_tired_punch_never_carries_a_flash_knockout() -> None: + assert "flash_roll" in _counter_hook(1000) + tired = _counter_hook(10) + assert "exhausted" in tired and "flash_roll" not in tired diff --git a/tests/test_hands/test_integration.py b/tests/test_hands/test_integration.py index 724cb733..0cbdf854 100644 --- a/tests/test_hands/test_integration.py +++ b/tests/test_hands/test_integration.py @@ -10,7 +10,7 @@ from intelstream.database.repository import Repository from intelstream.hands.auth import HandsAuth from intelstream.hands.engine import EngineConfig -from intelstream.hands.protocol import encode_client_input +from intelstream.hands.protocol import PROTOCOL_VERSION, encode_client_input from intelstream.hands.rooms import HandsRoomManager, RoomConfig from intelstream.hands.server import HandsServer from intelstream.hands.types import Hand, InputCommand, PunchAction, PunchClass, Target @@ -91,6 +91,7 @@ async def test_real_auth_spectator_reconnect_and_authoritative_elo(tmp_path) -> rooms = HandsRoomManager( repository, config=RoomConfig( + style_select_seconds=0.0, tick_interval_seconds=0.001, broadcast_every_ticks=1, reconnect_grace_seconds=0.2, @@ -161,7 +162,7 @@ async def test_real_auth_spectator_reconnect_and_authoritative_elo(tmp_path) -> two = await client.ws_connect(f"{base}/api/hands/ws", headers=headers) await one.send_json( { - "version": 3, + "version": PROTOCOL_VERSION, "type": "authenticate", "ticket": credentials["one"]["ticket"], } @@ -169,7 +170,7 @@ async def test_real_auth_spectator_reconnect_and_authoritative_elo(tmp_path) -> welcome_one = await wait_for_type(one, "welcome") await two.send_json( { - "version": 3, + "version": PROTOCOL_VERSION, "type": "authenticate", "ticket": credentials["two"]["ticket"], } @@ -180,7 +181,7 @@ async def test_real_auth_spectator_reconnect_and_authoritative_elo(tmp_path) -> spectator = await client.ws_connect(f"{base}/api/hands/ws", headers=headers) await spectator.send_json( { - "version": 3, + "version": PROTOCOL_VERSION, "type": "authenticate", "ticket": credentials["three"]["ticket"], } @@ -198,7 +199,7 @@ async def test_real_auth_spectator_reconnect_and_authoritative_elo(tmp_path) -> replacement = await client.ws_connect(f"{base}/api/hands/ws", headers=headers) await replacement.send_json( { - "version": 3, + "version": PROTOCOL_VERSION, "type": "authenticate", "ticket": welcome_one["reconnect_ticket"], } diff --git a/tests/test_hands/test_parity_fixtures.py b/tests/test_hands/test_parity_fixtures.py new file mode 100644 index 00000000..df9cad48 --- /dev/null +++ b/tests/test_hands/test_parity_fixtures.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from scripts.hands_parity_fixtures import documents + + +def test_the_tables_the_client_predicts_from_still_match_the_engine() -> None: + """The client's prediction tests check it against these files; a rule change that alters them + must regenerate them (scripts/hands_parity_fixtures.py), so the client is checked against it.""" + stale = [ + path.name + for path, text in documents().items() + if not path.exists() or path.read_text(encoding="utf-8") != text + ] + assert not stale, f"run scripts/hands_parity_fixtures.py to regenerate {stale}" diff --git a/tests/test_hands/test_protocol.py b/tests/test_hands/test_protocol.py index 308b480b..d0a757bb 100644 --- a/tests/test_hands/test_protocol.py +++ b/tests/test_hands/test_protocol.py @@ -2,22 +2,29 @@ import pytest +from intelstream.hands.cpu import CpuLevel from intelstream.hands.engine import BoxingEngine, EngineConfig from intelstream.hands.protocol import ( MAX_ACTIONS_PER_INPUT, MAX_FRAME_BYTES, MAX_TICK_LAG, MAX_TICK_LEAD, + PROTOCOL_VERSION, ProtocolError, + StyleChoice, + decode_client_frame, encode_client_input, encode_snapshot, parse_client_input, + parse_cpu_request, + parse_style_choice, parse_ticket_ack, snapshot_for_viewer, ) from intelstream.hands.types import ( ActionKind, DefensivePose, + FighterStyle, Foul, FoulAction, Hand, @@ -57,9 +64,14 @@ def test_round_trip_every_semantic_action() -> None: assert parse_client_input(encode_client_input(command), server_tick=20) == command +def current_frame(raw: str) -> str: + """A raw frame written by hand (for what json.dumps cannot write) in the current version.""" + return raw.replace('"version":V,', f'"version":{PROTOCOL_VERSION},', 1) + + def valid_payload() -> dict[str, object]: return { - "version": 3, + "version": PROTOCOL_VERSION, "type": "input", "sequence": 2, "client_tick": 10, @@ -121,8 +133,8 @@ def test_rejects_unknown_nested_fields_and_malformed_actions() -> None: def test_rejects_duplicate_fields() -> None: - frame = ( - '{"version":3,"type":"input","sequence":1,"sequence":2,' + frame = current_frame( + '{"version":V,"type":"input","sequence":1,"sequence":2,' '"client_tick":1,"move":{"x":0,"y":0},"defense":"none","actions":[]}' ) @@ -174,18 +186,72 @@ def test_rejects_malformed_frames(frame: str | bytes) -> None: def test_ticket_ack_is_strict_and_distinct_from_semantic_input() -> None: - frame = '{"version":3,"type":"ticket_ack","refresh_id":"refresh-identifier"}' + frame = current_frame('{"version":V,"type":"ticket_ack","refresh_id":"refresh-identifier"}') assert parse_ticket_ack(frame) == "refresh-identifier" assert parse_ticket_ack(json.dumps(valid_payload())) is None for malformed in ( - '{"version":3,"type":"ticket_ack","refresh_id":"short"}', - '{"version":3,"type":"ticket_ack","refresh_id":"refresh-identifier","extra":1}', - '{"version":3,"type":"ticket_ack","refresh_id":"one","refresh_id":"two"}', + current_frame('{"version":V,"type":"ticket_ack","refresh_id":"short"}'), + current_frame( + '{"version":V,"type":"ticket_ack","refresh_id":"refresh-identifier","extra":1}' + ), + current_frame('{"version":V,"type":"ticket_ack","refresh_id":"one","refresh_id":"two"}'), ): with pytest.raises(ProtocolError): parse_ticket_ack(malformed) +def test_computer_request_is_strict_and_distinct_from_other_frames() -> None: + for level in CpuLevel: + frame = json.dumps({"version": PROTOCOL_VERSION, "type": "cpu", "level": level.value}) + assert parse_cpu_request(frame) is level + assert parse_cpu_request(json.dumps(valid_payload())) is None + assert ( + parse_cpu_request( + current_frame('{"version":V,"type":"ticket_ack","refresh_id":"refresh-id"}') + ) + is None + ) + for malformed in ( + current_frame('{"version":V,"type":"cpu","level":"legend"}'), + current_frame('{"version":V,"type":"cpu"}'), + current_frame('{"version":V,"type":"cpu","level":1}'), + current_frame('{"version":V,"type":"cpu","level":"rookie","extra":1}'), + '{"version":2,"type":"cpu","level":"rookie"}', + current_frame('{"version":V,"type":"cpu","level":"rookie","level":"champion"}'), + "not json", + "x" * (MAX_FRAME_BYTES + 1), + ): + with pytest.raises(ProtocolError): + parse_cpu_request(malformed) + + +def test_style_choice_is_strict_and_distinct_from_other_frames() -> None: + for style in FighterStyle: + for ready in (True, False): + frame = json.dumps( + {"version": PROTOCOL_VERSION, "type": "style", "style": style.value, "ready": ready} + ) + assert parse_style_choice(frame) == StyleChoice(style, ready) + assert parse_style_choice(json.dumps(valid_payload())) is None + assert parse_style_choice(current_frame('{"version":V,"type":"cpu","level":"rookie"}')) is None + for malformed in ( + current_frame('{"version":V,"type":"style","style":"brawler","ready":true}'), + current_frame('{"version":V,"type":"style","style":"boxer"}'), + current_frame('{"version":V,"type":"style","style":"boxer","ready":1}'), + current_frame('{"version":V,"type":"style","style":"boxer","ready":"yes"}'), + current_frame('{"version":V,"type":"style","style":"boxer","ready":true,"extra":1}'), + '{"version":2,"type":"style","style":"boxer","ready":true}', + current_frame( + '{"version":V,"type":"style","style":"boxer","style":"slugger","ready":true}' + ), + current_frame('{"version":V,"type":"style","style":5,"ready":true}'), + "not json", + "x" * (MAX_FRAME_BYTES + 1), + ): + with pytest.raises(ProtocolError): + parse_style_choice(malformed) + + def test_snapshot_encoding_is_required_and_redacted_per_viewer() -> None: engine = BoxingEngine( match_id="match", @@ -290,3 +356,54 @@ def test_client_action_id_validated_and_round_trips() -> None: ] with pytest.raises(ProtocolError, match="action id"): parse_client_input(json.dumps(payload), server_tick=10) + + +def test_corner_instructions_and_choice_travel_on_the_wire() -> None: + payload = valid_payload() + payload["actions"] = [{"kind": "corner_cut"}, {"kind": "corner_swelling", "id": "rest-1"}] + command = parse_client_input(json.dumps(payload)) + assert command.actions == ( + MovementAction(ActionKind.CORNER_CUT), + MovementAction(ActionKind.CORNER_SWELLING, client_action_id="rest-1"), + ) + + engine = BoxingEngine( + match_id="match-1", + activity_instance_id="instance-1", + guild_id="guild-1", + player_one_id="one", + player_two_id="two", + seed=3, + config=EngineConfig(round_ticks=5, rounds=2, rest_ticks=30, countdown_ticks=0), + ) + fresh = json.loads(encode_snapshot(engine.snapshot(), viewer_id=None))["payload"] + assert [fighter["corner_choice"] for fighter in fresh["fighters"]] == [None, None] + while engine.phase.value != "rest": + engine.step() + engine.step({"two": InputCommand(1, 0, actions=(MovementAction(ActionKind.CORNER_BREATH),))}) + resting = json.loads(encode_snapshot(engine.snapshot(), viewer_id="one"))["payload"] + assert [fighter["corner_choice"] for fighter in resting["fighters"]] == [None, "breath"] + + +def test_a_frame_is_decoded_once_and_then_read_as_whichever_message_it_is() -> None: + request = decode_client_frame( + json.dumps({"version": PROTOCOL_VERSION, "type": "cpu", "level": "rookie"}) + ) + assert parse_ticket_ack(request) is None + assert parse_cpu_request(request) is CpuLevel.ROOKIE + assert parse_style_choice(request) is None + pick = decode_client_frame( + json.dumps( + {"version": PROTOCOL_VERSION, "type": "style", "style": "slugger", "ready": True} + ) + ) + assert parse_cpu_request(pick) is None + assert parse_style_choice(pick) == StyleChoice(FighterStyle.SLUGGER, True) + frame = encode_client_input(InputCommand(sequence=4, client_tick=0, move_x=500)) + envelope = decode_client_frame(frame) + assert parse_style_choice(envelope) is None + command = parse_client_input(envelope, last_sequence=3) + assert command.sequence == 4 and command.move_x == 500 + for bad in ("x" * (MAX_FRAME_BYTES + 1), "[1, 2]", "{", b"\xff\xfe"): + with pytest.raises(ProtocolError): + decode_client_frame(bad) diff --git a/tests/test_hands/test_rooms.py b/tests/test_hands/test_rooms.py index 68b43f1b..bec7b134 100644 --- a/tests/test_hands/test_rooms.py +++ b/tests/test_hands/test_rooms.py @@ -2,7 +2,7 @@ import asyncio import json -from dataclasses import dataclass, field, replace +from dataclasses import asdict, dataclass, field, replace from unittest.mock import AsyncMock, MagicMock import pytest @@ -10,9 +10,11 @@ from intelstream.database.repository import Repository from intelstream.hands import rooms as rooms_module from intelstream.hands.auth import AuthenticatedPlayer -from intelstream.hands.engine import EngineConfig -from intelstream.hands.protocol import encode_client_input +from intelstream.hands.cpu import CPU_STYLES, PROFILES, CpuBrain, CpuLevel, cpu_style +from intelstream.hands.engine import BoxingEngine, EngineConfig, PunchCount +from intelstream.hands.protocol import PROTOCOL_VERSION, StyleChoice, encode_client_input from intelstream.hands.rooms import ( + CPU_RECORDS, SNAPSHOT_BACKLOG_BYTES, HandsRoom, HandsRoomManager, @@ -25,6 +27,7 @@ CombatEvent, DefensivePose, EngineSnapshot, + FighterStyle, Hand, InputCommand, MovementAction, @@ -135,8 +138,12 @@ def room_config( close_timeout: float = 1.0, max_catch_up_ticks: int = 2, max_spectators: int = 20, + style_select: float = 0.0, + rematch_seat: float = 60.0, ) -> RoomConfig: return RoomConfig( + style_select_seconds=style_select, + rematch_seat_seconds=rematch_seat, tick_interval_seconds=tick_interval, broadcast_every_ticks=1, reconnect_grace_seconds=reconnect_grace, @@ -158,6 +165,46 @@ def room_config( ) +def test_room_config_rejects_room_caps_that_could_never_open_a_room() -> None: + assert RoomConfig(max_cpu_rooms=0).max_cpu_rooms == 0 + for bounds in ({"max_rooms": 0}, {"max_cpu_rooms": -1}): + with pytest.raises(ValueError, match="room bounds"): + RoomConfig(**bounds) + + +async def test_rooms_past_the_cap_are_refused_as_busy_until_one_closes( + repository: Repository, +) -> None: + manager = HandsRoomManager(repository, config=replace(room_config(), max_rooms=2)) + first = await manager.join(player("one", "instance-1"), FakeSocket()) + await manager.join(player("two", "instance-2"), FakeSocket()) + with pytest.raises(RoomError, match="service_busy"): + await manager.join(player("three", "instance-3"), FakeSocket()) + # A room that already exists still takes its second fighter. + await manager.join(player("four", "instance-1"), FakeSocket()) + await wait_until(lambda: manager.room_count == 1) + assert first.room.finished + await manager.join(player("three", "instance-3"), FakeSocket()) + assert manager.room_count == 2 + await manager.close() + + +async def test_computer_bouts_past_the_cap_are_refused_as_busy(repository: Repository) -> None: + manager = HandsRoomManager( + repository, config=replace(room_config(round_ticks=100_000), max_cpu_rooms=1) + ) + first = await manager.join(player("one", "instance-1"), FakeSocket()) + second = await manager.join(player("two", "instance-2"), FakeSocket()) + assert await first.room.request_cpu("one", first.connection, CpuLevel.ROOKIE) + with pytest.raises(RoomError, match="service_busy"): + await second.room.request_cpu("two", second.connection, CpuLevel.ROOKIE) + assert second.room.cpu is None + # Two people still box each other: the cap is on bouts one person can start alone. + await manager.join(player("three", "instance-2"), FakeSocket()) + assert second.room.engine is not None + await manager.close() + + def test_room_config_allows_no_spectators_but_rejects_negative_bound() -> None: assert RoomConfig(max_spectators=0).max_spectators == 0 with pytest.raises(ValueError, match="spectator bound"): @@ -861,7 +908,7 @@ async def test_ticket_refresh_queue_coalesces_and_rejects_replaced_connection( "reconnect_ticket": "ticket-99", "refresh_id": "refresh-id-000099", "type": "ticket", - "version": 3, + "version": PROTOCOL_VERSION, } ] @@ -1064,7 +1111,7 @@ async def controlled_sleep(_delay: float) -> None: "grace_ms": 22_750, "player_id": "two", "type": "paused", - "version": 3, + "version": PROTOCOL_VERSION, } two_socket = FakeSocket() @@ -1181,7 +1228,9 @@ async def test_persistence_failure_errors_closes_and_unregisters(repository: Rep assert one.closed and two.closed for socket in (one, two): errors = [json.loads(message) for message in socket.messages if '"type":"error"' in message] - assert errors == [{"code": "persistence_failed", "type": "error", "version": 3}] + assert errors == [ + {"code": "persistence_failed", "type": "error", "version": PROTOCOL_VERSION} + ] assert all("database detail" not in message for message in socket.messages) await manager.close() @@ -1222,7 +1271,7 @@ def broken_step(*_args: object) -> None: assert socket.closed assert socket.close_code == 1011 errors = [json.loads(message) for message in socket.messages if '"type":"error"' in message] - assert errors == [{"code": "internal_error", "type": "error", "version": 3}] + assert errors == [{"code": "internal_error", "type": "error", "version": PROTOCOL_VERSION}] safe_logger.exception.assert_called_once() assert await repository.get_hands_match("match-engine-failure") is None for user_id in ("one", "two"): @@ -1271,10 +1320,10 @@ async def test_shutdown_after_result_shields_persistence_from_cancelled_waiter( entered = asyncio.Event() release = asyncio.Event() - async def delayed_record(result): + async def delayed_record(result, **details): entered.set() await release.wait() - return await original_record(result) + return await original_record(result, **details) repository.record_hands_match = delayed_record manager = HandsRoomManager( @@ -1387,6 +1436,27 @@ async def test_both_disconnect_abandons_without_match_or_elo_change( await manager.close() +async def test_the_ready_message_carries_each_fighters_record(repository: Repository) -> None: + original_get = repository.get_or_create_hands_rating + + async def with_record(guild_id: str, user_id: str): + rating = await original_get(guild_id, user_id) + if user_id == "one": + rating.wins, rating.losses, rating.draws, rating.knockouts = 12, 3, 1, 8 + return rating + + repository.get_or_create_hands_rating = with_record + manager = HandsRoomManager(repository, config=room_config(round_ticks=1000)) + first_socket = FakeSocket() + await manager.join(player("one"), first_socket) + await manager.join(player("two"), FakeSocket()) + await wait_until(lambda: "ready" in message_types(first_socket)) + players = {entry["id"]: entry for entry in payloads(first_socket, "ready")[0]["players"]} + assert players["one"]["record"] == {"wins": 12, "losses": 3, "draws": 1, "knockouts": 8} + assert players["two"]["record"] == {"wins": 0, "losses": 0, "draws": 0, "knockouts": 0} + await manager.close() + + async def test_slow_rating_lookup_does_not_block_independent_join( repository: Repository, ) -> None: @@ -1644,10 +1714,10 @@ async def test_reconnect_during_result_persistence_gets_snapshot_then_exact_fina persistence_entered = asyncio.Event() persistence_release = asyncio.Event() - async def delayed_record(result): + async def delayed_record(result, **details): persistence_entered.set() await persistence_release.wait() - return await original_record(result) + return await original_record(result, **details) repository.record_hands_match = delayed_record manager = HandsRoomManager( @@ -1761,6 +1831,172 @@ async def test_close_is_idempotent_and_leaves_no_hands_tasks(repository: Reposit assert not {name for name in active_names if name.startswith("hands-")} +def payloads(socket: FakeSocket, kind: str) -> list[dict[str, object]]: + return [ + json.loads(message) for message in socket.messages if json.loads(message)["type"] == kind + ] + + +async def test_a_lone_fighter_calls_in_the_computer_for_an_unrated_bout( + repository: Repository, +) -> None: + manager = HandsRoomManager( + repository, + config=room_config(round_ticks=90), + match_id_factory=lambda: "match-cpu", + seed_factory=lambda: 11, + ) + socket = FakeSocket() + one = await manager.join(player("one"), socket) + await wait_until(lambda: "waiting" in message_types(socket)) + + assert await one.room.request_cpu("one", one.connection, CpuLevel.CONTENDER) + engine = one.room.engine + assert engine is not None + assert engine.players == ("one", "cpu:contender") + await wait_until(lambda: "ready" in message_types(socket)) + ready = payloads(socket, "ready")[0] + assert ready["players"][1] == { + "id": "cpu:contender", + "name": PROFILES[CpuLevel.CONTENDER].name, + "avatar": None, + "rating": PROFILES[CpuLevel.CONTENDER].rating, + "connected": True, + "cpu": True, + "style": engine.fighter("cpu:contender").style.value, + "record": CPU_RECORDS[CpuLevel.CONTENDER].payload(), + } + assert engine.fighter("cpu:contender").style in CPU_STYLES[CpuLevel.CONTENDER] + assert "cpu" not in ready["players"][0] + await wait_until(lambda: "final" in message_types(socket), deadline_seconds=5) + + final = payloads(socket, "final")[0] + assert final["ratings"] == { + "one": {"before": 1000, "after": 1000}, + "cpu:contender": { + "before": PROFILES[CpuLevel.CONTENDER].rating, + "after": PROFILES[CpuLevel.CONTENDER].rating, + }, + } + assert await repository.get_hands_match("match-cpu") is None + rating = await repository.get_hands_rating("guild-1", "one") + assert rating is not None and rating.bouts == 0 and rating.rating == 1000 + assert await repository.get_hands_rating("guild-1", "cpu:contender") is None + await wait_until(lambda: manager.room_count == 0) + await manager.close() + + +async def test_the_run_loop_plays_the_computer_through_the_engine( + repository: Repository, +) -> None: + manager = HandsRoomManager(repository, config=room_config(round_ticks=600)) + one = await manager.join(player("one"), FakeSocket()) + assert await one.room.request_cpu("one", one.connection, CpuLevel.CHAMPION) + engine = one.room.engine + assert engine is not None + await wait_until(lambda: engine.tick > 40, deadline_seconds=3) + computer = engine.fighter("cpu:champion") + assert computer.last_sequence >= 30 + assert (computer.x, computer.y) != (180, 0) + await manager.close() + + +async def test_the_computer_is_not_called_once_a_second_fighter_is_seated( + repository: Repository, +) -> None: + manager = HandsRoomManager(repository, config=room_config(round_ticks=1000)) + one = await manager.join(player("one"), FakeSocket()) + two = await manager.join(player("two"), FakeSocket()) + assert not await one.room.request_cpu("one", one.connection, CpuLevel.ROOKIE) + assert not await two.room.request_cpu("two", two.connection, CpuLevel.ROOKIE) + assert one.room.cpu is None + assert one.room.engine is not None and one.room.engine.players == ("one", "two") + await manager.close() + + +async def test_the_computer_is_called_once(repository: Repository) -> None: + manager = HandsRoomManager(repository, config=room_config(round_ticks=1000)) + socket = FakeSocket() + one = await manager.join(player("one"), socket) + assert await one.room.request_cpu("one", one.connection, CpuLevel.ROOKIE) + engine = one.room.engine + assert not await one.room.request_cpu("one", one.connection, CpuLevel.CHAMPION) + assert one.room.engine is engine + assert one.room.cpu is not None and one.room.cpu.level is CpuLevel.ROOKIE + await wait_until(lambda: "snapshot" in message_types(socket)) + assert message_types(socket).count("ready") == 1 + await manager.close() + + +async def test_only_the_current_connection_can_call_the_computer( + repository: Repository, +) -> None: + manager = HandsRoomManager(repository, config=room_config(round_ticks=1000)) + first = await manager.join(player("one"), FakeSocket()) + await manager.join(player("one"), FakeSocket()) + with pytest.raises(RoomError, match="connection_replaced"): + await first.room.request_cpu("one", first.connection, CpuLevel.ROOKIE) + assert first.room.engine is None + await manager.close() + + +async def test_a_spectator_joining_a_computer_bout_sees_both_fighters( + repository: Repository, +) -> None: + manager = HandsRoomManager(repository, config=room_config(round_ticks=1000)) + one = await manager.join(player("one"), FakeSocket()) + assert await one.room.request_cpu("one", one.connection, CpuLevel.ROOKIE) + watcher_socket = FakeSocket() + watcher = await manager.join(player("two"), watcher_socket) + assert watcher.role == "spectator" + await wait_until(lambda: "snapshot" in message_types(watcher_socket)) + welcome = payloads(watcher_socket, "welcome")[0] + assert [entry["id"] for entry in welcome["players"]] == ["one", "cpu:rookie"] + await manager.close() + + +async def test_a_computer_bout_is_abandoned_when_its_fighter_never_returns( + repository: Repository, +) -> None: + manager = HandsRoomManager( + repository, + config=room_config(round_ticks=1000, reconnect_grace=0.02), + match_id_factory=lambda: "match-cpu-abandoned", + ) + one = await manager.join(player("one"), FakeSocket()) + assert await one.room.request_cpu("one", one.connection, CpuLevel.CHAMPION) + engine = one.room.engine + assert engine is not None + await manager.leave(one) + tick = engine.tick + await wait_until(lambda: manager.room_count == 0) + assert engine.result is None and engine.tick - tick <= 2 + assert await repository.get_hands_match("match-cpu-abandoned") is None + await manager.close() + + +async def test_a_rematch_against_the_computer_starts_a_fresh_bout( + repository: Repository, +) -> None: + ids = iter(("match-cpu-1", "match-cpu-2")) + manager = HandsRoomManager( + repository, config=room_config(round_ticks=30), match_id_factory=lambda: next(ids) + ) + first_socket = FakeSocket() + first = await manager.join(player("one"), first_socket) + assert await first.room.request_cpu("one", first.connection, CpuLevel.ROOKIE) + await wait_until(lambda: "final" in message_types(first_socket), deadline_seconds=5) + await wait_until(lambda: manager.room_count == 0) + + second_socket = FakeSocket() + second = await manager.join(player("one"), second_socket) + assert second.room is not first.room + await wait_until(lambda: "waiting" in message_types(second_socket)) + assert await second.room.request_cpu("one", second.connection, CpuLevel.ROOKIE) + await wait_until(lambda: "final" in message_types(second_socket), deadline_seconds=5) + assert payloads(second_socket, "final")[0]["match_id"] == "match-cpu-2" + + async def test_a_rematch_reaches_a_new_room_while_the_old_one_is_still_closing( repository: Repository, ) -> None: @@ -2115,3 +2351,804 @@ async def controlled_sleep(_delay: float) -> None: assert engine.fighter("one").last_sequence == welcome["next_sequence"] + 4 sleep_release.set() await manager.close() + + +def ready_choice(style: FighterStyle, *, ready: bool = True) -> StyleChoice: + return StyleChoice(style, ready) + + +def shown_styles(message: dict[str, object]) -> dict[str, str]: + return {entry["id"]: entry["style"] for entry in message["players"] if "style" in entry} + + +async def test_fighters_pick_styles_and_the_bout_starts_when_both_are_ready( + repository: Repository, +) -> None: + manager = HandsRoomManager(repository, config=room_config(round_ticks=600, style_select=5.0)) + first_socket, second_socket = FakeSocket(), FakeSocket() + one = await manager.join(player("one"), first_socket) + two = await manager.join(player("two"), second_socket) + room = one.room + await wait_until(lambda: "select" in message_types(first_socket)) + assert room.engine is None + opening = payloads(first_socket, "select")[0] + assert 4000 < opening["deadline_ms"] <= 5000 + assert opening["ready"] == [] and shown_styles(opening) == {} + assert [entry["id"] for entry in opening["players"]] == ["one", "two"] + + await room.choose_style("one", one.connection, ready_choice(FighterStyle.SLUGGER)) + await wait_until(lambda: len(payloads(second_socket, "select")) == 2) + # The other corner learns that one has settled, never on what: both are revealed at the bell. + assert payloads(second_socket, "select")[1]["ready"] == ["one"] + assert shown_styles(payloads(second_socket, "select")[1]) == {} + assert shown_styles(payloads(first_socket, "select")[1]) == {"one": "slugger"} + assert room.engine is None + + await room.choose_style( + "two", two.connection, ready_choice(FighterStyle.COUNTER_PUNCHER, ready=False) + ) + assert room.engine is None + # A style still being chosen is not shown to anyone, and a settled one only to its fighter. + watcher_socket = FakeSocket() + await manager.join(player("three"), watcher_socket) + await wait_until(lambda: "select" in message_types(watcher_socket)) + assert shown_styles(payloads(watcher_socket, "welcome")[0]) == {} + assert shown_styles(payloads(watcher_socket, "select")[0]) == {} + await room.choose_style("two", two.connection, ready_choice(FighterStyle.BOXER)) + engine = room.engine + assert engine is not None + assert engine.fighter("one").style is FighterStyle.SLUGGER + assert engine.fighter("two").style is FighterStyle.BOXER + await wait_until(lambda: "ready" in message_types(first_socket)) + ready = payloads(first_socket, "ready")[0] + assert {entry["id"]: entry["style"] for entry in ready["players"]} == { + "one": "slugger", + "two": "boxer", + } + await manager.close() + + +async def test_the_deadline_starts_the_bout_with_each_fighters_choice( + repository: Repository, +) -> None: + manager = HandsRoomManager(repository, config=room_config(round_ticks=600, style_select=0.15)) + first_socket = FakeSocket() + one = await manager.join(player("one"), first_socket) + # Before the pick begins a style changes nothing. + await one.room.choose_style("one", one.connection, ready_choice(FighterStyle.SWARMER)) + two = await manager.join(player("two"), FakeSocket()) + await two.room.choose_style( + "two", two.connection, ready_choice(FighterStyle.COUNTER_PUNCHER, ready=False) + ) + await two.room.choose_style( + "two", two.connection, ready_choice(FighterStyle.SLUGGER, ready=False) + ) + assert one.room.engine is None + await wait_until(lambda: one.room.engine is not None) + engine = one.room.engine + assert engine is not None + assert engine.fighter("one").style is FighterStyle.BALANCED + assert engine.fighter("two").style is FighterStyle.SLUGGER + await wait_until(lambda: "ready" in message_types(first_socket)) + await manager.close() + + +async def test_a_fighter_who_never_picks_boxes_balanced(repository: Repository) -> None: + manager = HandsRoomManager(repository, config=room_config(round_ticks=600, style_select=0.1)) + one = await manager.join(player("one"), FakeSocket()) + await manager.join(player("two"), FakeSocket()) + await wait_until(lambda: one.room.engine is not None) + engine = one.room.engine + assert engine is not None + assert {engine.fighter(pid).style for pid in ("one", "two")} == {FighterStyle.BALANCED} + await manager.close() + + +async def test_whoever_arrives_during_the_pick_is_told_about_it(repository: Repository) -> None: + manager = HandsRoomManager(repository, config=room_config(round_ticks=600, style_select=5.0)) + one = await manager.join(player("one"), FakeSocket()) + two = await manager.join(player("two"), FakeSocket()) + await one.room.choose_style("one", one.connection, ready_choice(FighterStyle.BOXER)) + + returning = FakeSocket() + one_again = await manager.join(player("one"), returning) + await wait_until(lambda: "select" in message_types(returning)) + assert payloads(returning, "select")[-1]["ready"] == ["one"] + assert shown_styles(payloads(returning, "select")[-1]) == {"one": "boxer"} + # The returning client offers its remembered style again; the settled pick stands. + await one_again.room.choose_style( + "one", one_again.connection, ready_choice(FighterStyle.SLUGGER, ready=False) + ) + + watcher_socket = FakeSocket() + watcher = await manager.join(player("three"), watcher_socket) + assert watcher.role == "spectator" + await wait_until(lambda: "select" in message_types(watcher_socket)) + assert "snapshot" not in message_types(watcher_socket) + with pytest.raises(RoomError) as raised: + await watcher.room.choose_style( + "three", watcher.connection, ready_choice(FighterStyle.SLUGGER) + ) + assert raised.value.code == "spectator_read_only" + await two.room.choose_style("two", two.connection, ready_choice(FighterStyle.SLUGGER)) + engine = one.room.engine + assert engine is not None + assert engine.fighter("one").style is FighterStyle.BOXER + await manager.close() + + +async def test_the_computer_picks_at_once_and_waits_for_the_fighter( + repository: Repository, +) -> None: + manager = HandsRoomManager( + repository, + config=room_config(round_ticks=600, style_select=5.0), + seed_factory=lambda: 4242, + ) + socket = FakeSocket() + one = await manager.join(player("one"), socket) + await wait_until(lambda: "waiting" in message_types(socket)) + assert await one.room.request_cpu("one", one.connection, CpuLevel.CHAMPION) + assert one.room.engine is None + await wait_until(lambda: "select" in message_types(socket)) + computer_style = cpu_style(CpuLevel.CHAMPION, 4242) + select = payloads(socket, "select")[0] + assert select["ready"] == ["cpu:champion"] + # The computer has settled, but its style is revealed at the bell like anyone's. + assert shown_styles(select) == {} + assert [entry["id"] for entry in select["players"]] == ["one", "cpu:champion"] + assert not await one.room.request_cpu("one", one.connection, CpuLevel.ROOKIE) + + await one.room.choose_style("one", one.connection, ready_choice(FighterStyle.SWARMER)) + engine = one.room.engine + assert engine is not None + assert engine.fighter("cpu:champion").style is computer_style + assert engine.fighter("one").style is FighterStyle.SWARMER + computer = one.room.cpu + assert computer is not None and computer.brain is not None + assert computer.brain.style is computer_style + await manager.close() + + +async def test_a_friend_who_arrives_during_the_computers_pick_takes_its_seat( + repository: Repository, +) -> None: + manager = HandsRoomManager( + repository, + config=room_config(style_select=5.0), + match_id_factory=lambda: "match-friend", + ) + first_socket = FakeSocket() + one = await manager.join(player("one"), first_socket) + assert await one.room.request_cpu("one", one.connection, CpuLevel.CHAMPION) + await wait_until(lambda: "select" in message_types(first_socket)) + + second_socket = FakeSocket() + friend = await manager.join(player("two"), second_socket) + assert friend.role == "fighter" + assert friend.room is one.room + assert one.room.cpu is None + await wait_until(lambda: len(payloads(first_socket, "select")) == 2) + fresh = payloads(first_socket, "select")[-1] + assert [entry["id"] for entry in fresh["players"]] == ["one", "two"] + assert not any(entry.get("cpu") for entry in fresh["players"]) + assert fresh["ready"] == [] and fresh["deadline_ms"] > 4000 + assert message_types(second_socket) == ["welcome", "select"] + # The computer cannot come back into the pick, and a third arrival watches. + assert not await one.room.request_cpu("one", one.connection, CpuLevel.ROOKIE) + watcher = await manager.join(player("three"), FakeSocket()) + assert watcher.role == "spectator" + + await one.room.choose_style("one", one.connection, ready_choice(FighterStyle.BOXER)) + await friend.room.choose_style("two", friend.connection, ready_choice(FighterStyle.SLUGGER)) + engine = one.room.engine + assert engine is not None and engine.players == ("one", "two") + await wait_until(lambda: "final" in message_types(first_socket), deadline_seconds=5) + # A bout between two people is rated. + assert await repository.get_hands_match("match-friend") is not None + await manager.close() + + +async def test_the_computer_keeps_its_seat_once_the_bout_is_on(repository: Repository) -> None: + manager = HandsRoomManager(repository, config=room_config(round_ticks=600)) + one = await manager.join(player("one"), FakeSocket()) + assert await one.room.request_cpu("one", one.connection, CpuLevel.ROOKIE) + assert one.room.engine is not None + late = await manager.join(player("two"), FakeSocket()) + assert late.role == "spectator" + assert one.room.cpu is not None + await manager.close() + + +async def test_the_computer_cannot_be_called_once_two_fighters_are_picking( + repository: Repository, +) -> None: + manager = HandsRoomManager(repository, config=room_config(round_ticks=600, style_select=5.0)) + one = await manager.join(player("one"), FakeSocket()) + await manager.join(player("two"), FakeSocket()) + assert not await one.room.request_cpu("one", one.connection, CpuLevel.ROOKIE) + assert one.room.cpu is None + await manager.close() + + +async def test_an_opponent_who_never_comes_back_returns_the_room_to_waiting( + repository: Repository, +) -> None: + manager = HandsRoomManager( + repository, config=room_config(round_ticks=600, style_select=5.0, reconnect_grace=0.05) + ) + first_socket = FakeSocket() + one = await manager.join(player("one"), first_socket) + two = await manager.join(player("two"), FakeSocket()) + await wait_until(lambda: "select" in message_types(first_socket)) + await manager.leave(two) + await wait_until(lambda: message_types(first_socket)[-1] == "waiting") + assert one.room.engine is None + assert one.room.player_ids == ("one",) + # The corner can be filled again, and the pick starts over. + third_socket = FakeSocket() + await manager.join(player("three"), third_socket) + await wait_until(lambda: "select" in message_types(third_socket)) + await manager.close() + + +def connected_flags(message: dict[str, object]) -> dict[str, bool]: + return {entry["id"]: entry["connected"] for entry in message["players"]} + + +async def test_a_drop_and_a_return_during_the_pick_reach_the_other_corner( + repository: Repository, +) -> None: + manager = HandsRoomManager( + repository, config=room_config(round_ticks=600, style_select=5.0, reconnect_grace=5.0) + ) + first_socket, second_socket = FakeSocket(), FakeSocket() + one = await manager.join(player("one"), first_socket) + two = await manager.join(player("two"), second_socket) + await wait_until(lambda: "select" in message_types(first_socket)) + + await manager.leave(two) + await wait_until(lambda: len(payloads(first_socket, "select")) == 2) + assert connected_flags(payloads(first_socket, "select")[-1]) == {"one": True, "two": False} + # The pick goes on: a pause belongs to the bout, which has not started. + assert "paused" not in message_types(first_socket) + + returning = FakeSocket() + await manager.join(player("two"), returning) + await wait_until(lambda: len(payloads(first_socket, "select")) == 3) + assert connected_flags(payloads(first_socket, "select")[-1]) == {"one": True, "two": True} + assert message_types(returning) == ["welcome", "select"] + assert connected_flags(payloads(returning, "select")[0]) == {"one": True, "two": True} + assert one.room.engine is None + await manager.close() + + +async def test_a_fighter_who_arrives_while_the_other_is_away_sees_him_away_in_the_pick( + repository: Repository, +) -> None: + manager = HandsRoomManager( + repository, config=room_config(round_ticks=600, style_select=5.0, reconnect_grace=5.0) + ) + one = await manager.join(player("one"), FakeSocket()) + await manager.leave(one) + second_socket = FakeSocket() + await manager.join(player("two"), second_socket) + await wait_until(lambda: "select" in message_types(second_socket)) + assert message_types(second_socket) == ["welcome", "select"] + assert connected_flags(payloads(second_socket, "select")[0]) == {"one": False, "two": True} + + first_again = FakeSocket() + await manager.join(player("one"), first_again) + await wait_until(lambda: len(payloads(second_socket, "select")) == 2) + assert connected_flags(payloads(second_socket, "select")[-1]) == {"one": True, "two": True} + assert "paused" not in message_types(second_socket) + await manager.close() + + +async def test_a_seat_still_empty_at_the_bell_opens_the_bout_paused( + repository: Repository, +) -> None: + manager = HandsRoomManager( + repository, config=room_config(round_ticks=600, style_select=0.1, reconnect_grace=5.0) + ) + first_socket = FakeSocket() + one = await manager.join(player("one"), first_socket) + two = await manager.join(player("two"), FakeSocket()) + await manager.leave(two) + await wait_until(lambda: "paused" in message_types(first_socket)) + after_pick = message_types(first_socket)[message_types(first_socket).index("ready") :] + assert after_pick[:2] == ["ready", "paused"] + paused = payloads(first_socket, "paused")[0] + assert paused["player_id"] == "two" + assert 3000 < paused["grace_ms"] <= 5000 + engine = one.room.engine + assert engine is not None + paused_at = engine.tick + await asyncio.sleep(0.05) + assert engine.tick == paused_at + + await manager.join(player("two"), FakeSocket()) + await wait_until(lambda: "resumed" in message_types(first_socket)) + await wait_until(lambda: engine.tick > paused_at) + await manager.close() + + +async def test_a_seat_that_opens_during_the_pick_goes_to_the_waiting_spectator( + repository: Repository, +) -> None: + tickets = iter(f"seat-ticket-{index}" for index in range(10)) + manager = HandsRoomManager( + repository, + config=room_config(style_select=5.0, reconnect_grace=0.05), + match_id_factory=lambda: "match-seated", + ) + first_socket = FakeSocket() + one = await manager.join(player("one"), first_socket) + two = await manager.join(player("two"), FakeSocket()) + watcher_socket = FakeSocket() + watcher = await manager.join( + player("three"), watcher_socket, reconnect_ticket_factory=lambda: next(tickets) + ) + assert watcher.role == "spectator" + with pytest.raises(RoomError, match="spectator_read_only"): + await watcher.room.request_cpu("three", watcher.connection, CpuLevel.ROOKIE) + + # Two never comes back: his seat goes to the spectator, on the connection he already has. + await manager.leave(two) + await wait_until(lambda: one.room.player_ids == ("one", "three")) + assert one.room.spectator_ids == () + await wait_until(lambda: message_types(watcher_socket)[-2:] == ["welcome", "select"]) + # Watching: the pick, then two dropping out of it. Seated: welcomed again, and a fresh pick. + assert message_types(watcher_socket) == ["welcome", "select", "select", "welcome", "select"] + seated = payloads(watcher_socket, "welcome")[1] + assert (seated["role"], seated["seat"], seated["next_sequence"]) == ("fighter", 2, 0) + assert seated["reconnect_ticket"] == "seat-ticket-1" + assert watcher_socket.uncompressed == ["welcome", "welcome"] + fresh = payloads(first_socket, "select")[-1] + assert [entry["id"] for entry in fresh["players"]] == ["one", "three"] + assert fresh["ready"] == [] and fresh["deadline_ms"] > 4000 + + # He picks through the membership he joined with, and the bout is his. + await one.room.choose_style("one", one.connection, ready_choice(FighterStyle.BOXER)) + await watcher.room.choose_style("three", watcher.connection, ready_choice(FighterStyle.SWARMER)) + engine = one.room.engine + assert engine is not None and engine.players == ("one", "three") + assert engine.fighter("three").style is FighterStyle.SWARMER + await wait_until(lambda: "final" in message_types(watcher_socket), deadline_seconds=5) + assert await repository.get_hands_match("match-seated") is not None + await manager.close() + + +async def test_a_seated_spectator_who_leaves_keeps_his_seat_through_the_grace( + repository: Repository, +) -> None: + manager = HandsRoomManager( + repository, config=room_config(style_select=5.0, reconnect_grace=0.05) + ) + first_socket = FakeSocket() + one = await manager.join(player("one"), first_socket) + two = await manager.join(player("two"), FakeSocket()) + watcher = await manager.join(player("three"), FakeSocket()) + await manager.leave(two) + await wait_until(lambda: one.room.player_ids == ("one", "three")) + room = one.room + + await manager.leave(watcher) + # A fighter's place now, so leaving is a drop with a grace, as for anyone seated. + assert room.player_ids == ("one", "three") + await wait_until( + lambda: ( + connected_flags(payloads(first_socket, "select")[-1]) == {"one": True, "three": False} + ) + ) + returning = FakeSocket() + again = await manager.join(player("three"), returning) + assert again.role == "fighter" and again.room is room + await manager.close() + + +async def finished_bout( + manager: HandsRoomManager, first: str = "one", second: str | None = "two" +) -> HandsRoom: + """A bout fought to its result in instance-1, whose room is then retired.""" + socket = FakeSocket() + one = await manager.join(player(first), socket) + if second is None: + assert await one.room.request_cpu(first, one.connection, CpuLevel.ROOKIE) + else: + two = await manager.join(player(second), FakeSocket()) + await two.room.choose_style(second, two.connection, ready_choice(FighterStyle.BOXER)) + await one.room.choose_style(first, one.connection, ready_choice(FighterStyle.BOXER)) + await wait_until(lambda: "final" in message_types(socket), deadline_seconds=5) + await wait_until(lambda: manager.room_count == 0) + return one.room + + +async def test_the_rematch_room_holds_the_fighters_seats_and_seats_others_as_spectators( + repository: Repository, +) -> None: + manager = HandsRoomManager(repository, config=room_config(style_select=5.0)) + old = await finished_bout(manager) + assert set(old.rematch_fighters) == {"one", "two"} + + # Somebody who relaunches before the fighters are back cannot take a seat. + early_socket = FakeSocket() + early = await manager.join(player("three"), early_socket) + assert early.role == "spectator" and early.room is not old + await wait_until(lambda: message_types(early_socket) == ["welcome", "waiting"]) + welcome, waiting = (json.loads(message) for message in early_socket.messages) + assert welcome["players"] == [] + assert waiting["open_seats"] == 2 + + first_socket = FakeSocket() + one = await manager.join(player("one"), first_socket) + assert one.role == "fighter" and one.room is early.room + latecomer = await manager.join(player("four"), FakeSocket()) + assert latecomer.role == "spectator" + await manager.join(player("two"), FakeSocket()) + assert one.room.player_ids == ("one", "two") + assert one.room.spectator_ids == ("three", "four") + await wait_until(lambda: "select" in message_types(early_socket)) + await manager.close() + + +async def test_a_held_seat_nobody_comes_back_for_goes_to_whoever_waits_for_it( + repository: Repository, +) -> None: + manager = HandsRoomManager(repository, config=room_config(style_select=5.0, rematch_seat=0.2)) + await finished_bout(manager) + watcher_socket = FakeSocket() + watcher = await manager.join(player("three"), watcher_socket) + first_socket = FakeSocket() + one = await manager.join(player("one"), first_socket) + assert (watcher.role, one.role) == ("spectator", "fighter") + + # Two never comes back: once the window is over, his seat goes to the one watching. + await wait_until(lambda: one.room.player_ids == ("one", "three"), deadline_seconds=2) + # The seat changes hands before the socket's writer has sent the new welcome and select. + await wait_until(lambda: message_types(watcher_socket)[-2:] == ["welcome", "select"]) + assert payloads(watcher_socket, "welcome")[-1]["role"] == "fighter" + await wait_until(lambda: "select" in message_types(first_socket)) + assert [entry["id"] for entry in payloads(first_socket, "select")[-1]["players"]] == [ + "one", + "three", + ] + await manager.close() + + +async def test_a_seat_is_held_only_for_those_still_in_the_ring_at_the_result( + repository: Repository, +) -> None: + manager = HandsRoomManager(repository, config=room_config(style_select=5.0)) + # Against the computer, only the person's seat is held: a friend can take the other. + await finished_bout(manager, second=None) + friend = await manager.join(player("two"), FakeSocket()) + assert friend.role == "fighter" + await manager.close() + + manager = HandsRoomManager( + repository, config=room_config(round_ticks=100_000, reconnect_grace=0.05) + ) + first_socket = FakeSocket() + one = await manager.join(player("one"), first_socket) + two = await manager.join(player("two"), FakeSocket()) + # Two walks out and forfeits: nobody holds his seat for him. + await manager.leave(two) + await wait_until(lambda: "final" in message_types(first_socket), deadline_seconds=5) + await wait_until(lambda: manager.room_count == 0) + assert one.room.rematch_fighters == ("one",) + newcomer = await manager.join(player("three"), FakeSocket()) + assert newcomer.role == "fighter" + await manager.close() + + +async def test_a_style_sent_after_the_bell_changes_nothing(repository: Repository) -> None: + manager = HandsRoomManager(repository, config=room_config(round_ticks=600)) + one = await manager.join(player("one"), FakeSocket()) + await manager.join(player("two"), FakeSocket()) + engine = one.room.engine + assert engine is not None + await one.room.choose_style("one", one.connection, ready_choice(FighterStyle.SLUGGER)) + assert engine.fighter("one").style is FighterStyle.BALANCED + await manager.close() + + +async def test_a_flood_of_style_picks_is_throttled_with_the_fighters_inputs( + repository: Repository, +) -> None: + manager = HandsRoomManager(repository, config=room_config(round_ticks=600, style_select=5.0)) + first_socket = FakeSocket() + one = await manager.join(player("one"), first_socket) + two = await manager.join(player("two"), FakeSocket()) + await wait_until(lambda: "select" in message_types(first_socket)) + limit = 8 + for _ in range(limit): + await two.room.choose_style( + "two", two.connection, ready_choice(FighterStyle.BOXER, ready=False) + ) + # Over the rate a pick is dropped, settled or not. + await two.room.choose_style("two", two.connection, ready_choice(FighterStyle.SLUGGER)) + assert one.room.engine is None + with pytest.raises(RoomError, match="rate_limited"): + for _ in range(4 * limit): + await two.room.choose_style( + "two", two.connection, ready_choice(FighterStyle.SLUGGER, ready=False) + ) + await one.room.choose_style("one", one.connection, ready_choice(FighterStyle.SWARMER)) + assert one.room.engine is None + await manager.close() + + +async def test_a_rated_bout_records_and_logs_both_fighters_styles( + repository: Repository, monkeypatch: pytest.MonkeyPatch +) -> None: + safe_logger = MagicMock() + monkeypatch.setattr(rooms_module, "logger", safe_logger) + manager = HandsRoomManager( + repository, + config=room_config(style_select=5.0), + match_id_factory=lambda: "match-styles", + ) + first_socket = FakeSocket() + one = await manager.join(player("one"), first_socket) + two = await manager.join(player("two"), FakeSocket()) + await one.room.choose_style("one", one.connection, ready_choice(FighterStyle.SWARMER)) + await two.room.choose_style("two", two.connection, ready_choice(FighterStyle.SLUGGER)) + await wait_until(lambda: "final" in message_types(first_socket), deadline_seconds=5) + + match = await repository.get_hands_match("match-styles") + assert match is not None + stored = json.loads(match.result_json) + assert (stored["player_one_style"], stored["player_two_style"]) == ("swarmer", "slugger") + safe_logger.info.assert_any_call( + "Hands bout started", + instance_id="instance-1", + match_id="match-styles", + cpu_level=None, + player_one_style="swarmer", + player_two_style="slugger", + ) + await manager.close() + + +def test_the_engine_counts_each_fighters_punches_as_the_events_show_them() -> None: + engine = BoxingEngine( + match_id="match-count", + activity_instance_id="instance-1", + guild_id="guild-1", + player_one_id="one", + player_two_id="two", + seed=29, + config=EngineConfig(rounds=1, round_ticks=2400, countdown_ticks=0), + ) + brains = [ + CpuBrain("one", "two", CpuLevel.CHAMPION, 3, FighterStyle.SWARMER), + CpuBrain("two", "one", CpuLevel.CONTENDER, 5, FighterStyle.BOXER), + ] + events: list[CombatEvent] = [] + while engine.result is None and engine.tick < 2400: + for brain in brains: + command = brain.decide(engine) + if command is not None: + engine.submit_input(brain.player_id, command) + events.extend(engine.step().events) + + def guarded(hit: CombatEvent) -> bool: + return any( + other.kind in {"block", "perfect_block"} + and other.tick == hit.tick + and other.action_id == hit.action_id + for other in events + ) + + blocked = 0 + for fighter_id in ("one", "two"): + starts = [e for e in events if e.kind == "punch_start" and e.actor_id == fighter_id] + hits = [e for e in events if e.kind in {"hit", "counter_hit"} and e.actor_id == fighter_id] + clean = [hit for hit in hits if not guarded(hit)] + blocked += len(hits) - len(clean) + assert engine.fighter(fighter_id).punches == PunchCount( + thrown=len(starts), + landed=len(clean), + jabs_thrown=sum(e.detail.split(":")[1] == "jab" for e in starts), + jabs_landed=sum(hit.detail.split(":")[0] == "jab" for hit in clean), + ) + # The bout had punches the guard took, which do not count as landed. + assert blocked > 0 + + +async def test_the_final_carries_the_engines_punch_counts_for_the_whole_bout( + repository: Repository, +) -> None: + manager = HandsRoomManager(repository, config=room_config(round_ticks=90)) + socket = FakeSocket() + one = await manager.join(player("one"), socket) + assert await one.room.request_cpu("one", one.connection, CpuLevel.CHAMPION) + engine = one.room.engine + assert engine is not None + await wait_until(lambda: "final" in message_types(socket), deadline_seconds=5) + final = payloads(socket, "final")[0] + assert set(final["punches"]) == {"one", "cpu:champion"} + for fighter_id, counted in final["punches"].items(): + assert counted == asdict(engine.fighter(fighter_id).punches) + assert final["punches"]["cpu:champion"]["thrown"] > 0 + await manager.close() + + +async def test_a_flood_of_inputs_after_the_result_is_cut_off_through_the_hold( + repository: Repository, +) -> None: + manager = HandsRoomManager(repository, config=room_config(result_hold=1.0)) + socket = FakeSocket() + one = await manager.join(player("one"), socket) + assert await one.room.request_cpu("one", one.connection, CpuLevel.ROOKIE) + await wait_until(lambda: "final" in message_types(socket), deadline_seconds=5) + frame = encode_client_input(InputCommand(sequence=0, client_tick=0)) + limit = 8 + # Inputs in flight as the bout ends are dropped without a word. + for _ in range(limit): + await one.room.submit_frame("one", one.connection, frame) + # A stream of them through the result hold draws on the same allowance as any other. + with pytest.raises(RoomError, match="rate_limited"): + for _ in range(4 * limit): + await one.room.submit_frame("one", one.connection, frame) + await manager.close() + + +async def test_a_rematch_picks_styles_afresh(repository: Repository) -> None: + manager = HandsRoomManager(repository, config=room_config(round_ticks=30, style_select=5.0)) + first_socket = FakeSocket() + one = await manager.join(player("one"), first_socket) + two = await manager.join(player("two"), FakeSocket()) + await one.room.choose_style("one", one.connection, ready_choice(FighterStyle.SLUGGER)) + await two.room.choose_style("two", two.connection, ready_choice(FighterStyle.BOXER)) + await wait_until(lambda: "final" in message_types(first_socket), deadline_seconds=5) + await wait_until(lambda: manager.room_count == 0) + + rematch_socket = FakeSocket() + one_again = await manager.join(player("one"), rematch_socket) + assert one_again.room is not one.room + two_again = await manager.join(player("two"), FakeSocket()) + await wait_until(lambda: "select" in message_types(rematch_socket)) + opening = payloads(rematch_socket, "select")[0] + assert opening["ready"] == [] and shown_styles(opening) == {} + await one_again.room.choose_style( + "one", one_again.connection, ready_choice(FighterStyle.SWARMER) + ) + await two_again.room.choose_style( + "two", two_again.connection, ready_choice(FighterStyle.SLUGGER) + ) + engine = one_again.room.engine + assert engine is not None + assert engine.fighter("one").style is FighterStyle.SWARMER + assert engine.fighter("two").style is FighterStyle.SLUGGER + await manager.close() + + +async def test_a_computer_bout_pauses_with_its_fighter_and_resumes_on_his_reconnect( + repository: Repository, +) -> None: + manager = HandsRoomManager( + repository, config=room_config(round_ticks=100_000, reconnect_grace=5.0) + ) + one = await manager.join(player("one"), FakeSocket(), reconnect_ticket="first-rotation") + assert await one.room.request_cpu("one", one.connection, CpuLevel.CHAMPION) + engine = one.room.engine + assert engine is not None + computer = engine.fighter("cpu:champion") + await wait_until(lambda: engine.tick > 20) + + await manager.leave(one) + # Nobody walks or guards on what he held when the pause began, the computer included, and the + # computer does not box on while its opponent is away. + for fighter_id in ("one", "cpu:champion"): + held = engine.fighter(fighter_id).held_input + assert (held.move_x, held.move_y, held.defense) == (0, 0, DefensivePose.NONE) + paused_at = (engine.tick, computer.last_sequence) + await asyncio.sleep(0.05) + assert (engine.tick, computer.last_sequence) == paused_at + + socket = FakeSocket() + again = await manager.join(player("one"), socket, reconnect_ticket="next-rotation") + assert again.room is one.room + await wait_until(lambda: len(socket.messages) >= 3) + assert message_types(socket)[:3] == ["welcome", "snapshot", "resumed"] + assert socket.uncompressed == ["welcome"] + await wait_until(lambda: computer.last_sequence > paused_at[1]) + await manager.close() + + +async def test_a_computer_that_fails_mid_bout_voids_it_and_frees_the_instance( + repository: Repository, monkeypatch: pytest.MonkeyPatch +) -> None: + safe_logger = MagicMock() + monkeypatch.setattr(rooms_module, "logger", safe_logger) + manager = HandsRoomManager( + repository, + config=room_config(round_ticks=100_000), + match_id_factory=lambda: "match-cpu-failure", + ) + socket = FakeSocket() + one = await manager.join(player("one"), socket) + assert await one.room.request_cpu("one", one.connection, CpuLevel.ROOKIE) + computer = one.room.cpu + engine = one.room.engine + assert computer is not None and computer.brain is not None and engine is not None + await wait_until(lambda: engine.tick > 5) + + def broken_decide(_engine: object) -> None: + raise RuntimeError("computer bug") + + monkeypatch.setattr(computer.brain, "decide", broken_decide) + await wait_until(lambda: manager.room_count == 0) + + assert socket.closed + assert socket.close_code == 1011 + errors = [json.loads(message) for message in socket.messages if '"type":"error"' in message] + assert errors == [{"code": "internal_error", "type": "error", "version": PROTOCOL_VERSION}] + safe_logger.exception.assert_called_once() + assert engine.result is None + assert await repository.get_hands_match("match-cpu-failure") is None + rating = await repository.get_hands_rating("guild-1", "one") + assert rating is not None + assert (rating.bouts, rating.rating) == (0, 1000) + # The instance is free for a fresh bout, against the computer again if he likes. + again_socket = FakeSocket() + again = await manager.join(player("one"), again_socket) + assert again.room is not one.room + await wait_until(lambda: "waiting" in message_types(again_socket)) + assert await again.room.request_cpu("one", again.connection, CpuLevel.ROOKIE) + await manager.close() + + +async def test_a_spectator_who_arrives_during_the_pick_gets_the_newest_snapshot_once_it_starts( + repository: Repository, +) -> None: + sleep_release = asyncio.Event() + + async def controlled_sleep(_delay: float) -> None: + await sleep_release.wait() + + manager = HandsRoomManager( + repository, + config=room_config(round_ticks=1000, style_select=5.0), + sleep=controlled_sleep, + ) + one = await manager.join(player("one"), FakeSocket()) + two = await manager.join(player("two"), FakeSocket()) + watcher_socket = FakeSocket() + watcher = await manager.join(player("three"), watcher_socket) + assert watcher.role == "spectator" + await wait_until(lambda: "select" in message_types(watcher_socket)) + assert message_types(watcher_socket) == ["welcome", "select"] + assert watcher_socket.uncompressed == ["welcome"] + + # The watcher's transport is backed up as the bell goes: snapshots wait, newest kept. + watcher_socket.buffered = SNAPSHOT_BACKLOG_BYTES + 1 + room = one.room + await room.choose_style("one", one.connection, ready_choice(FighterStyle.BOXER)) + await room.choose_style("two", two.connection, ready_choice(FighterStyle.SLUGGER)) + engine = room.engine + assert engine is not None + # The tick loop has stepped once and is parked in its sleep. + await wait_until(lambda: engine.tick >= 1) + base = engine.snapshot() + hit = CombatEvent(event_id=900, tick=base.tick + 1, kind="hit", actor_id="two", amount=12) + for offset in range(1, 6): + room._broadcast_snapshot( + replace(base, tick=base.tick + offset, events=(hit,) if offset == 1 else ()) + ) + await asyncio.sleep(0.05) + assert "snapshot" not in message_types(watcher_socket) + + watcher_socket.buffered = 0 + await wait_until(lambda: bool(snapshot_payloads(watcher_socket))) + await asyncio.sleep(0.02) + snapshots = snapshot_payloads(watcher_socket) + assert [payload["tick"] for payload in snapshots] == [base.tick + 5] + events = snapshots[0]["events"] + assert isinstance(events, list) + assert 900 in {event["event_id"] for event in events} + assert {fighter["style"] for fighter in snapshots[0]["fighters"]} == {"boxer", "slugger"} + sleep_release.set() + await manager.close() diff --git a/tests/test_hands/test_scoring.py b/tests/test_hands/test_scoring.py index 4bafd911..04b74cd8 100644 --- a/tests/test_hands/test_scoring.py +++ b/tests/test_hands/test_scoring.py @@ -76,3 +76,27 @@ def test_knockdown_score_preserves_explicit_point_deduction() -> None: assert score.player_one == 9 assert score.player_two == 8 + + +def test_ring_control_alone_never_wins_a_round() -> None: + """A round in which nobody lands a clean punch and the damage is even is even on every card, + however long one fighter held the centre of the ring.""" + mover = RoundPerformance(damage=30, blocked_hits=4, control=2 * 3600) + still = RoundPerformance(damage=30, blocked_hits=4) + + for profile in JUDGE_PROFILES: + score = score_round(mover, still, profile) + assert (score.player_one, score.player_two) == (10, 10) + + +def test_ring_control_settles_an_even_round_of_boxing_but_never_outweighs_a_clean_punch() -> None: + mover = RoundPerformance(damage=300, clean_hits=12, control=2 * 3600) + still = RoundPerformance(damage=300, clean_hits=12) + # One clean straight more, with its damage. + puncher = RoundPerformance(damage=330, clean_hits=13) + + scores = {profile.name: score_round(mover, still, profile) for profile in JUDGE_PROFILES} + assert (scores["Generalship"].player_one, scores["Generalship"].player_two) == (10, 9) + for profile in JUDGE_PROFILES: + score = score_round(mover, puncher, profile) + assert score.player_one <= score.player_two diff --git a/tests/test_hands/test_server.py b/tests/test_hands/test_server.py index db808b6e..662a17ae 100644 --- a/tests/test_hands/test_server.py +++ b/tests/test_hands/test_server.py @@ -21,6 +21,7 @@ from intelstream.hands import server as server_module from intelstream.hands.auth import AuthenticatedPlayer, AuthExchange, HandsAuth, HandsAuthError from intelstream.hands.engine import EngineConfig +from intelstream.hands.protocol import PROTOCOL_VERSION from intelstream.hands.rooms import ( SNAPSHOT_BACKLOG_BYTES, HandsRoomManager, @@ -81,6 +82,9 @@ def activate_ticket(self, ticket: object, player: AuthenticatedPlayer) -> None: if owner == player and candidate != ticket: self.tickets.pop(candidate, None) + def instance_for_state(self, state: object) -> str | None: + return {"oauth-state": "instance", "other-state": "elsewhere"}.get(str(state)) + async def close(self) -> None: self.closed = True @@ -127,6 +131,7 @@ async def start_server( admission: AdmissionConfig | None = None, monotonic_clock: Callable[[], float] = time.monotonic, static_root: Path | None = None, + websocket_heartbeat: float = server_module.WEBSOCKET_HEARTBEAT_SECONDS, ) -> tuple[HandsServer, FakeAuth, str]: fake_auth = auth or FakeAuth() server = HandsServer( @@ -146,6 +151,7 @@ async def start_server( admission=admission, monotonic_clock=monotonic_clock, static_root=static_root, + websocket_heartbeat_seconds=websocket_heartbeat, ) await server.start() assert server.bound_port is not None @@ -179,7 +185,9 @@ async def connect_reader_that_stalls( while b"\r\n\r\n" not in received: received += await loop.sock_recv(raw, 1) assert b" 101 " in received.split(b"\r\n", 1)[0] - payload = json.dumps({"version": 3, "type": "authenticate", "ticket": ticket}).encode() + payload = json.dumps( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": ticket} + ).encode() assert len(payload) < 126 mask = os.urandom(4) masked = bytes(byte ^ mask[index % 4] for index, byte in enumerate(payload)) @@ -230,7 +238,9 @@ async def connect_deflate_client( ), "", ) - payload = json.dumps({"version": 3, "type": "authenticate", "ticket": ticket}).encode() + payload = json.dumps( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": ticket} + ).encode() mask = os.urandom(4) masked = bytes(byte ^ mask[index % 4] for index, byte in enumerate(payload)) writer.write(bytes([0x81, 0x80 | len(payload)]) + mask + masked) @@ -265,6 +275,7 @@ def stalled_reader_rooms( return HandsRoomManager( repository, config=RoomConfig( + style_select_seconds=0.0, tick_interval_seconds=0.002, reconnect_grace_seconds=0.5, result_hold_seconds=0.0, @@ -343,6 +354,15 @@ async def test_health_static_security_and_safe_resolution( assert "Hands" in body assert index.headers["Cache-Control"] == "no-store" assert "frame-ancestors" in index.headers["Content-Security-Policy"] + policy = dict( + directive.strip().split(" ", 1) + for directive in index.headers["Content-Security-Policy"].split(";") + ) + # Discord's image host only for the players' avatars, as the bundle scan allows. + assert policy["img-src"] == "'self' data: blob: https://cdn.discordapp.com/avatars/" + assert policy["connect-src"] == "'self'" + assert policy["script-src"] == "'self'" + assert policy["default-src"] == "'self'" assert "client-secret" not in body asset = await client.get(f"{base}/app.js") @@ -391,7 +411,7 @@ async def test_bootstrap_token_origin_schema_media_and_no_store( bootstrap = await post_bootstrap(client, base, "instance", headers=headers) assert await bootstrap.json() == { "client_id": APP, - "protocol": 3, + "protocol": PROTOCOL_VERSION, "state": "oauth-state", "simulation": { "tick_rate": 30, @@ -487,7 +507,9 @@ async def test_websocket_requires_ticket_first_without_query_and_times_out( await ws.close() malformed = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) - await malformed.send_json({"version": 3, "type": "authenticate", "ticket": "bad"}) + await malformed.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "bad"} + ) message = await malformed.receive(timeout=1) assert json.loads(message.data)["code"] == "invalid_ticket" await malformed.close() @@ -503,8 +525,14 @@ async def test_an_outdated_client_is_told_in_its_own_version_without_spending_it server, _auth, base = await start_server(repository, auth=auth) async with aiohttp.ClientSession() as client: for frame in ( - {"version": 2, "type": "authenticate", "ticket": "valid"}, - {"version": 4, "type": "authenticate", "ticket": "valid", "build": "next"}, + # A window still on the build before this protocol, and one from a newer build. + {"version": PROTOCOL_VERSION - 1, "type": "authenticate", "ticket": "valid"}, + { + "version": PROTOCOL_VERSION + 1, + "type": "authenticate", + "ticket": "valid", + "build": "next", + }, ): socket = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) await socket.send_json(frame) @@ -521,9 +549,9 @@ async def test_an_outdated_client_is_told_in_its_own_version_without_spending_it assert "valid" in auth.tickets socket = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) - await socket.send_json({"version": 3, "type": "resume", "ticket": "valid"}) + await socket.send_json({"version": PROTOCOL_VERSION, "type": "resume", "ticket": "valid"}) error = json.loads((await socket.receive(timeout=1)).data) - assert error == {"code": "invalid_ticket", "type": "error", "version": 3} + assert error == {"code": "invalid_ticket", "type": "error", "version": PROTOCOL_VERSION} await socket.close() await server.close() @@ -556,7 +584,9 @@ async def join(self, *_args, **_kwargs): ) async with aiohttp.ClientSession() as client: socket = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) - await socket.send_json({"version": 3, "type": "authenticate", "ticket": "valid"}) + await socket.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "valid"} + ) async with asyncio.timeout(2): await rooms.entered.wait() await asyncio.sleep(2 * auth_timeout) @@ -596,7 +626,9 @@ def issue_ticket(self, player: AuthenticatedPlayer) -> str: server, _auth, base = await start_server(repository, auth=auth) async with aiohttp.ClientSession() as client: socket = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) - await socket.send_json({"version": 3, "type": "authenticate", "ticket": "valid"}) + await socket.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "valid"} + ) await admission_entered.wait() assert auth.ticket_counter == 0 now = 2000.0 @@ -626,6 +658,7 @@ async def controlled_sleep(_delay: float) -> None: rooms = HandsRoomManager( repository, config=RoomConfig( + style_select_seconds=0.0, tick_interval_seconds=0.002, broadcast_every_ticks=1, reconnect_grace_seconds=0.1, @@ -646,7 +679,9 @@ async def controlled_sleep(_delay: float) -> None: sockets = [] for ticket in ("one", "two"): ws = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) - await ws.send_json({"version": 3, "type": "authenticate", "ticket": ticket}) + await ws.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": ticket} + ) sockets.append(ws) async with asyncio.timeout(1): seen_ready = False @@ -655,7 +690,9 @@ async def controlled_sleep(_delay: float) -> None: if message.type == aiohttp.WSMsgType.TEXT: seen_ready = json.loads(message.data)["type"] == "ready" third = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) - await third.send_json({"version": 3, "type": "authenticate", "ticket": "three"}) + await third.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "three"} + ) welcome = json.loads((await third.receive(timeout=1)).data) assert welcome["type"] == "welcome" assert welcome["role"] == "spectator" @@ -676,6 +713,220 @@ async def controlled_sleep(_delay: float) -> None: await server.close() +async def test_a_lone_fighter_calls_the_computer_and_a_spectator_cannot( + repository: Repository, +) -> None: + auth = FakeAuth() + auth.tickets = { + "one": AuthenticatedPlayer("one", GUILD, "room", "One", None), + "two": AuthenticatedPlayer("two", GUILD, "room", "Two", None), + } + rooms = HandsRoomManager( + repository, + config=RoomConfig( + style_select_seconds=0.0, + tick_interval_seconds=0.002, + broadcast_every_ticks=1, + reconnect_grace_seconds=0.1, + result_hold_seconds=0.05, + engine_config=EngineConfig( + rounds=1, + round_ticks=5000, + rest_ticks=0, + countdown_ticks=1, + flash_ko_enabled=False, + ), + ), + match_id_factory=lambda: "server-cpu", + ) + server, _auth, base = await start_server(repository, auth=auth, rooms=rooms) + + async def receive_type(ws: aiohttp.ClientWebSocketResponse, kind: str) -> dict: + async with asyncio.timeout(1): + while True: + message = await ws.receive() + if message.type != aiohttp.WSMsgType.TEXT: + raise AssertionError(f"socket closed before {kind}") + payload = json.loads(message.data) + if payload["type"] == kind: + return payload + + async with aiohttp.ClientSession() as client: + fighter = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) + await fighter.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "one"} + ) + await receive_type(fighter, "waiting") + await fighter.send_json({"version": PROTOCOL_VERSION, "type": "cpu", "level": "champion"}) + ready = await receive_type(fighter, "ready") + assert [player["id"] for player in ready["players"]] == ["one", "cpu:champion"] + assert ready["players"][1]["cpu"] is True + snapshot = await receive_type(fighter, "snapshot") + assert [entry["player_id"] for entry in snapshot["payload"]["fighters"]] == [ + "one", + "cpu:champion", + ] + + watcher = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) + await watcher.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "two"} + ) + welcome = await receive_type(watcher, "welcome") + assert welcome["role"] == "spectator" + await watcher.send_json({"version": PROTOCOL_VERSION, "type": "cpu", "level": "rookie"}) + error = await receive_type(watcher, "error") + assert error["code"] == "spectator_read_only" + await watcher.close() + assert not fighter.closed + async with asyncio.timeout(1): + await fighter.close() + async with asyncio.timeout(1): + await server.close() + + +async def test_fighters_pick_their_styles_over_the_socket_and_a_spectator_cannot( + repository: Repository, +) -> None: + auth = FakeAuth() + auth.tickets = { + name: AuthenticatedPlayer(name, GUILD, "room", name.title(), None) + for name in ("one", "two", "three") + } + rooms = HandsRoomManager( + repository, + config=RoomConfig( + style_select_seconds=5.0, + tick_interval_seconds=0.002, + broadcast_every_ticks=1, + reconnect_grace_seconds=0.1, + result_hold_seconds=0.05, + engine_config=EngineConfig( + rounds=1, + round_ticks=5000, + rest_ticks=0, + countdown_ticks=1, + flash_ko_enabled=False, + ), + ), + match_id_factory=lambda: "server-styles", + ) + server, _auth, base = await start_server(repository, auth=auth, rooms=rooms) + + async def receive_type(ws: aiohttp.ClientWebSocketResponse, kind: str) -> dict: + async with asyncio.timeout(1): + while True: + message = await ws.receive() + if message.type != aiohttp.WSMsgType.TEXT: + raise AssertionError(f"socket closed before {kind}") + payload = json.loads(message.data) + if payload["type"] == kind: + return payload + + def choice(style: str, ready: bool) -> dict: + return {"version": PROTOCOL_VERSION, "type": "style", "style": style, "ready": ready} + + async with aiohttp.ClientSession() as client: + sockets = {} + for name in ("one", "two"): + sockets[name] = await client.ws_connect( + f"{base}/api/hands/ws", headers={"Origin": ORIGIN} + ) + await sockets[name].send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": name} + ) + await receive_type(sockets["one"], "select") + await sockets["one"].send_json(choice("swarmer", True)) + await sockets["two"].send_json(choice("counter_puncher", True)) + ready = await receive_type(sockets["two"], "ready") + assert {player["id"]: player["style"] for player in ready["players"]} == { + "one": "swarmer", + "two": "counter_puncher", + } + snapshot = await receive_type(sockets["one"], "snapshot") + assert [entry["style"] for entry in snapshot["payload"]["fighters"]] == [ + "swarmer", + "counter_puncher", + ] + + watcher = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) + await watcher.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "three"} + ) + assert (await receive_type(watcher, "welcome"))["role"] == "spectator" + await watcher.send_json(choice("slugger", True)) + assert (await receive_type(watcher, "error"))["code"] == "spectator_read_only" + await watcher.close() + async with asyncio.timeout(1): + for ws in sockets.values(): + await ws.close() + async with asyncio.timeout(1): + await server.close() + + +async def test_a_spectator_seated_before_the_bell_fights_over_the_same_socket( + repository: Repository, +) -> None: + auth = FakeAuth() + auth.tickets = { + name: AuthenticatedPlayer(name, GUILD, "room", name.title(), None) + for name in ("one", "two", "three") + } + rooms = HandsRoomManager( + repository, + config=RoomConfig( + style_select_seconds=5.0, + tick_interval_seconds=0.002, + reconnect_grace_seconds=0.1, + result_hold_seconds=0.05, + engine_config=EngineConfig(rounds=1, round_ticks=5000, rest_ticks=0, countdown_ticks=1), + ), + ) + server, _auth, base = await start_server(repository, auth=auth, rooms=rooms) + + async def receive_type(ws: aiohttp.ClientWebSocketResponse, kind: str) -> dict: + async with asyncio.timeout(2): + while True: + message = await ws.receive() + if message.type != aiohttp.WSMsgType.TEXT: + raise AssertionError(f"socket closed before {kind}") + payload = json.loads(message.data) + assert payload["type"] != "error", payload + if payload["type"] == kind: + return payload + + async def connect(name: str) -> aiohttp.ClientWebSocketResponse: + ws = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) + await ws.send_json({"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": name}) + return ws + + def choice(style: str) -> dict: + return {"version": PROTOCOL_VERSION, "type": "style", "style": style, "ready": True} + + async with aiohttp.ClientSession() as client: + one = await connect("one") + two = await connect("two") + await receive_type(one, "select") + three = await connect("three") + assert (await receive_type(three, "welcome"))["role"] == "spectator" + await two.close() + seated = await receive_type(three, "welcome") + assert (seated["role"], seated["seat"]) == ("fighter", 2) + assert seated["reconnect_ticket"] in auth.tickets + await three.send_json(choice("slugger")) + await one.send_json(choice("boxer")) + ready = await receive_type(three, "ready") + assert {player["id"]: player["style"] for player in ready["players"]} == { + "one": "boxer", + "three": "slugger", + } + await receive_type(three, "snapshot") + async with asyncio.timeout(1): + for ws in (one, three): + await ws.close() + async with asyncio.timeout(1): + await server.close() + + async def test_state_updates_are_deflated_but_frames_carrying_a_ticket_never_are( repository: Repository, ) -> None: @@ -716,7 +967,7 @@ async def read_until(kind: str, count: int = 1) -> None: async with aiohttp.ClientSession() as client: two = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) - await two.send_json({"version": 3, "type": "authenticate", "ticket": "two"}) + await two.send_json({"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "two"}) await read_until("snapshot", 3) refresh_now.set() await read_until("ticket") @@ -749,9 +1000,10 @@ async def test_a_spectator_on_a_slow_mobile_link_keeps_up_with_the_fight( rooms = HandsRoomManager( repository, config=RoomConfig( + style_select_seconds=0.0, engine_config=EngineConfig( rounds=1, round_ticks=1_000_000, countdown_ticks=1, flash_ko_enabled=False - ) + ), ), match_id_factory=lambda: "slow-link", ) @@ -770,12 +1022,16 @@ async def fight(socket: aiohttp.ClientWebSocketResponse) -> None: socket = await client.ws_connect( f"{base}/api/hands/ws", headers={"Origin": ORIGIN}, compress=15 ) - await socket.send_json({"version": 3, "type": "authenticate", "ticket": ticket}) + await socket.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": ticket} + ) fighters.append(asyncio.create_task(fight(socket))) spectator = await client.ws_connect( f"http://127.0.0.1:{proxy_port}/api/hands/ws", headers={"Origin": ORIGIN}, compress=15 ) - await spectator.send_json({"version": 3, "type": "authenticate", "ticket": "three"}) + await spectator.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "three"} + ) loop = asyncio.get_running_loop() started = loop.time() lag_ticks: list[int] = [] @@ -812,7 +1068,7 @@ async def test_a_fighter_whose_socket_stops_reading_is_paused_then_forfeits( assert server.bound_port is not None async with aiohttp.ClientSession() as client: one = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) - await one.send_json({"version": 3, "type": "authenticate", "ticket": "one"}) + await one.send_json({"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "one"}) await receive_until(one, "waiting") stalled, _frames = await connect_reader_that_stalls(server.bound_port, "two") try: @@ -843,7 +1099,7 @@ async def test_reconnecting_over_a_stalled_socket_does_not_hold_up_any_join( assert server.bound_port is not None async with aiohttp.ClientSession() as client: one = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) - await one.send_json({"version": 3, "type": "authenticate", "ticket": "one"}) + await one.send_json({"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "one"}) await receive_until(one, "waiting") stalled, frames = await connect_reader_that_stalls(server.bound_port, "two", read_frames=1) try: @@ -859,8 +1115,11 @@ async def test_reconnecting_over_a_stalled_socket_does_not_hold_up_any_join( # the pause is lasting rather than one overlapped write still in flight. transport = old_socket._request.transport assert transport is not None + # The server's own kernel send buffer is kept small too, so the stall builds in a + # fraction of a second however large the system would autotune it. + transport.get_extra_info("socket").setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 4096) transport.set_write_buffer_limits(high=1024) - async with asyncio.timeout(10): + async with asyncio.timeout(30): while not ( # noqa: ASYNC110 old_socket._request.protocol.writing_paused and transport.get_write_buffer_size() > SNAPSHOT_BACKLOG_BYTES @@ -872,9 +1131,15 @@ async def test_reconnecting_over_a_stalled_socket_does_not_hold_up_any_join( ) elsewhere = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) await replacement.send_json( - {"version": 3, "type": "authenticate", "ticket": frames[0]["reconnect_ticket"]} + { + "version": PROTOCOL_VERSION, + "type": "authenticate", + "ticket": frames[0]["reconnect_ticket"], + } + ) + await elsewhere.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "elsewhere"} ) - await elsewhere.send_json({"version": 3, "type": "authenticate", "ticket": "elsewhere"}) welcome = await receive_until(replacement, "welcome", deadline_seconds=2) assert welcome["player_id"] == "two" await receive_until(replacement, "resumed", deadline_seconds=2) @@ -982,7 +1247,7 @@ async def test_http_admission_is_per_caller_and_forwarded_headers_require_trust( spoofed = await post_bootstrap( client, direct_base, - "two", + "one", headers={"Origin": ORIGIN, "X-Forwarded-For": "203.0.113.2"}, ) assert first.status == 200 @@ -1011,7 +1276,7 @@ async def test_http_admission_is_per_caller_and_forwarded_headers_require_trust( forged_left = await post_bootstrap( client, trusted_base, - "two", + "one", headers={ "Origin": ORIGIN, "X-Forwarded-For": "192.0.2.99, 203.0.113.10", @@ -1036,6 +1301,46 @@ async def test_http_admission_is_per_caller_and_forwarded_headers_require_trust( await trusted.close() +async def test_players_behind_one_proxy_address_are_limited_per_activity_instance( + repository: Repository, +) -> None: + # Discord's Activity proxy hides players' addresses: every request arrives from its address. + server, _auth, base = await start_server( + repository, + admission=AdmissionConfig( + request_limit=10, + per_caller_request_limit=1, + request_window_seconds=60, + trusted_proxy_cidrs=("127.0.0.0/8", "::1/128"), + ), + ) + proxy = {"Origin": ORIGIN, "X-Forwarded-For": "162.159.0.1"} + async with aiohttp.ClientSession() as client: + statuses = [ + (await post_bootstrap(client, base, instance, headers=proxy)).status + for instance in ("voice-a", "voice-b", "voice-a") + ] + # One voice channel's sign-ins cannot crowd out another's, and each is still limited. + assert statuses == [200, 200, 429] + + async def token(state: str) -> int: + response = await client.post( + f"{base}/api/hands/token", + json={"code": "code", "state": state}, + headers=proxy, + ) + return response.status + + # The exchange is scoped through the state the server issued for an instance; a state it + # never issued names nothing and counts against the address. + assert await token("oauth-state") == 200 + assert await token("other-state") == 401 + assert await token("oauth-state") == 429 + assert await token("forged-one") == 401 + assert await token("forged-two") == 429 + await server.close() + + async def test_per_caller_window_capacity_purges_expired_callers( repository: Repository, ) -> None: @@ -1074,7 +1379,7 @@ async def bootstrap(caller: str, instance: str) -> int: assert len(server._bootstrap_caller_limit._requests) == 2 clock.value = 11 assert await bootstrap("203.0.113.3", "three") == 200 - assert list(server._bootstrap_caller_limit._requests) == ["203.0.113.3"] + assert list(server._bootstrap_caller_limit._requests) == ["203.0.113.3 three"] await server.close() @@ -1121,7 +1426,7 @@ async def begin(self, instance_id: object): same = await post_bootstrap( client, base, - "same", + "one", headers={"Origin": ORIGIN, "X-Forwarded-For": "203.0.113.1"}, ) other = asyncio.create_task( @@ -1161,11 +1466,15 @@ async def test_per_caller_websocket_auth_slots_release_and_isolate_callers( assert caught.value.status == 503 other = await client.ws_connect(f"{base}/api/hands/ws", headers=headers_two) - await other.send_json({"version": 3, "type": "authenticate", "ticket": "bad"}) + await other.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "bad"} + ) assert json.loads((await other.receive(timeout=1)).data)["code"] == "invalid_ticket" await other.close() - await first.send_json({"version": 3, "type": "authenticate", "ticket": "bad"}) + await first.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "bad"} + ) assert json.loads((await first.receive(timeout=1)).data)["code"] == "invalid_ticket" await first.close() replacement = await client.ws_connect(f"{base}/api/hands/ws", headers=headers_one) @@ -1215,13 +1524,17 @@ async def begin(self, instance_id: object): with pytest.raises(aiohttp.WSServerHandshakeError) as caught: await client.ws_connect(f"{base}/api/hands/ws", headers=headers) assert caught.value.status == 503 - await first_ws.send_json({"version": 3, "type": "authenticate", "ticket": "bad"}) + await first_ws.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "bad"} + ) await first_ws.receive(timeout=1) await first_ws.close() auth.tickets["valid"] = AuthenticatedPlayer("one", GUILD, "room", "One", None) available = await client.ws_connect(f"{base}/api/hands/ws", headers=headers) - await available.send_json({"version": 3, "type": "authenticate", "ticket": "valid"}) + await available.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "valid"} + ) welcome = await available.receive(timeout=1) assert json.loads(welcome.data)["type"] == "welcome" await available.close() @@ -1234,13 +1547,17 @@ async def test_ticket_replay_does_not_replace_live_socket(repository: Repository server, _auth, base = await start_server(repository, auth=auth) async with aiohttp.ClientSession() as client: live = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) - await live.send_json({"version": 3, "type": "authenticate", "ticket": "one-use"}) + await live.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "one-use"} + ) welcome = json.loads((await live.receive(timeout=1)).data) assert welcome["type"] == "welcome" assert welcome["reconnect_ticket"].startswith("rotated-") replay = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) - await replay.send_json({"version": 3, "type": "authenticate", "ticket": "one-use"}) + await replay.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "one-use"} + ) error = json.loads((await replay.receive(timeout=1)).data) assert error["code"] == "invalid_ticket" await replay.close() @@ -1301,7 +1618,9 @@ def activate_ticket(self, ticket: object, player: AuthenticatedPlayer) -> None: base = f"http://127.0.0.1:{server.bound_port}" async with aiohttp.ClientSession() as client: live = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) - await live.send_json({"version": 3, "type": "authenticate", "ticket": initial_ticket}) + await live.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": initial_ticket} + ) welcome = json.loads((await live.receive(timeout=1)).data) original_rotation = welcome["reconnect_ticket"] @@ -1315,7 +1634,7 @@ def activate_ticket(self, ticket: object, player: AuthenticatedPlayer) -> None: assert refreshed_ticket != original_rotation await live.send_json( { - "version": 3, + "version": PROTOCOL_VERSION, "type": "ticket_ack", "refresh_id": refreshed["refresh_id"], } @@ -1329,7 +1648,7 @@ def activate_ticket(self, ticket: object, player: AuthenticatedPlayer) -> None: replacement = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) await replacement.send_json( - {"version": 3, "type": "authenticate", "ticket": refreshed_ticket} + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": refreshed_ticket} ) replacement_welcome = json.loads((await replacement.receive(timeout=1)).data) assert replacement_welcome["type"] == "welcome" @@ -1354,9 +1673,11 @@ async def test_unexpected_post_upgrade_failure_closes_with_generic_error( auth.tickets["valid"] = AuthenticatedPlayer("one", GUILD, "room", "One", None) async with aiohttp.ClientSession() as client: socket = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) - await socket.send_json({"version": 3, "type": "authenticate", "ticket": "valid"}) + await socket.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "valid"} + ) error = json.loads((await socket.receive(timeout=1)).data) - assert error == {"code": "internal_error", "type": "error", "version": 3} + assert error == {"code": "internal_error", "type": "error", "version": PROTOCOL_VERSION} assert socket.closed or (await socket.receive(timeout=1)).type in { aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED, @@ -1442,3 +1763,185 @@ async def test_request_body_limit_and_clean_shutdown(repository: Repository) -> await server.close() assert not server.running assert auth.closed + + +async def _bout_against_the_computer( + client: aiohttp.ClientSession, base: str, auth: FakeAuth, name: str +) -> aiohttp.ClientWebSocketResponse: + auth.tickets[name] = AuthenticatedPlayer(name, GUILD, f"room-{name}", name, None) + ws = await client.ws_connect(f"{base}/api/hands/ws", headers={"Origin": ORIGIN}) + await ws.send_json({"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": name}) + async with asyncio.timeout(2): + while json.loads((await ws.receive()).data)["type"] != "waiting": + pass + await ws.send_json({"version": PROTOCOL_VERSION, "type": "cpu", "level": "rookie"}) + async with asyncio.timeout(2): + while json.loads((await ws.receive()).data)["type"] != "select": + pass + # The computer picks its style at once; the bout starts as soon as this fighter confirms one. + await ws.send_json( + {"version": PROTOCOL_VERSION, "type": "style", "style": "balanced", "ready": True} + ) + async with asyncio.timeout(2): + while json.loads((await ws.receive()).data)["type"] != "ready": + pass + return ws + + +async def _errors_until_closed(ws: aiohttp.ClientWebSocketResponse, seconds: float) -> list[str]: + errors: list[str] = [] + try: + async with asyncio.timeout(seconds): + while True: + message = await ws.receive() + if message.type != aiohttp.WSMsgType.TEXT: + errors.append("closed") + return errors + payload = json.loads(message.data) + if payload["type"] == "error": + errors.append(payload["code"]) + except TimeoutError: + errors.append("open") + return errors + + +def _cpu_bout_rooms(repository: Repository) -> HandsRoomManager: + return HandsRoomManager( + repository, + config=RoomConfig( + tick_interval_seconds=0.01, + reconnect_grace_seconds=1.0, + result_hold_seconds=0.05, + engine_config=EngineConfig( + rounds=1, round_ticks=50_000, rest_ticks=0, countdown_ticks=1 + ), + ), + ) + + +async def test_a_flood_of_computer_requests_is_cut_off_like_a_flood_of_inputs( + repository: Repository, +) -> None: + auth = FakeAuth() + server, _auth, base = await start_server( + repository, auth=auth, rooms=_cpu_bout_rooms(repository) + ) + async with aiohttp.ClientSession() as client: + repeat = await _bout_against_the_computer(client, base, auth, "repeat") + await repeat.send_json({"version": PROTOCOL_VERSION, "type": "cpu", "level": "champion"}) + assert await _errors_until_closed(repeat, 0.3) == ["open"] + await repeat.close() + + flooder = await _bout_against_the_computer(client, base, auth, "flooder") + frame = json.dumps({"version": PROTOCOL_VERSION, "type": "cpu", "level": "champion"}) + for _ in range(4000): + await flooder.send_str(frame) + assert await _errors_until_closed(flooder, 2.0) == ["rate_limited", "closed"] + async with asyncio.timeout(1): + await server.close() + + +async def test_a_fighter_who_vanishes_without_a_close_pauses_the_bout_within_the_heartbeat( + repository: Repository, +) -> None: + # In production a silent drop pauses the bout about 7.5 s after the fighter's last frame. + assert server_module.WEBSOCKET_HEARTBEAT_SECONDS == 5.0 + heartbeat = 0.2 + auth = FakeAuth() + rooms = _cpu_bout_rooms(repository) + server, _auth, base = await start_server( + repository, auth=auth, rooms=rooms, websocket_heartbeat=heartbeat + ) + async with aiohttp.ClientSession() as client: + # A page that sends nothing (blurred, hidden) but whose browser answers pings stays. + idle = await _bout_against_the_computer(client, base, auth, "idle") + deadline = asyncio.get_running_loop().time() + 6 * heartbeat + while asyncio.get_running_loop().time() < deadline: + message = await idle.receive(timeout=1) + assert message.type == aiohttp.WSMsgType.TEXT + await idle.close() + + auth.tickets["vanish"] = AuthenticatedPlayer("vanish", GUILD, "room-vanish", "V", None) + vanish = await client.ws_connect( + f"{base}/api/hands/ws", headers={"Origin": ORIGIN}, autoping=False + ) + await vanish.send_json( + {"version": PROTOCOL_VERSION, "type": "authenticate", "ticket": "vanish"} + ) + for request, until in ( + ({"version": PROTOCOL_VERSION, "type": "cpu", "level": "rookie"}, "select"), + ( + {"version": PROTOCOL_VERSION, "type": "style", "style": "boxer", "ready": True}, + "ready", + ), + ): + await vanish.send_json(request) + async with asyncio.timeout(2): + while True: + message = await vanish.receive() + text = message.data if message.type == aiohttp.WSMsgType.TEXT else "{}" + if json.loads(text).get("type") == until: + break + room = rooms._rooms["room-vanish"] + engine = room.engine + assert engine is not None + # The fighter's connection goes quiet: no frames, no pongs, no close. + silent_since = asyncio.get_running_loop().time() + async with asyncio.timeout(3): + while room._slots["vanish"].connection is not None: # noqa: ASYNC110 + await asyncio.sleep(0.01) + noticed_after = asyncio.get_running_loop().time() - silent_since + assert noticed_after < 1.5 * heartbeat + 0.3 + paused_at = engine.tick + await asyncio.sleep(0.1) + assert engine.tick == paused_at + await vanish.close() + async with asyncio.timeout(1): + await server.close() + + +async def test_each_frame_is_decoded_once( + repository: Repository, monkeypatch: pytest.MonkeyPatch +) -> None: + from intelstream.hands import protocol + + decoded: list[object] = [] + arrived = asyncio.Event() + + def counting(frame: str | bytes, **kwargs: object) -> object: + # Every JSON parse the protocol module makes, whichever function makes it. + decoded.append(frame) + arrived.set() + return json.loads(frame, **kwargs) # type: ignore[arg-type] + + monkeypatch.setattr( + protocol, + "json", + SimpleNamespace(loads=counting, dumps=json.dumps, JSONDecodeError=json.JSONDecodeError), + ) + auth = FakeAuth() + server, _auth, base = await start_server( + repository, auth=auth, rooms=_cpu_bout_rooms(repository) + ) + async with aiohttp.ClientSession() as client: + fighter = await _bout_against_the_computer(client, base, auth, "once") + before = len(decoded) + arrived.clear() + await fighter.send_json( + { + "version": PROTOCOL_VERSION, + "type": "input", + "sequence": 0, + "client_tick": 0, + "move": {"x": 1000, "y": 0}, + "defense": "none", + "actions": [], + } + ) + async with asyncio.timeout(1): + await arrived.wait() + await asyncio.sleep(0.05) + assert len(decoded) == before + 1 + await fighter.close() + async with asyncio.timeout(1): + await server.close() diff --git a/tests/test_hands/test_styles.py b/tests/test_hands/test_styles.py new file mode 100644 index 00000000..2ae7a005 --- /dev/null +++ b/tests/test_hands/test_styles.py @@ -0,0 +1,448 @@ +from __future__ import annotations + +import random +from math import isqrt + +import pytest + +from intelstream.hands.engine import BoxingEngine, EngineConfig +from intelstream.hands.protocol import encode_snapshot +from intelstream.hands.rules import PUNCH_RULES, STYLE_RULES, StyleRule, style_punch_rule +from intelstream.hands.types import ( + ActionKind, + CombatEvent, + DefensivePose, + FighterStyle, + Hand, + InputCommand, + MovementAction, + Power, + PunchAction, + PunchClass, + Target, +) + +BALANCED = FighterStyle.BALANCED +BOXER = FighterStyle.BOXER +SLUGGER = FighterStyle.SLUGGER +SWARMER = FighterStyle.SWARMER +COUNTER = FighterStyle.COUNTER_PUNCHER + + +def styled_engine( + one: FighterStyle = BALANCED, two: FighterStyle = BALANCED, *, gap: int = 90 +) -> BoxingEngine: + engine = BoxingEngine( + match_id="match-styles", + activity_instance_id="instance-1", + guild_id="guild-1", + player_one_id="one", + player_two_id="two", + seed=7, + config=EngineConfig( + rounds=1, round_ticks=3000, rest_ticks=0, countdown_ticks=0, flash_ko_enabled=False + ), + styles=(one, two), + ) + engine.fighter("one").x = -gap // 2 + engine.fighter("two").x = gap // 2 + return engine + + +def command( + sequence: int, + *, + action: PunchAction | MovementAction | None = None, + defense: DefensivePose = DefensivePose.NONE, + move_x: int = 0, +) -> InputCommand: + return InputCommand( + sequence=sequence, + client_tick=sequence, + move_x=move_x, + defense=defense, + actions=(action,) if action is not None else (), + ) + + +def punch( + punch_class: PunchClass = PunchClass.STRAIGHT, + *, + hand: Hand = Hand.RIGHT, + target: Target = Target.HEAD, + power: Power = Power.NORMAL, +) -> PunchAction: + return PunchAction(hand, punch_class, target, power) + + +def first_event(engine: BoxingEngine, kinds: set[str], limit: int = 120) -> CombatEvent: + for _ in range(limit): + for event in engine.step().events: + if event.kind in kinds: + return event + raise AssertionError(f"none of {kinds} emitted") + + +def landed(attacker: FighterStyle, action: PunchAction, defender: FighterStyle = BALANCED) -> int: + engine = styled_engine(attacker, defender) + engine.step({"one": command(1, action=action)}) + return first_event(engine, {"hit", "counter_hit"}).amount + + +def test_every_style_is_in_the_manifest_and_balanced_changes_nothing() -> None: + assert set(STYLE_RULES) == set(FighterStyle) + assert STYLE_RULES[BALANCED] == StyleRule() + engine = BoxingEngine( + match_id="m", + activity_instance_id="i", + guild_id="g", + player_one_id="one", + player_two_id="two", + seed=1, + ) + assert engine.fighter("one").style is BALANCED + assert engine.fighter("two").style is BALANCED + + +@pytest.mark.parametrize( + ("style", "punch_class", "startup_delta", "recovery_delta"), + [ + (BOXER, PunchClass.JAB, 0, -1), + (BOXER, PunchClass.STRAIGHT, 0, 0), + (BOXER, PunchClass.HOOK, 0, 0), + (SLUGGER, PunchClass.HOOK, 0, 1), + (SLUGGER, PunchClass.UPPERCUT, 0, 1), + (SLUGGER, PunchClass.JAB, 0, 0), + (SWARMER, PunchClass.HOOK, -1, 0), + (SWARMER, PunchClass.UPPERCUT, -1, 0), + (SWARMER, PunchClass.STRAIGHT, 0, 0), + (COUNTER, PunchClass.STRAIGHT, 0, 0), + ], +) +def test_a_style_changes_the_timing_of_its_own_punches( + style: FighterStyle, punch_class: PunchClass, startup_delta: int, recovery_delta: int +) -> None: + def timing(fighter_style: FighterStyle) -> tuple[int, int]: + engine = styled_engine(fighter_style) + engine.step({"one": command(1, action=punch(punch_class))}) + attack = engine.fighter("one").attack + assert attack is not None + return attack.rule.startup, attack.rule.recovery + + base_startup, base_recovery = timing(BALANCED) + startup, recovery = timing(style) + assert startup - base_startup == startup_delta + assert recovery - base_recovery == recovery_delta + + +def test_a_boxer_reaches_further() -> None: + def reach(style: FighterStyle) -> int: + engine = styled_engine(style) + engine.step({"one": command(1, action=punch(PunchClass.JAB, hand=Hand.LEFT))}) + attack = engine.fighter("one").attack + assert attack is not None + return attack.rule.reach + + base = PUNCH_RULES[(PunchClass.JAB, Target.HEAD, Power.NORMAL)].reach + assert reach(BALANCED) == base + assert reach(BOXER) == base * 102 // 100 + assert reach(SWARMER) == base + + # A jab that falls just short of a balanced fighter's reach still lands for a boxer. + gap = base + 2 + for style, expected in ((BALANCED, "whiff"), (BOXER, "hit")): + engine = styled_engine(style, gap=gap) + engine.step({"one": command(1, action=punch(PunchClass.JAB, hand=Hand.LEFT))}) + assert first_event(engine, {"hit", "whiff"}).kind == expected + + +def test_a_slugger_hits_harder_and_the_others_softer() -> None: + straight = punch(PunchClass.STRAIGHT) + balanced = landed(BALANCED, straight) + assert landed(SLUGGER, straight) > balanced + assert landed(SWARMER, straight) == landed(COUNTER, straight) < landed(BOXER, straight) + assert landed(BOXER, straight) < balanced + + +def test_a_swarmer_works_the_body_harder_than_the_head() -> None: + body = punch(PunchClass.HOOK, target=Target.BODY) + head = punch(PunchClass.HOOK) + assert landed(SWARMER, body) > landed(BALANCED, body) + assert landed(SWARMER, head) < landed(BALANCED, head) + + +def test_a_slugger_pays_more_for_a_punch_and_a_swarmer_tires_less() -> None: + def spent(style: FighterStyle) -> tuple[int, int]: + engine = styled_engine(style) + fighter = engine.fighter("one") + stamina, conditioning = fighter.stamina, fighter.conditioning + engine.step({"one": command(1, action=punch(PunchClass.UPPERCUT, power=Power.POWER))}) + return stamina - fighter.stamina, conditioning - fighter.conditioning + + base_cost = PUNCH_RULES[(PunchClass.UPPERCUT, Target.HEAD, Power.POWER)].stamina_cost + balanced_stamina, balanced_conditioning = spent(BALANCED) + slugger_stamina, _ = spent(SLUGGER) + _, swarmer_conditioning = spent(SWARMER) + # One tick of recovery is folded into each figure; it is the same for all three. + price = STYLE_RULES[SLUGGER].stamina_cost_percent + assert price > 100 + assert slugger_stamina - balanced_stamina == base_cost * price // 100 - base_cost + assert swarmer_conditioning < balanced_conditioning + + +def test_a_miss_costs_a_slugger_more_and_tires_a_swarmer_less() -> None: + def whiffed(style: FighterStyle) -> tuple[int, int]: + engine = styled_engine(style, gap=400) + fighter = engine.fighter("one") + engine.step({"one": command(1, action=punch(PunchClass.HOOK, power=Power.POWER))}) + for _ in range(60): + conditioning = fighter.conditioning + for event in engine.step().events: + if event.kind == "whiff": + return event.amount, conditioning - fighter.conditioning + raise AssertionError("the hook never missed") + + base_whiff = PUNCH_RULES[(PunchClass.HOOK, Target.HEAD, Power.POWER)].whiff_cost + balanced_cost, balanced_conditioning = whiffed(BALANCED) + slugger_cost, _ = whiffed(SLUGGER) + _, swarmer_conditioning = whiffed(SWARMER) + assert balanced_cost == base_whiff + assert slugger_cost == base_whiff * STYLE_RULES[SLUGGER].stamina_cost_percent // 100 + assert swarmer_conditioning < balanced_conditioning + + +def _conditioning_spent(style: FighterStyle, jabs: int, *, stamina: int, gap: int) -> int: + """Conditioning a fighter of `style` spends on `jabs` lead jabs, each thrown with `stamina`.""" + engine = styled_engine(style, gap=gap) + fighter = engine.fighter("one") + before = fighter.conditioning + for sequence in range(1, jabs + 1): + fighter.stamina = stamina + engine.fighter("two").poise = 600 + engine.step({"one": command(sequence, action=punch(PunchClass.JAB, hand=Hand.LEFT))}) + while fighter.attack is not None: + engine.step() + return before - fighter.conditioning + + +def test_a_swarmer_keeps_its_whole_conditioning_saving_however_little_each_punch_costs() -> None: + saving = STYLE_RULES[SWARMER].conditioning_loss_percent + assert saving < 100 + # Sixty whiffed jabs: three points to throw each and two for the miss, for a balanced fighter. + balanced = _conditioning_spent(BALANCED, 60, stamina=1000, gap=400) + assert balanced == 60 * (3 + 2) + assert ( + abs(_conditioning_spent(SWARMER, 60, stamina=1000, gap=400) - balanced * saving // 100) <= 1 + ) + # Arm punches out of breath cost a point each, and the swarmer his share of it, not nothing. + tired = _conditioning_spent(BALANCED, 30, stamina=10, gap=90) + assert tired == 30 + assert abs(_conditioning_spent(SWARMER, 30, stamina=10, gap=90) - tired * saving // 100) <= 1 + + +def test_a_combination_waits_out_a_punch_the_style_cannot_yet_pay_for() -> None: + straight = (PunchClass.STRAIGHT, Target.HEAD, Power.NORMAL) + base_cost = PUNCH_RULES[straight].stamina_cost + slugger_cost = style_punch_rule(PUNCH_RULES[straight], STYLE_RULES[SLUGGER]).stamina_cost + assert base_cost < slugger_cost - 3 + + def jab_ran_its_course(style: FighterStyle) -> bool: + engine = styled_engine(style) + fighter = engine.fighter("one") + engine.step({"one": command(1, action=punch(PunchClass.JAB, hand=Hand.LEFT))}) + engine.step({"one": command(2, action=punch(PunchClass.STRAIGHT))}) + jab = fighter.attack + assert jab is not None and fighter.pending_actions + while fighter.attack is jab: + # Enough for a balanced fighter's straight, not for a slugger's. + fighter.stamina = base_cost + engine.step() + assert jab.landed + return jab.age >= jab.total_ticks + + assert not jab_ran_its_course(BALANCED) + assert jab_ran_its_course(SLUGGER) + + +def test_a_slugger_breaks_a_man_down_faster_and_takes_it_better() -> None: + def poise_lost(attacker: FighterStyle, defender: FighterStyle) -> int: + engine = styled_engine(attacker, defender) + engine.step({"one": command(1, action=punch(PunchClass.HOOK))}) + before = engine.fighter("two").poise + first_event(engine, {"hit"}) + return before - engine.fighter("two").poise + + balanced = poise_lost(BALANCED, BALANCED) + assert poise_lost(SLUGGER, BALANCED) > balanced + assert poise_lost(BALANCED, SLUGGER) < balanced + + +def poise_and_damage(attacker: FighterStyle, *, counter: bool = False) -> tuple[int, int]: + """Poise and damage a straight from `attacker` takes off a balanced fighter.""" + engine = styled_engine(attacker, BALANCED) + if counter: + engine.fighter("one").counter_ticks = 30 + before = engine.fighter("two").poise + engine.step({"one": command(1, action=punch(PunchClass.STRAIGHT))}) + damage = first_event(engine, {"hit", "counter_hit"}).amount + return before - engine.fighter("two").poise, damage + + +@pytest.mark.parametrize("style", [BOXER, SLUGGER, SWARMER, COUNTER]) +def test_a_style_hits_as_hard_into_a_mans_legs_as_into_his_face(style: FighterStyle) -> None: + """A style's power is one number: the poise its punches take is scaled like their damage.""" + rule = STYLE_RULES[style] + assert rule.poise_damage_percent == rule.impact_percent + balanced_poise, balanced_damage = poise_and_damage(BALANCED) + poise, damage = poise_and_damage(style) + assert abs(poise * 100 // balanced_poise - damage * 100 // balanced_damage) <= 2 + + +def test_a_counter_punchers_counters_land_a_little_harder_but_not_into_the_legs_of_a_stronger_man() -> ( + None +): + balanced_poise, balanced_damage = poise_and_damage(BALANCED, counter=True) + poise, damage = poise_and_damage(COUNTER, counter=True) + assert balanced_damage < damage <= balanced_damage * 104 // 100 + assert balanced_poise <= poise <= balanced_poise * 104 // 100 + + +def test_a_swarmer_is_quicker_on_the_feet_and_a_slugger_slower() -> None: + def travelled(style: FighterStyle) -> int: + engine = styled_engine(style, gap=600) + start = engine.fighter("one").x + for sequence in range(1, 31): + engine.step({"one": command(sequence, move_x=1000)}) + return engine.fighter("one").x - start + + balanced = travelled(BALANCED) + quicker = STYLE_RULES[SWARMER].move_speed_percent + assert 100 < quicker <= 105 + assert balanced * (quicker - 1) // 100 <= travelled(SWARMER) <= balanced * (quicker + 1) // 100 + assert travelled(SLUGGER) < balanced * 98 // 100 + assert travelled(BOXER) == balanced + + +def test_a_quick_fighter_still_reports_a_velocity_clients_accept() -> None: + engine = styled_engine(SWARMER, gap=600) + for sequence in range(1, 31): + snapshot = engine.step({"one": command(sequence, move_x=1000)}) + fighter = snapshot.fighters[0] + assert -7 <= fighter.velocity_x <= 7 + assert -7 <= fighter.velocity_y <= 7 + + +def test_a_quick_fighter_pushed_onto_a_corner_pad_still_reports_a_velocity_clients_accept() -> None: + # The pad keeps the speed along it, which for a fresh swarmer is 7.7 units a tick. + pad = styled_engine(SWARMER, SWARMER).fighter("one") + pad.x, pad.y = 400, 340 + pad.velocity_fixed_x, pad.velocity_fixed_y = -7700, 0 + BoxingEngine._clamp_to_ring(pad) + assert (pad.velocity_x, pad.velocity_y) == (-7, 0) + assert pad.velocity_fixed_x == -7700 + + # Walking out of a corner while the other swarmer walks him back onto the pad. + engine = styled_engine(SWARMER, SWARMER) + one, two = engine.fighter("one"), engine.fighter("two") + one.x, one.y, two.x, two.y = -369, 364, -300, 330 + velocities = set() + for sequence in range(1, 80): + dx, dy = one.x - two.x, one.y - two.y + distance = max(1, isqrt(dx * dx + dy * dy)) + snapshot = engine.step( + { + "one": InputCommand(sequence, sequence, 1000, 0), + "two": InputCommand( + sequence, sequence, dx * 1000 // distance, dy * 1000 // distance + ), + } + ) + velocities.update((f.velocity_x, f.velocity_y) for f in snapshot.fighters) + assert max(max(abs(x), abs(y)) for x, y in velocities) == 7 + + # Random scrambles in every corner, both fighters fresh swarmers. + rng = random.Random(4205227423) + for _ in range(40): + engine = styled_engine(SWARMER, SWARMER) + one, two = engine.fighter("one"), engine.fighter("two") + sign_x, sign_y = rng.choice((-1, 1)), rng.choice((-1, 1)) + one.x, one.y = sign_x * rng.randrange(300, 420), sign_y * rng.randrange(300, 420) + two.x, two.y = ( + one.x - sign_x * rng.randrange(60, 120), + one.y - sign_y * rng.randrange(0, 80), + ) + for sequence in range(1, 45): + moves = [(rng.randrange(-1000, 1001), rng.randrange(-1000, 1001)) for _ in range(2)] + snapshot = engine.step( + { + "one": InputCommand(sequence, sequence, *moves[0]), + "two": InputCommand(sequence, sequence, *moves[1]), + } + ) + for fighter in snapshot.fighters: + assert -7 <= fighter.velocity_x <= 7 + assert -7 <= fighter.velocity_y <= 7 + + +def test_a_counter_puncher_makes_a_miss_cost_more() -> None: + def counter_after_a_slip(style: FighterStyle) -> tuple[int, int, int]: + engine = styled_engine(BALANCED, style) + engine.step( + { + "one": command(1, action=punch(PunchClass.STRAIGHT)), + "two": command(1, action=MovementAction(ActionKind.SLIP_LEFT)), + } + ) + slipping = engine.fighter("two").evasion_ticks + first_event(engine, {"evade"}) + window = engine.fighter("two").counter_ticks + engine.step({"two": command(2, action=punch(PunchClass.HOOK, hand=Hand.LEFT))}) + return slipping, window, first_event(engine, {"counter_hit"}).amount + + balanced_slip, balanced_window, balanced_counter = counter_after_a_slip(BALANCED) + counter_slip, counter_window, counter_counter = counter_after_a_slip(COUNTER) + assert counter_slip == balanced_slip + 1 + assert counter_window == balanced_window + 3 + assert counter_counter > balanced_counter + swarmer_slip, swarmer_window, _ = counter_after_a_slip(SWARMER) + assert swarmer_slip == balanced_slip + 1 + assert swarmer_window == balanced_window + + +def test_a_counter_puncher_has_longer_to_counter_after_a_parry() -> None: + def window_after_parry(style: FighterStyle) -> int: + engine = styled_engine(BALANCED, style) + engine.step({"one": command(1, action=punch(PunchClass.STRAIGHT, power=Power.POWER))}) + attack = engine.fighter("one").attack + assert attack is not None + while attack.age < attack.rule.startup - 2: + engine.step() + engine.step({"two": command(1, defense=DefensivePose.GUARD_HIGH)}) + assert first_event(engine, {"block", "perfect_block", "hit"}).kind == "perfect_block" + return engine.fighter("two").counter_ticks + + assert window_after_parry(COUNTER) == window_after_parry(BALANCED) + 3 + + +def test_a_counter_puncher_has_a_wider_perfect_block() -> None: + def blocked(style: FighterStyle, lead: int) -> str: + engine = styled_engine(BALANCED, style) + engine.step({"one": command(1, action=punch(PunchClass.STRAIGHT, power=Power.POWER))}) + attack = engine.fighter("one").attack + assert attack is not None + while attack.age < attack.rule.startup - lead: + engine.step() + engine.step({"two": command(1, defense=DefensivePose.GUARD_HIGH)}) + return first_event(engine, {"block", "perfect_block", "hit"}).kind + + def earliest_perfect(style: FighterStyle) -> int: + return max(lead for lead in range(2, 12) if blocked(style, lead) == "perfect_block") + + assert earliest_perfect(COUNTER) == earliest_perfect(BALANCED) + 1 + + +def test_the_snapshot_says_how_each_fighter_boxes() -> None: + engine = styled_engine(SLUGGER, COUNTER) + snapshot = engine.step() + assert [fighter.style for fighter in snapshot.fighters] == [SLUGGER, COUNTER] + assert '"style":"slugger"' in encode_snapshot(snapshot, viewer_id=None) diff --git a/web/hands/replays/golden.json b/web/hands/replays/golden.json index 5ce0278f..8537a7c4 100644 --- a/web/hands/replays/golden.json +++ b/web/hands/replays/golden.json @@ -16,12 +16,98 @@ { "marker": "knockdown", "tick": 120 + }, + { + "marker": "get_up", + "tick": 156 + }, + { + "marker": "box", + "tick": 420 + }, + { + "marker": "outflanked_straight_submit", + "tick": 499 + }, + { + "marker": "outflanked_whiff", + "tick": 506 + }, + { + "marker": "recovery_cancel", + "tick": 528 + }, + { + "marker": "rocked", + "tick": 558 + }, + { + "marker": "hit_inside_the_stun", + "tick": 581 + }, + { + "marker": "clinch", + "tick": 609 + }, + { + "marker": "referee_break", + "tick": 654 + }, + { + "marker": "foul", + "tick": 666 + }, + { + "marker": "resume", + "tick": 726 + }, + { + "marker": "clinch_before_the_bell", + "tick": 752 + }, + { + "marker": "round_end", + "tick": 762 + }, + { + "marker": "bell_breaks_the_clinch", + "tick": 762 + }, + { + "marker": "corner", + "tick": 764 + }, + { + "marker": "corner", + "tick": 764 + }, + { + "marker": "round_start", + "tick": 1002 + }, + { + "marker": "body_collapse", + "tick": 1094 + }, + { + "marker": "body_knockdown", + "tick": 1104 } ], "seed": 20260805, "setup": { + "round_one_ends_ten_ticks_into_the_second_clinch": true, + "two_body_and_stamina_set_before_the_body_hook": [ + 700, + 300 + ], + "two_poise_set_before_power_straight": 170, "two_poise_set_before_straight": 1 }, + "styles": { + "one": "boxer", + "two": "slugger" + }, "tick_rate": 30, "ticks": [ { @@ -29,7 +115,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "79766b22c72d9776f7e2722e7ff8c7e318509dcded3d36503b3aa70cb2c165d0", + "checksum": "d9789a56ca456a86fa99368c9d4baed27380c040d8a71cad7d90cd4b97b05e28", "events": [], "fighters": [ { @@ -47,6 +133,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -70,6 +157,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -102,6 +190,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -125,6 +214,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -150,7 +240,7 @@ "tick": 1 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 1 }, @@ -159,7 +249,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "de1a6e2c276b3a6021417d0c717893fd26625189512ed9b739cee3d3b6855c99", + "checksum": "daefec096549945c4c58d2a4c1ed938b413dd23a34f49f73fe3952509a4bd93e", "events": [], "fighters": [ { @@ -177,6 +267,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -200,6 +291,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -232,6 +324,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -255,6 +348,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -280,7 +374,7 @@ "tick": 2 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 2 }, @@ -289,7 +383,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "c03b865b0901eb4fcd4ebddccab47e7fe2707cc327346912fecff234207a8561", + "checksum": "409eeb6cf553e24918cd7ca2545ab4090dd6b0a2b4eb5654dc90a289033c6dd2", "events": [], "fighters": [ { @@ -307,6 +401,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -330,6 +425,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -362,6 +458,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -385,6 +482,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -410,7 +508,7 @@ "tick": 3 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 3 }, @@ -419,7 +517,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "33f624d69e5d7c950896629ff4e0398afbe78d5a6cb483f3bcd0d29f3ea3f2b1", + "checksum": "6df1932c5b256588ce0607bbe19fa1bc664770a5df788a08d4d7a11bf76d047b", "events": [], "fighters": [ { @@ -437,6 +535,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -460,6 +559,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -492,6 +592,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -515,6 +616,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -540,7 +642,7 @@ "tick": 4 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 4 }, @@ -549,7 +651,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "f826567353e5e4497f3b6c59e5d6290f743adbe2e5e5b71ccd3942282e4af137", + "checksum": "2b43724daaa07e474fe9cef5c85465ed884e488f333f975d005158316f4e2a49", "events": [], "fighters": [ { @@ -567,6 +669,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -590,6 +693,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -622,6 +726,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -645,6 +750,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -670,7 +776,7 @@ "tick": 5 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 5 }, @@ -679,7 +785,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "408e4343017ce544a0abdef19b185ef1eba98c3a434f407a53901519b32c4dcd", + "checksum": "29f44f0ed5f8098ce314913419737983c7311d7df22474ef6ef5fb75287f9dbb", "events": [], "fighters": [ { @@ -697,6 +803,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -720,6 +827,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -752,6 +860,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -775,6 +884,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -800,7 +910,7 @@ "tick": 6 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 6 }, @@ -809,7 +919,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "102381786e04e1a7ddc9b2fd69f03953a7c723e8d41bd254f9e8c30253ec51ee", + "checksum": "a36a3f815da5ec92f75a019162cca749437cad8fb48d66fefe8273851751b632", "events": [], "fighters": [ { @@ -827,6 +937,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -850,6 +961,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -882,6 +994,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -905,6 +1018,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -930,7 +1044,7 @@ "tick": 7 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 7 }, @@ -939,7 +1053,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "9405b93a294fadc4acf97ae76e91bcbdb67eb304b1f01cb4329463d4748ab548", + "checksum": "a113a29082d343968ba81665271c58405bc978e2e22e070cc0977a699beeeedf", "events": [], "fighters": [ { @@ -957,6 +1071,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -980,6 +1095,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -1012,6 +1128,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -1035,6 +1152,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -1060,7 +1178,7 @@ "tick": 8 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 8 }, @@ -1069,7 +1187,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "1c589aae6813fc2a99c3942e4814f3219c7bca06db638dd8461bd5d58188b65d", + "checksum": "76727315a7f09cf2ed0262f7e063d1fae2f3ce945004c374f348bd8b67963e11", "events": [], "fighters": [ { @@ -1087,6 +1205,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -1110,6 +1229,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -1142,6 +1262,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -1165,6 +1286,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -1190,7 +1312,7 @@ "tick": 9 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 9 }, @@ -1199,7 +1321,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "16a339c5a15ea09a53e450bfd1436ea3d8920259da235ae13ff1706dbf9e3cb9", + "checksum": "4c8227171d63bc09d17750721090127727666bb785d273897ab0aa1bfde8f3f3", "events": [], "fighters": [ { @@ -1217,6 +1339,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -1240,6 +1363,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -1272,6 +1396,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -1295,6 +1420,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -1320,7 +1446,7 @@ "tick": 10 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 10 }, @@ -1329,7 +1455,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "954e0c472d5165c57f6c88810724749e85e5af2f26153901a4faa4107b3a3dcb", + "checksum": "188297f9fc9c0efbdc8732fe241acf4114aa3b1dad6f2874dd30a10a8f7f4797", "events": [], "fighters": [ { @@ -1347,6 +1473,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -1370,6 +1497,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -1402,6 +1530,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -1425,6 +1554,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -1450,7 +1580,7 @@ "tick": 11 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 11 }, @@ -1459,7 +1589,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "30a7e98a7148919981057de0372c7d10fe3b73f07434c8bb072ebb4cd72db05b", + "checksum": "1d0d845ebb1eff36170d599c26f0c5f4e69feac6af72cf4d775ff0676701e713", "events": [], "fighters": [ { @@ -1477,6 +1607,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -1500,6 +1631,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -1532,6 +1664,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -1555,6 +1688,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -1580,7 +1714,7 @@ "tick": 12 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 12 }, @@ -1589,7 +1723,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "f8b6686328491f8f2ee6499c131b6c7dedc418b9393552100500324be5cd0d39", + "checksum": "40284769563e6f175671645bd48f0f6ca79dc188c37811f2b22047e2fb2a08e3", "events": [], "fighters": [ { @@ -1607,6 +1741,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -1630,6 +1765,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -1662,6 +1798,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -1685,6 +1822,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -1710,7 +1848,7 @@ "tick": 13 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 13 }, @@ -1719,7 +1857,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "164f0b135e9e3041870194a459b8dd5d9e664d547c84bf6b877ee6984f05ecd7", + "checksum": "71d9162b9f4e58eadd66d35bcd92614399713db74a34d1e6949f8f4bb823af0f", "events": [], "fighters": [ { @@ -1737,6 +1875,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -1760,6 +1899,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -1792,6 +1932,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -1815,6 +1956,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -1840,7 +1982,7 @@ "tick": 14 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 14 }, @@ -1849,7 +1991,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "8526e35d8af8036b64fab2a6c878ccb0412c146bd15d5db16812148d999fa401", + "checksum": "3c282f8aa5c601d37c7d8e1613507a7fb99050caaa68dad31faa20fcc79f43ba", "events": [], "fighters": [ { @@ -1867,6 +2009,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -1890,6 +2033,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -1922,6 +2066,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -1945,6 +2090,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -1970,7 +2116,7 @@ "tick": 15 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 15 }, @@ -1979,7 +2125,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "5541ee70656c419eae45bddf454db3d83bccd45a13487aca2fad3193f44b5569", + "checksum": "094388c0dc69dd433a019d9ee1e28930ae70423a128c02ca8785c8d0a5bdfb6c", "events": [], "fighters": [ { @@ -1997,6 +2143,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -2020,6 +2167,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -2052,6 +2200,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -2075,6 +2224,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -2100,7 +2250,7 @@ "tick": 16 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 16 }, @@ -2109,7 +2259,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "f804aa1e1062343517297df7fbedf374fb682fac8b11a393f4d38a71a51d1482", + "checksum": "fde8831d318604377ac234b71a5d59fa07fdb66e438c958c6bc12d1b29472b36", "events": [], "fighters": [ { @@ -2127,6 +2277,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -2150,6 +2301,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -2182,6 +2334,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -2205,6 +2358,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -2230,7 +2384,7 @@ "tick": 17 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 17 }, @@ -2239,7 +2393,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "3ce17735cdf58c05ee1afabfd8daae827264a9450675d9a3197801eb5b7bed1c", + "checksum": "2eae50211a9e34d14998ee82640b2f7682ba6469146f47a550e638a6e475a957", "events": [], "fighters": [ { @@ -2257,6 +2411,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -2280,6 +2435,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -2312,6 +2468,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -2335,6 +2492,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -2360,7 +2518,7 @@ "tick": 18 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 18 }, @@ -2369,7 +2527,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "baf088cff943a91d2d9aef390fdd09c77461c94a8620e7fcac631adc375a79d4", + "checksum": "4df805f77861d8bed4fe08c249f8ab9980a58eb7e5606f2837fc2f7566714b00", "events": [], "fighters": [ { @@ -2387,6 +2545,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -2410,6 +2569,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -2442,6 +2602,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -2465,6 +2626,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -2490,7 +2652,7 @@ "tick": 19 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 19 }, @@ -2499,7 +2661,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "2abab3e818918bdd6e6613050c3f4cdaaa6658800749a3e728042e6f5666585a", + "checksum": "94ab3fb42c6ebcd9578099d52767ec643926038156bd762866d5c071cc417dd8", "events": [], "fighters": [ { @@ -2517,6 +2679,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -2540,6 +2703,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -2572,6 +2736,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -2595,6 +2760,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -2620,7 +2786,7 @@ "tick": 20 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 20 }, @@ -2629,7 +2795,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "61e4050b0df3fbd050ec384d0f9648bc9b52f0554a221ac8102d07e642141d04", + "checksum": "2802cb8d9037d25536c48a5279837ab5e46f682a4d4fbf2a6b12234e877810ae", "events": [], "fighters": [ { @@ -2647,6 +2813,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -2670,6 +2837,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -2702,6 +2870,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -2725,6 +2894,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -2750,7 +2920,7 @@ "tick": 21 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 21 }, @@ -2759,7 +2929,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "0b5768384f832314617b54a646753004c86ac5970206beaab0f140d40b433464", + "checksum": "89ebf62f7a1837e5c833b8ae3bad15a26389e04631ca1470f14f74415a4d49d5", "events": [], "fighters": [ { @@ -2777,6 +2947,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -2800,6 +2971,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -2832,6 +3004,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -2855,6 +3028,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -2880,7 +3054,7 @@ "tick": 22 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 22 }, @@ -2889,7 +3063,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "1f54d3fdc91c9ac4c9d834806cece2dda9adc3b2e8ce49488129ad8ece1d0936", + "checksum": "6d0fc19e50e1d6a1b78ac82c0d84a80eb246d908787f91620d02fcf366227447", "events": [], "fighters": [ { @@ -2907,6 +3081,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -2930,6 +3105,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -2962,6 +3138,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -2985,6 +3162,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -3010,7 +3188,7 @@ "tick": 23 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 23 }, @@ -3019,7 +3197,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "6012f68de42ca0475f9507a8a2e292e9921d02ea98801f90ddc7fac9256fc75b", + "checksum": "32e0613dd3e401e34c1774fb9fc572121db954842f67ce07d55e89f311189f7b", "events": [], "fighters": [ { @@ -3037,6 +3215,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -3060,6 +3239,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -3092,6 +3272,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -3115,6 +3296,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -3140,7 +3322,7 @@ "tick": 24 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 24 }, @@ -3149,7 +3331,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "0ae8208503c82bc812fee8b8ecded926188c0acb3daf736ec5ba755c457fe40e", + "checksum": "fd8b168294773129fff85a89c72218d7479feb31f3be02df00f91b22c27c0adc", "events": [], "fighters": [ { @@ -3167,6 +3349,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -3190,6 +3373,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -3222,6 +3406,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -3245,6 +3430,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -3270,7 +3456,7 @@ "tick": 25 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 25 }, @@ -3279,7 +3465,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "ff32f2e05d0b020e07e85b21971aa98f3ee25cd8111a65832791f2de5f73d41e", + "checksum": "475a31f7657d9a3bb51228ab5ed4fc7618d7fb66ee4e010a1f9e7eadd7e88392", "events": [], "fighters": [ { @@ -3297,6 +3483,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -3320,6 +3507,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -3352,6 +3540,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -3375,6 +3564,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -3400,7 +3590,7 @@ "tick": 26 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 26 }, @@ -3409,7 +3599,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "c78c09a8900b71dd264f06acade430fb75f2fd9862719f75337343e2859859ee", + "checksum": "3007ad5de5cc6e56bae30d70a06134ec88d08a1852bbe252d6872bc77f706ec5", "events": [], "fighters": [ { @@ -3427,6 +3617,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -3450,6 +3641,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -3482,6 +3674,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -3505,6 +3698,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -3530,7 +3724,7 @@ "tick": 27 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 27 }, @@ -3539,7 +3733,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "a888bff8cb907683f895b0c26a3992df56a5e5e515ad9024c99c3c32e9fb7789", + "checksum": "05dc621b03e224ee58fcc31a592917ce3680ea6d548e0e3cf2fdbd86600d67dd", "events": [], "fighters": [ { @@ -3557,6 +3751,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -3580,6 +3775,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -3612,6 +3808,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -3635,6 +3832,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -3660,7 +3858,7 @@ "tick": 28 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 28 }, @@ -3669,7 +3867,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "2ec19283887ec620bc09c1dab49bcf6f270babe5e5790ef2bd4862985b33922a", + "checksum": "b3451708614c159ad5275ef32f4c2b207b663f553fb344f4ce682b641d198242", "events": [], "fighters": [ { @@ -3687,6 +3885,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -3710,6 +3909,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -3742,6 +3942,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -3765,6 +3966,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -3790,7 +3992,7 @@ "tick": 29 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 29 }, @@ -3799,7 +4001,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "fd7596f2d3939c387f53a545e2146e8d5a70d56d47751e6d0d6acb493b9dbdc5", + "checksum": "1a42594d8c588edd2f6e2a37e6f25444c4972b1e5e87113cda0f0642bdb060e0", "events": [], "fighters": [ { @@ -3817,6 +4019,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -3840,6 +4043,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -3872,6 +4076,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -3895,6 +4100,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -3920,7 +4126,7 @@ "tick": 30 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 30 }, @@ -3929,7 +4135,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "4bba12fb0c9d30e2324ab2d938bda1a4a25f29150565d2ecaab2c12a1c908458", + "checksum": "4cd874387a283995224039f5fe00b0419fb7cd35b01d053fc67900449d1e5d5a", "events": [], "fighters": [ { @@ -3947,6 +4153,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -3970,6 +4177,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -4002,6 +4210,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -4025,6 +4234,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -4050,7 +4260,7 @@ "tick": 31 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 31 }, @@ -4059,7 +4269,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "a7e2604281f4dd6df5e3568921811585a12f292d96f1f0c764839ec3f7c00dbc", + "checksum": "0829ff05f173f2c11effaf9d6af41a71e9c87e4e1af7e93acd20220311758082", "events": [], "fighters": [ { @@ -4077,6 +4287,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -4100,6 +4311,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -4132,6 +4344,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -4155,6 +4368,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -4180,7 +4394,7 @@ "tick": 32 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 32 }, @@ -4189,7 +4403,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "1d8624b77a995d03f314c0601397253fe89dd4fb585783cc2a46faedd51ed3f0", + "checksum": "e9fe9935649e861ddaaffd50df122f8cbe70de0665ec02f19cb38c7f9dffdeb6", "events": [], "fighters": [ { @@ -4207,6 +4421,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -4230,6 +4445,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -4262,6 +4478,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -4285,6 +4502,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -4310,7 +4528,7 @@ "tick": 33 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 33 }, @@ -4319,7 +4537,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "6fcdc2dab202a4cf5aa9e455b73c41107b3bb684c646fa85ef999d7b882aa2e1", + "checksum": "d291c4773f951d45adc7d2e3da3fe768f8adc2e4bbc34070f1df03d10d911dfe", "events": [], "fighters": [ { @@ -4337,6 +4555,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -4360,6 +4579,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -4392,6 +4612,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -4415,6 +4636,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -4440,7 +4662,7 @@ "tick": 34 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 34 }, @@ -4449,7 +4671,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "38b77c909cac951109d7f02e7b8fb362f05b655cd66068424a0dba31c669f0a5", + "checksum": "557953bc70044a2e6eee7fbd20eb6d10d8680e53c10a40c977e3654fb00bc559", "events": [], "fighters": [ { @@ -4467,6 +4689,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -4490,6 +4713,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -4522,6 +4746,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -4545,6 +4770,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -4570,7 +4796,7 @@ "tick": 35 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 35 }, @@ -4579,7 +4805,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "efa529d80553023827037261efa646f2f60513ddd3842deee1fb363cb3905a11", + "checksum": "751c78dc6042cbc03dee185c43fb9a42369581e04531a4e119d619f1d0272b00", "events": [], "fighters": [ { @@ -4597,6 +4823,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -4620,6 +4847,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -4652,6 +4880,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -4675,6 +4904,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -4700,7 +4930,7 @@ "tick": 36 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 36 }, @@ -4709,7 +4939,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "3ddcf973750658a70b49a9e7521c7a304e1b8fa1ab1e52ca168f526521ca366b", + "checksum": "332db5a70c5b4d1e9d34402561f9bacd329fce59bc5f9f4700fc6a54c9140a1d", "events": [], "fighters": [ { @@ -4727,6 +4957,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -4750,6 +4981,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -4782,6 +5014,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -4805,6 +5038,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -4830,7 +5064,7 @@ "tick": 37 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 37 }, @@ -4839,7 +5073,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "8fd5e97691d1f0fe8507d3a8b258b1af14281df0f56a63b698ee60e469999048", + "checksum": "1b939aee0d7521ad68f4760b529bfa81858a7dbee34192ad4ea6d5930e2c912d", "events": [], "fighters": [ { @@ -4857,6 +5091,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -4880,6 +5115,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -4912,6 +5148,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -4935,6 +5172,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -4960,7 +5198,7 @@ "tick": 38 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 38 }, @@ -4969,7 +5207,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "caf9ced612362ddef1f715372d3355bc806308d5906a28bb08e72ba99839ec86", + "checksum": "dd1b34b8ccbc559aa8eedcd6617e3ec9f111b882fc59aed89159e1a6046e09de", "events": [], "fighters": [ { @@ -4987,6 +5225,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -5010,6 +5249,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -5042,6 +5282,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -5065,6 +5306,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -5090,7 +5332,7 @@ "tick": 39 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 39 }, @@ -5099,7 +5341,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "0864173a68899a532b200a22b4d741484ec6fa581e0f451e88c4ca15f4d74c39", + "checksum": "73258266f6cc08b92af9a55627eea8211669a9b8f6a288cd6147f3a8c6745fd1", "events": [], "fighters": [ { @@ -5117,6 +5359,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -5140,6 +5383,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -5172,6 +5416,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -5195,6 +5440,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -5220,7 +5466,7 @@ "tick": 40 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 40 }, @@ -5229,7 +5475,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "a15f075d9bdfd1a8fb624acd0aa770ce88f1ff74adaf2637c464fe6f2eafeff7", + "checksum": "b3ea28b852b4fe2b68d9c5b172f9cd72325129b793c9c9da7f245c4d112ce155", "events": [], "fighters": [ { @@ -5247,6 +5493,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -5270,6 +5517,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -5302,6 +5550,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -5325,6 +5574,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -5350,7 +5600,7 @@ "tick": 41 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 41 }, @@ -5359,7 +5609,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "5a9f03e3d3a2936acfa90b5ff5fe2f0b0dcaddd7292d94df48958d4e57694055", + "checksum": "42a01bfd5137bbcc6f6485b67cba26383c8f523526146fa083af0cb5144e2ac0", "events": [], "fighters": [ { @@ -5377,6 +5627,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -5400,6 +5651,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -5432,6 +5684,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -5455,6 +5708,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -5480,7 +5734,7 @@ "tick": 42 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 42 }, @@ -5489,7 +5743,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "c340198a48fe2b373550a6985081cf85d1d5fc441e3afc9d6284396e5b51de6c", + "checksum": "8b3d0d3365a713141af3124a4bee0dea71a2edf3bfe1aad0de32ea01e5de2f58", "events": [], "fighters": [ { @@ -5507,6 +5761,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -5530,6 +5785,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -5562,6 +5818,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -5585,6 +5842,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -5610,7 +5868,7 @@ "tick": 43 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 43 }, @@ -5619,7 +5877,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "b92be27b1d7cec9e021e20f7a45e052dadd6f4f8030969b85c8b7bce82633fde", + "checksum": "68eefd761ff958d8645ae532b1fa1ad88f4d2b27afe7e3bc9178e0348c8c3948", "events": [], "fighters": [ { @@ -5637,6 +5895,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -5660,6 +5919,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -5692,6 +5952,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -5715,6 +5976,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -5740,7 +6002,7 @@ "tick": 44 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 44 }, @@ -5749,7 +6011,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "bd9f88879f742cd99c042e2b53f3b24b7d01f44ef5aec6a164a19491987f3b5e", + "checksum": "36e739d81ae1a96257ef0e5d4986e4e218e1fef1a8548d6ce464c48efee60561", "events": [], "fighters": [ { @@ -5767,6 +6029,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -5790,6 +6053,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -5822,6 +6086,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -5845,6 +6110,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -5870,7 +6136,7 @@ "tick": 45 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 45 }, @@ -5879,7 +6145,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "a3abae2dde6746ade2489134c9cabe6d1168782a6c21ba1189d554af4749370a", + "checksum": "f1244058500ece1b3ff017397f7a7f2ae462071d1299ec747395ce16653abaa3", "events": [], "fighters": [ { @@ -5897,6 +6163,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -5920,6 +6187,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -5952,6 +6220,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -5975,6 +6244,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -6000,7 +6270,7 @@ "tick": 46 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 46 }, @@ -6009,7 +6279,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "46761be6c7643ea676cf83dc8ad70895b95ca50e0f4405dc46c91a43aed46a1a", + "checksum": "5ea88492a2eb78409ec3112e1c44577cf830dd41b357c2f70a7b8455ba6f3aa6", "events": [], "fighters": [ { @@ -6027,6 +6297,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -6050,6 +6321,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -6082,6 +6354,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -6105,6 +6378,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -6130,7 +6404,7 @@ "tick": 47 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 47 }, @@ -6139,7 +6413,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "9633402a915e804f043a259c4bb1a10a7b39346dc79f99966a7d50fa506116b5", + "checksum": "2bcc921baa7587f742461a369d1b8e8d4369e30ca8442616960ef0c7abf41122", "events": [], "fighters": [ { @@ -6157,6 +6431,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -6180,6 +6455,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -6212,6 +6488,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -6235,6 +6512,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -6260,7 +6538,7 @@ "tick": 48 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 48 }, @@ -6269,7 +6547,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "1a5a884e6c95489dc95fa2e0952d713112ee058200116370dc40a39538d0e636", + "checksum": "53dc1659d3e413a3c9da3894af33d3fd7d0b8f9e800d8819f96dbbe21ca81bad", "events": [], "fighters": [ { @@ -6287,6 +6565,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -6310,6 +6589,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -6342,6 +6622,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -6365,6 +6646,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -6390,7 +6672,7 @@ "tick": 49 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 49 }, @@ -6399,7 +6681,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "cdbcdc9b4f3295823a21a2298d7ba574a528a1b3b4ae3943f6afe810d5fa390c", + "checksum": "6e0e5d24fcb86f4490b80b8fd71e50c167ef9aaef8e9f6e96874f73f02907689", "events": [], "fighters": [ { @@ -6417,6 +6699,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -6440,6 +6723,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -6472,6 +6756,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -6495,6 +6780,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -6520,7 +6806,7 @@ "tick": 50 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 50 }, @@ -6529,7 +6815,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "c94886af72d7de8969c652d02ce93048e85fc6672a44f5e0431ac4b6be358c62", + "checksum": "9edb4711071ad0f2fa0f6b635c259cf43d55129c791197c6cacd0b7735268181", "events": [], "fighters": [ { @@ -6547,6 +6833,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -6570,6 +6857,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -6602,6 +6890,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -6625,6 +6914,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -6650,7 +6940,7 @@ "tick": 51 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 51 }, @@ -6659,7 +6949,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "98dce9a9490e81924da180ebdf319fee3073a87fe3df26123882caf93c64a6b2", + "checksum": "8a24961641fc08c671b511b52f4b0d749a872d852b3a617f7d4664513c0fb8d0", "events": [], "fighters": [ { @@ -6677,6 +6967,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -6700,6 +6991,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -6732,6 +7024,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -6755,6 +7048,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -6780,7 +7074,7 @@ "tick": 52 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 52 }, @@ -6789,7 +7083,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "e1a53c87c6ce9046cb33b405e1d57d112f429061805c2cce049a92483e6537a5", + "checksum": "8251bd11f361b3012512849ef32b721d810fd1f34e7e03b29a3586ad0fbedf3e", "events": [], "fighters": [ { @@ -6807,6 +7101,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -6830,6 +7125,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -6862,6 +7158,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -6885,6 +7182,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -6910,7 +7208,7 @@ "tick": 53 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 53 }, @@ -6919,7 +7217,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "472c8db0252fc995b15ef0cdf06fa226a99f5cf086458155fe41aae962557b46", + "checksum": "a8355a22b8bc88f505ac0c3fc576c311e53ccd29351c7571a4b3d976f125f8da", "events": [], "fighters": [ { @@ -6937,6 +7235,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -6960,6 +7259,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -6992,6 +7292,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -7015,6 +7316,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -7040,7 +7342,7 @@ "tick": 54 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 54 }, @@ -7049,7 +7351,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "674b0c2230f9be8ecc9c32c375497269b4d9218c86a8f14342473cfb57075b12", + "checksum": "b00303624868d141cc550bbb0abe4c1d20066207073ea2481ef7071a98e5fc4d", "events": [], "fighters": [ { @@ -7067,6 +7369,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -7090,6 +7393,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -7122,6 +7426,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -7145,6 +7450,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -7170,7 +7476,7 @@ "tick": 55 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 55 }, @@ -7179,7 +7485,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "c41efd2170f700ee2d405117dfe744ddc7a9f74d0c1cbe76adabf50fae12d3e9", + "checksum": "2bb757bd8484c8cec5e248f17ed5f74345315c00049b59200ac4c6eb1fe8a6c2", "events": [], "fighters": [ { @@ -7197,6 +7503,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -7220,6 +7527,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -7252,6 +7560,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -7275,6 +7584,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -7300,7 +7610,7 @@ "tick": 56 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 56 }, @@ -7309,7 +7619,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "b3335b27f1149c24141d1375e2fb7510cdeed7d3dcfe21c9575cefa2b116dbf2", + "checksum": "38f9269ec4f3b6cf2ecb40c4b514b917dc3a4e32483ea7a45447e107021f4a1d", "events": [], "fighters": [ { @@ -7327,6 +7637,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -7350,6 +7661,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -7382,6 +7694,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -7405,6 +7718,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -7430,7 +7744,7 @@ "tick": 57 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 57 }, @@ -7439,7 +7753,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "6fc4316409f2b74c9675c86af4fb418fd9315bba6903d7d4b10093b6e292d127", + "checksum": "5290b2a6864eafa892d2154dafffa8abb192e437384b514775438748f36dea75", "events": [], "fighters": [ { @@ -7457,6 +7771,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -7480,6 +7795,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -7512,6 +7828,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -7535,6 +7852,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -7560,7 +7878,7 @@ "tick": 58 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 58 }, @@ -7569,7 +7887,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "c4fc7264c0237db763d578ee1a10c2d556169244534aacd788dd2027a61e11d3", + "checksum": "45d15d81d5e8d602a1116886f0741408bea29a5ed7638bc608b1458b1f5866d9", "events": [], "fighters": [ { @@ -7587,6 +7905,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -7610,6 +7929,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -7642,6 +7962,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -7665,6 +7986,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -7690,7 +8012,7 @@ "tick": 59 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 59 }, @@ -7699,7 +8021,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "61fa36a9e7b235c8cceacd5b4adc50477f09e9baee31dc6ad5c8bcfdfa1a9cd7", + "checksum": "22c71b67a998f8b4ad422b4178a78238c4969d1e3b8b1959e3dc68aa97ac1d7d", "events": [], "fighters": [ { @@ -7717,6 +8039,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -7740,6 +8063,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -7772,6 +8096,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -7795,6 +8120,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -7820,7 +8146,7 @@ "tick": 60 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 60 }, @@ -7829,7 +8155,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "c392aea37f6a317d4993bdb844e0f243d6d1c0c60612a163b4f543b7b35366c7", + "checksum": "d61b383ade25d35a7c7223472d1609a8d6ea4a42c78da49f8ec718a9ef406a51", "events": [], "fighters": [ { @@ -7847,6 +8173,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -7870,6 +8197,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -7902,6 +8230,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -7925,6 +8254,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -7950,7 +8280,7 @@ "tick": 61 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 61 }, @@ -7959,7 +8289,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "1f8e762d943170d38ffe9d72ff851f337ec1a88d270674bbdb56f3c139f98eba", + "checksum": "a7c9c3b9efd95a068afd3f735664363bcb51a51dc5b9374430aabc6416b330b4", "events": [], "fighters": [ { @@ -7977,6 +8307,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -8000,6 +8331,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -8032,6 +8364,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -8055,6 +8388,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -8080,7 +8414,7 @@ "tick": 62 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 62 }, @@ -8089,7 +8423,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "affc5db381799946f1b243169e8388c80c4af8fcbe9b9a200f91dbfb3dbce111", + "checksum": "d796385c452cc7b3093a167b257ad1524be828757a10be985734cd48a200cd7c", "events": [], "fighters": [ { @@ -8107,6 +8441,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -8130,6 +8465,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -8162,6 +8498,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -8185,6 +8522,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -8210,7 +8548,7 @@ "tick": 63 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 63 }, @@ -8219,7 +8557,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "c7dde928407a640e71bdbab86abae920869257ec647d54daf5fc72f187052bae", + "checksum": "b1b480588957cb4f729ce9b2bae594c90654b3b67be35f55132dc6273822aaa8", "events": [], "fighters": [ { @@ -8237,6 +8575,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -8260,6 +8599,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -8292,6 +8632,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -8315,6 +8656,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -8340,7 +8682,7 @@ "tick": 64 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 64 }, @@ -8349,7 +8691,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "7848300e6aece7cca4370712246eb69f02c1e711f0b030f695adf14a5ed9882a", + "checksum": "6ae44cbd2c6b4d78f2f8273fb6a49e1be3bcba80397c5c0464b0e1331b3b98f6", "events": [], "fighters": [ { @@ -8367,6 +8709,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -8390,6 +8733,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -8422,6 +8766,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -8445,6 +8790,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -8470,7 +8816,7 @@ "tick": 65 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 65 }, @@ -8479,7 +8825,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "918ce15996efbd24ad1baefb580f5ce468d6af77a3f934e9ae953677e5a42b2b", + "checksum": "3d994e3cd367792a1dded1b31e1ebd53efe6161177f337c40d92431c384301bb", "events": [], "fighters": [ { @@ -8497,6 +8843,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -8520,6 +8867,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -8552,6 +8900,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -8575,6 +8924,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -8600,7 +8950,7 @@ "tick": 66 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 66 }, @@ -8609,7 +8959,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "1cf08f75b388f033b1bf7c52402cdd99b178d0db63f579af575c71c68ec8e5b2", + "checksum": "ed3f151361d56f20f7f529449a1f686cfc7bc995f8db90a75c627b528940b423", "events": [], "fighters": [ { @@ -8627,6 +8977,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -8650,6 +9001,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -8682,6 +9034,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -8705,6 +9058,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -8730,7 +9084,7 @@ "tick": 67 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 67 }, @@ -8739,7 +9093,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "a875adde02d0fd76eaaae2acb0844839f9a66b83ab89cf454aa25b167cac2c8b", + "checksum": "591125fb7bee8b250f926976fb281720e0cb7cd631194552eb6968612f3306ac", "events": [], "fighters": [ { @@ -8757,6 +9111,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -8780,6 +9135,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -8812,6 +9168,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -8835,6 +9192,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -8860,7 +9218,7 @@ "tick": 68 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 68 }, @@ -8869,7 +9227,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "f25ca7c75063ac897c69287530816a6ee43980e6bb65b52c1a92854aeb01d4d0", + "checksum": "b647d414216faea784d8055a1d60b5bf11f84afc025ae36bfb3a14b0ae00c086", "events": [], "fighters": [ { @@ -8887,6 +9245,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -8910,6 +9269,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -8942,6 +9302,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -8965,6 +9326,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -8990,7 +9352,7 @@ "tick": 69 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 69 }, @@ -8999,7 +9361,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "3b9382a37aef62ae45f68c463b27843c9b2d888f7d55b8bfc2bd590078906c10", + "checksum": "6f7fcf81f6ccb8a4e064264a87a93b6165f144f021cb37a7d97bb0fb3569bd9d", "events": [], "fighters": [ { @@ -9017,6 +9379,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -9040,6 +9403,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -9072,6 +9436,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -9095,6 +9460,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -9120,7 +9486,7 @@ "tick": 70 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 70 }, @@ -9129,7 +9495,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "4c331e6738e4dc9ec865b74783f31d8fac227184292a00dc31face0fbc0190fe", + "checksum": "5d91d6a55cc3476bdcd778ea8f946a75038a883131ef4fc228184f24af5c79c9", "events": [], "fighters": [ { @@ -9147,6 +9513,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -9170,6 +9537,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -9202,6 +9570,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -9225,6 +9594,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -9250,7 +9620,7 @@ "tick": 71 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 71 }, @@ -9262,7 +9632,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": false, "startup": 3, "target": "head" @@ -9270,7 +9640,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "772bc6d7c01512d8307c457a76264c9c09f1c5b9d7f27c62a7622bc011dd2770", + "checksum": "01a30833187008f454573ac024b4aa7cb5892ca00016be4b3909409b7b052fab", "events": [ { "action_id": "one@72", @@ -9294,13 +9664,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -9324,6 +9695,7 @@ "stamina": 959, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -9356,6 +9728,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -9379,6 +9752,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -9404,7 +9778,7 @@ "tick": 72 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 72 }, @@ -9416,7 +9790,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": false, "startup": 3, "target": "head" @@ -9424,7 +9798,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "0707844ccbefb4db659a54da119f934f47ffcc22f588bce8ccda7345923d8bdb", + "checksum": "27c1a840f25712e9d07c306d354414c6c87996f1ed6b85d4a5a338f875b77b69", "events": [], "fighters": [ { @@ -9435,13 +9809,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -9465,6 +9840,7 @@ "stamina": 960, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -9497,6 +9873,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -9520,6 +9897,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -9545,7 +9923,7 @@ "tick": 73 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 73 }, @@ -9557,7 +9935,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": false, "startup": 3, "target": "head" @@ -9565,7 +9943,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "a95a6b944bdcf0ddd4db0ba32bbebfc8e4e2197669f22f6e4c69c028e5c04564", + "checksum": "616bfec969886be5ae5b7ee50d8594a8e0631eef8235454e0f7ec088c08246d8", "events": [], "fighters": [ { @@ -9576,13 +9954,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -9606,6 +9985,7 @@ "stamina": 961, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -9638,6 +10018,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -9661,6 +10042,7 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -9686,7 +10068,7 @@ "tick": 74 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 74 }, @@ -9698,7 +10080,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": true, "startup": 3, "target": "head" @@ -9706,12 +10088,12 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "589b81316dc89f5ad6278d94644bff58a24154355728df0f3ed65e700650c313", + "checksum": "c1958b7217398ff3dc4f1d121c42bfb62f382c2e2fa112e42b57071e7b35643a", "events": [ { "action_id": "one@72", "actor_id": "two", - "amount": 44, + "amount": 26, "blood": 0, "detail": "", "direction": 0, @@ -9723,8 +10105,8 @@ { "action_id": "one@72", "actor_id": "one", - "amount": 14, - "blood": 3, + "amount": 3, + "blood": 0, "detail": "jab:head", "direction": 1, "event_id": 3, @@ -9742,13 +10124,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -9772,6 +10155,7 @@ "stamina": 962, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -9804,6 +10188,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -9815,28 +10200,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 674, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 566, + "poise": 593, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -9852,7 +10238,7 @@ "tick": 75 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 75 }, @@ -9864,7 +10250,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": true, "startup": 3, "target": "head" @@ -9872,7 +10258,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "096e48035830fef8d818f5af0ea55fa1311834317e82437e729248e135c1f355", + "checksum": "f4244a4aa4e56f2702a936cebaf59019da3e4e3b6248a803632ebe6a30f1622f", "events": [], "fighters": [ { @@ -9883,13 +10269,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -9913,6 +10300,7 @@ "stamina": 963, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -9945,6 +10333,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -9956,28 +10345,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 677, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 567, + "poise": 594, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -9993,7 +10383,7 @@ "tick": 76 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 76 }, @@ -10005,7 +10395,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": true, "startup": 3, "target": "head" @@ -10013,7 +10403,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "30f66859627a2ebdf7720c1308b851ba1c3962d9b3bcaea1af2ab89d09e9452b", + "checksum": "d3a4288a29f021915b3e2021f5d65c25df0f41f1ed6bc80180e7a2e2fc6a9924", "events": [], "fighters": [ { @@ -10024,13 +10414,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -10054,6 +10445,7 @@ "stamina": 964, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -10086,6 +10478,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -10097,28 +10490,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 677, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 568, + "poise": 594, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -10134,7 +10528,7 @@ "tick": 77 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 77 }, @@ -10146,7 +10540,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": true, "startup": 3, "target": "head" @@ -10154,7 +10548,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "cc34e9e2446ae489ba3892ed242c2e2df750e16f44e14abd2a32460a09e3cbc5", + "checksum": "5c18a50dfa55b207d21637876b8d0f363ce42f47a83ea5e1ab4397da8ad817b4", "events": [], "fighters": [ { @@ -10165,13 +10559,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -10195,6 +10590,7 @@ "stamina": 965, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -10227,6 +10623,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -10238,28 +10635,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 677, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 569, + "poise": 595, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -10275,7 +10673,7 @@ "tick": 78 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 78 }, @@ -10287,7 +10685,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": true, "startup": 3, "target": "head" @@ -10295,7 +10693,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "f13ef5e652cedf0a845e327507b2cfe09f23e6006b8a1314a5a572287f4b66f2", + "checksum": "b9da4696313e21ed910362860b26caf3506d2dbcb4b6a4de2f3f9e1f80b6348b", "events": [], "fighters": [ { @@ -10306,13 +10704,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -10336,6 +10735,7 @@ "stamina": 966, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -10368,6 +10768,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -10379,28 +10780,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 677, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 570, + "poise": 595, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -10416,7 +10818,7 @@ "tick": 79 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 79 }, @@ -10428,7 +10830,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": true, "startup": 3, "target": "head" @@ -10436,7 +10838,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "e30e4fb8c2bc641a4e9353830830cd4a8e089ddfbe7abb16a58e299a88fd39f8", + "checksum": "4a53bc9fbb5f43a19b8d50f912ecbba2bf53b17a955bda7ac867bb13d53eae98", "events": [], "fighters": [ { @@ -10447,13 +10849,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -10477,6 +10880,7 @@ "stamina": 967, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -10509,6 +10913,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -10520,28 +10925,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 680, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 571, + "poise": 596, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -10557,7 +10963,7 @@ "tick": 80 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 80 }, @@ -10569,7 +10975,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": true, "startup": 3, "target": "head" @@ -10577,7 +10983,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "cb5f0be352c9c08c4d0eff8e43b0d6e9215a8e475b2cf88cbfa08d847d366165", + "checksum": "a51e6bd8bd287c66d9b4092b3bce957d55f764a04c44db00f25befab19b4445b", "events": [], "fighters": [ { @@ -10588,13 +10994,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -10618,6 +11025,7 @@ "stamina": 968, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -10650,6 +11058,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -10661,28 +11070,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 680, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 572, + "poise": 596, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -10698,7 +11108,7 @@ "tick": 81 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 81 }, @@ -10710,7 +11120,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": true, "startup": 3, "target": "head" @@ -10718,7 +11128,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "06619441e1de9fd3256371a50dd918a7e7fd56f3eace853412470c96f9052efb", + "checksum": "091efc5a67115716b64496c28499fdedf4465f1e9e6e0f020fd44509434ad5c7", "events": [], "fighters": [ { @@ -10729,13 +11139,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -10759,6 +11170,7 @@ "stamina": 969, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -10791,6 +11203,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -10802,28 +11215,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 680, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 573, + "poise": 597, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -10839,27 +11253,16 @@ "tick": 82 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 82 }, { - "attack_one": { - "active": 2, - "age": 11, - "class": "jab", - "combo_bonus": 0, - "hand": "left", - "power": "normal", - "recovery": 7, - "resolved": true, - "startup": 3, - "target": "head" - }, + "attack_one": null, "attack_two": null, "snapshot": { "payload": { - "checksum": "bf10d3a9b570bca14f42f1f389b4c0b2b0726bcb3b04dcf5420108c15ba9e8c8", + "checksum": "97f829b52b63e59a4bade88321a08592cc5d67c9f2c5e94b9351780e7f41637e", "events": [], "fighters": [ { @@ -10870,13 +11273,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -10897,9 +11301,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 970, + "stamina": 972, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -10932,6 +11337,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -10943,28 +11349,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 680, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 574, + "poise": 597, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -10980,7 +11387,7 @@ "tick": 83 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 83 }, @@ -10989,7 +11396,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "a2ab531be90b309e185017a3f431f8a07ada1b02a22f5c9bfa1fdee6f1cb67d8", + "checksum": "6dd79116361c5c5008e6f8aa77eb12c20e5ffd2f6bfe9b725491b7ae64f95b6d", "events": [], "fighters": [ { @@ -11000,13 +11407,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -11027,9 +11435,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 974, + "stamina": 976, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -11062,6 +11471,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -11073,28 +11483,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 683, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 575, + "poise": 598, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -11110,7 +11521,7 @@ "tick": 84 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 84 }, @@ -11119,7 +11530,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "543f8dd95245a77f1195e965d225006868e9a5a24db6f8630fa3a7c28ce5518e", + "checksum": "f4b8a925484b7af4b566e35c482e42ad9f127252e528ca82daa43e08f2f49af6", "events": [], "fighters": [ { @@ -11130,13 +11541,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -11157,9 +11569,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 978, + "stamina": 980, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -11192,6 +11605,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -11203,28 +11617,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 683, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 576, + "poise": 598, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -11240,7 +11655,7 @@ "tick": 85 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 85 }, @@ -11249,7 +11664,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "761b9d5bbbea017a0a44e06de09779888e5907e16992c632ed5493d6aec5b70f", + "checksum": "82ae950889c7c24b2fdd534e32f232499e7e5395ffc7a49bd627907f41652666", "events": [], "fighters": [ { @@ -11260,13 +11675,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -11287,9 +11703,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 982, + "stamina": 984, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -11322,6 +11739,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -11333,28 +11751,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 683, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 577, + "poise": 599, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -11370,7 +11789,7 @@ "tick": 86 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 86 }, @@ -11379,7 +11798,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "fca60322f40deda09fbc0a19bc4a7cc1ddb19379c52d88713e769b5156baeb2e", + "checksum": "144be854e8c83f2d287ed64bbf3a99280ac361f3f6c188b41c55932be86c5fed", "events": [], "fighters": [ { @@ -11390,13 +11809,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -11417,9 +11837,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 986, + "stamina": 988, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -11452,6 +11873,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -11463,28 +11885,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 683, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 578, + "poise": 599, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -11500,7 +11923,7 @@ "tick": 87 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 87 }, @@ -11509,7 +11932,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "a3bbdda8114bd6c112ba3f00619aa996fbcb358547cd4e96488d0301345e259a", + "checksum": "d7c0c09cfae9e7edc8dc64b8c0e06178811e06dc7097a0c3fa63a417508ea9b3", "events": [], "fighters": [ { @@ -11520,13 +11943,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -11547,9 +11971,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 990, + "stamina": 992, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -11582,6 +12007,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -11593,28 +12019,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 686, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 579, + "poise": 600, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -11630,7 +12057,7 @@ "tick": 88 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 88 }, @@ -11639,7 +12066,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "84e1e2e05397e36ae94afa6a95c4e38bd5c7b7f85dc22e6b296930bc4cd2c9ec", + "checksum": "3bc2182d41eaf79fea517ba4addbb83ed679daf24d2f2b462dceb52542197abb", "events": [], "fighters": [ { @@ -11650,13 +12077,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -11677,9 +12105,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 994, + "stamina": 996, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -11712,6 +12141,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -11723,28 +12153,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 686, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 580, + "poise": 600, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -11760,7 +12191,7 @@ "tick": 89 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 89 }, @@ -11769,7 +12200,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "5b4e29f1b30bec348194b79de695d08751cdfeff4bc41d047195a5249e6c3b88", + "checksum": "42fad5821ed836e591aa33cae3355f422e53607e66a62d8a5619381d9914d5d7", "events": [], "fighters": [ { @@ -11780,13 +12211,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -11807,9 +12239,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 998, + "stamina": 999, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -11842,6 +12275,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -11853,28 +12287,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 686, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 581, + "poise": 600, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -11890,7 +12325,7 @@ "tick": 90 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 90 }, @@ -11899,7 +12334,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "167977262da1a8831ba53df08221dd455639eae7d2ea9f146df44207ecff003f", + "checksum": "7bbb0035e72c95087a691822ce9038880ca12be2d76928efd040ad954a0ae1c4", "events": [], "fighters": [ { @@ -11910,13 +12345,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -11940,6 +12376,7 @@ "stamina": 999, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -11972,6 +12409,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -11983,28 +12421,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 686, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 582, + "poise": 600, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -12020,7 +12459,7 @@ "tick": 91 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 91 }, @@ -12029,7 +12468,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "05238f5204e887a66e8ddddf23571163090e0faccd47dde5f3bae101788c5819", + "checksum": "e308ba1cedcb090ad0a95c9cecc2aada8f41d6ea745701f2138d391b0843ab3e", "events": [], "fighters": [ { @@ -12040,13 +12479,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -12070,6 +12510,7 @@ "stamina": 999, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -12102,6 +12543,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -12113,28 +12555,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 689, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 583, + "poise": 600, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -12150,7 +12593,7 @@ "tick": 92 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 92 }, @@ -12159,7 +12602,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "be2c9f27c594727992142d6869dae1547a04a25f887b1c81bb5ce48c45670dea", + "checksum": "549f1395921f1e6b832c9bc8bae3d2534e41de39a72da086d903544fd4c0ea5a", "events": [], "fighters": [ { @@ -12170,13 +12613,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -12200,6 +12644,7 @@ "stamina": 999, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -12232,6 +12677,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -12243,28 +12689,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 689, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 584, + "poise": 600, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -12280,7 +12727,7 @@ "tick": 93 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 93 }, @@ -12289,7 +12736,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "fbf43e7cc6187f8b4cb182babc2513e06be557f8c3f985aaf4875799b42949dd", + "checksum": "57d1b0cad24844eb7a3c1632c960955dce705a6350f6ab00fbabffaeac5e9a36", "events": [], "fighters": [ { @@ -12300,13 +12747,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -12330,6 +12778,7 @@ "stamina": 999, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -12362,6 +12811,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -12373,28 +12823,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 689, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 585, + "poise": 600, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -12410,7 +12861,7 @@ "tick": 94 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 94 }, @@ -12419,7 +12870,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "dbaa395332b17cdb682fb326dfde52b46cf97337a73c40bd10ec983673387fbf", + "checksum": "4e47b03f7925f96fa8727f7442992eeec9ab92173d58a216b479d146686dd3bd", "events": [], "fighters": [ { @@ -12430,13 +12881,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -12460,6 +12912,7 @@ "stamina": 999, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -12492,6 +12945,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -12503,28 +12957,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 689, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 586, + "poise": 600, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -12540,7 +12995,7 @@ "tick": 95 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 95 }, @@ -12549,7 +13004,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "e1b921fcca1cc26e93b508c49496f3675677574f829f1b18f4f14b0f39c7764f", + "checksum": "540f0b6f2537cb416d03e5a28dffbad851a956d0d23ee25cf6f34eaa9bcfaf28", "events": [], "fighters": [ { @@ -12560,13 +13015,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -12590,6 +13046,7 @@ "stamina": 999, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -12622,6 +13079,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -12633,28 +13091,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 692, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 587, + "poise": 600, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -12670,7 +13129,7 @@ "tick": 96 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 96 }, @@ -12679,7 +13138,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "dbde59f45c0cee86a0c3a7283ad40a9e5a139f1373224ecd6ae69c0b48b3e0a2", + "checksum": "cfd0efde0d4cdcc9749348fd4aa758144d2dcb8b322cd9b188499a9315e2b9da", "events": [], "fighters": [ { @@ -12690,13 +13149,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -12720,6 +13180,7 @@ "stamina": 999, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -12752,6 +13213,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -12763,28 +13225,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 692, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 588, + "poise": 600, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -12800,7 +13263,7 @@ "tick": 97 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 97 }, @@ -12809,7 +13272,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "db8608d8a6b0e483e9b554dbb4aa0cfc2913155ce8503f95d730dac13ed03745", + "checksum": "7eeb1cfd1ca260964e031e1ffc7b3c5a752edb99a75f0bcf27eec2c2c6d72d1c", "events": [], "fighters": [ { @@ -12820,13 +13283,14 @@ "action_id": "one@72", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 72, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -12850,6 +13314,7 @@ "stamina": 999, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -12882,6 +13347,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -12893,28 +13359,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 692, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 589, + "poise": 600, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -12930,7 +13397,7 @@ "tick": 98 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 98 }, @@ -12939,24 +13406,25 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "3119334d472fc7d7c1b858ceedf9beb317ebec69286453f018a3f337fa22c566", + "checksum": "82f8bbb40ad25494c8690ce8914c92a675c6d448ed3ff03dd5888923aefcdc4a", "events": [], "fighters": [ { - "action": "jab", - "action_active_ticks": 2, - "action_contact_tick": 75, - "action_hand": "left", - "action_id": "one@72", - "action_key": "jab:left:head:normal", - "action_power": "normal", - "action_recovery_ticks": 7, - "action_start_tick": 72, - "action_startup_ticks": 3, - "action_target": "head", + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 997, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -12980,6 +13448,7 @@ "stamina": 999, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -13012,6 +13481,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -13023,28 +13493,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 692, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 590, + "poise": 600, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -13060,7 +13531,7 @@ "tick": 99 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 99 }, @@ -13072,7 +13543,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": false, "startup": 3, "target": "head" @@ -13080,7 +13551,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "362ca9db1d2c91ba7e48d6b4a9d7d37a2b42e6d94ca38deaa2cfe6da2ce84b9a", + "checksum": "b20b76660438603f226901ef3eb2ea6788d43fd3ad7d052b911a7fdc9c2339d8", "events": [ { "action_id": "one@100", @@ -13104,13 +13575,14 @@ "action_id": "one@100", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 100, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 994, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -13134,6 +13606,7 @@ "stamina": 958, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -13166,6 +13639,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -13177,28 +13651,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 695, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 591, + "poise": 600, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -13214,7 +13689,7 @@ "tick": 100 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 100 }, @@ -13226,7 +13701,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": false, "startup": 3, "target": "head" @@ -13234,7 +13709,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "368faecde2745982b39f149ac256719f49f409cec2c704a44d955c40529fef82", + "checksum": "a52e6ec3ffa89ef39f1fa061edafee178bd23cd3bff2c1925ed50400b8da40c1", "events": [], "fighters": [ { @@ -13245,13 +13720,14 @@ "action_id": "one@100", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 100, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 994, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -13275,6 +13751,7 @@ "stamina": 959, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -13307,6 +13784,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -13318,28 +13796,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 695, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 592, + "poise": 600, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -13355,7 +13834,7 @@ "tick": 101 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 101 }, @@ -13367,7 +13846,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": false, "startup": 3, "target": "head" @@ -13375,7 +13854,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "738cb1ee36416d28604bb88d0b66c216c41ef307ef23f33244b6271288f4b8c2", + "checksum": "3b39363e1913f7fe5c7312b583fbb053c0fa8ed737231d8a2af4fe49011fd27e", "events": [], "fighters": [ { @@ -13386,13 +13865,14 @@ "action_id": "one@100", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 100, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 994, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -13416,6 +13896,7 @@ "stamina": 960, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -13448,6 +13929,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -13459,28 +13941,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 656, + "guard": 695, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 593, + "poise": 600, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 28, + "head": 3, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 14, - "swelling": 7 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -13496,7 +13979,7 @@ "tick": 102 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 102 }, @@ -13508,7 +13991,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": true, "startup": 3, "target": "head" @@ -13516,12 +13999,12 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "9d2d83108224e52739c68ce29da0a21249acbf4e9f01b1324c659c19782b583b", + "checksum": "b2a923ccd327bf6837ba5d963beb43b5e1a9dc4d04cb4b207b32e89b80c94349", "events": [ { "action_id": "one@100", "actor_id": "two", - "amount": 44, + "amount": 26, "blood": 0, "detail": "", "direction": 0, @@ -13533,8 +14016,8 @@ { "action_id": "one@100", "actor_id": "one", - "amount": 15, - "blood": 3, + "amount": 4, + "blood": 1, "detail": "jab:head", "direction": 1, "event_id": 6, @@ -13552,13 +14035,14 @@ "action_id": "one@100", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 100, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 994, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -13582,6 +14066,7 @@ "stamina": 961, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -13614,6 +14099,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -13625,28 +14111,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 612, + "guard": 669, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 559, + "poise": 593, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 58, + "head": 7, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 29, - "swelling": 14 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -13662,7 +14149,7 @@ "tick": 103 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 103 }, @@ -13674,7 +14161,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": true, "startup": 3, "target": "head" @@ -13682,7 +14169,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "bfe18cf01ccc7408b63bb97f0bc4e075be8b83d6729143472ab4051c66b1f8f2", + "checksum": "2cb66003f260517c01d18ad659ba1b8c23f1a4d5784082a0635b196506727919", "events": [], "fighters": [ { @@ -13693,13 +14180,14 @@ "action_id": "one@100", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 100, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 994, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -13723,6 +14211,7 @@ "stamina": 962, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -13755,6 +14244,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -13766,28 +14256,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 612, + "guard": 672, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 560, + "poise": 594, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 58, + "head": 7, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 29, - "swelling": 14 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -13803,7 +14294,7 @@ "tick": 104 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 104 }, @@ -13815,7 +14306,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": true, "startup": 3, "target": "head" @@ -13823,7 +14314,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "6a0be234b7a7c4330c2bd0c428310ee795a18f7b9769b6310432951f119c9fa5", + "checksum": "c70c570ec1bb8c945464c9ce85dad66192ddea3232d5baf2bb339b3ef4b41dde", "events": [], "fighters": [ { @@ -13834,13 +14325,14 @@ "action_id": "one@100", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 100, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 994, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -13864,6 +14356,7 @@ "stamina": 963, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -13896,6 +14389,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -13907,28 +14401,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 612, + "guard": 672, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 561, + "poise": 594, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 58, + "head": 7, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 29, - "swelling": 14 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -13944,7 +14439,7 @@ "tick": 105 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 105 }, @@ -13956,7 +14451,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": true, "startup": 3, "target": "head" @@ -13964,7 +14459,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "4db80ad3694b7ed2d76e1ae3d41a3d6ab41b2469cd3f15117e1c36002fce53d0", + "checksum": "72d1c276d3864bb20582af001003c1be7e9d4b86736f91058841a503fb097ea7", "events": [], "fighters": [ { @@ -13975,13 +14470,14 @@ "action_id": "one@100", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 100, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 994, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -14005,6 +14501,7 @@ "stamina": 964, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -14037,6 +14534,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -14048,28 +14546,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 612, + "guard": 672, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 562, + "poise": 595, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 58, + "head": 7, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 29, - "swelling": 14 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -14085,7 +14584,7 @@ "tick": 106 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 106 }, @@ -14097,7 +14596,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": true, "startup": 3, "target": "head" @@ -14105,7 +14604,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "217ebf75d8d34397e9dedb9166c393e78d268de62ba4595e9749b9c78cd78294", + "checksum": "8a9f67ddd94b7d07e89afc8e57ae89db3a63b7ec082ecdc3d9e7e726a0da7da3", "events": [], "fighters": [ { @@ -14116,13 +14615,14 @@ "action_id": "one@100", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 100, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 994, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -14146,6 +14646,7 @@ "stamina": 965, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -14178,6 +14679,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -14189,28 +14691,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 612, + "guard": 672, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 563, + "poise": 595, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 58, + "head": 7, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 29, - "swelling": 14 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -14226,7 +14729,7 @@ "tick": 107 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 107 }, @@ -14238,7 +14741,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": true, "startup": 3, "target": "head" @@ -14246,7 +14749,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "c926639046b303712d7c4ec7d9f1067839b520fd4a59a067b41c4dce58489cda", + "checksum": "eac9e69b2e65f454027f4f0705516f5880ac63ce4c764a9fc9fd0ba0afd387fd", "events": [], "fighters": [ { @@ -14257,13 +14760,14 @@ "action_id": "one@100", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 100, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 994, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -14287,6 +14791,7 @@ "stamina": 966, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -14319,6 +14824,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -14330,28 +14836,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 612, + "guard": 675, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 564, + "poise": 596, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 58, + "head": 7, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 29, - "swelling": 14 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -14367,7 +14874,7 @@ "tick": 108 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 108 }, @@ -14379,7 +14886,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": true, "startup": 3, "target": "head" @@ -14387,7 +14894,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "a8d09740ab1ca6c7bab72d0a4edbcdb50b743c77f579ea3d6b1bfaeab9feb5ac", + "checksum": "88c4c574f952908700c30d12d26e02483411db79c381bba0858c4cfa62fe6623", "events": [], "fighters": [ { @@ -14398,13 +14905,14 @@ "action_id": "one@100", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 100, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 994, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -14428,6 +14936,7 @@ "stamina": 967, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -14460,6 +14969,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -14471,28 +14981,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 612, + "guard": 675, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 565, + "poise": 596, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 58, + "head": 7, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 29, - "swelling": 14 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -14508,7 +15019,7 @@ "tick": 109 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 109 }, @@ -14520,7 +15031,7 @@ "combo_bonus": 0, "hand": "left", "power": "normal", - "recovery": 7, + "recovery": 6, "resolved": true, "startup": 3, "target": "head" @@ -14528,7 +15039,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "e671932e1a140847e6c590ff1cfa227e75bbe07184a43ae453e7adbfa91cd21d", + "checksum": "969b3802efe9e11e0ac2ca43fee97f64982c3b3a89a416b0e3db34a2319aa834", "events": [], "fighters": [ { @@ -14539,13 +15050,14 @@ "action_id": "one@100", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 100, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 994, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -14569,6 +15081,7 @@ "stamina": 968, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -14601,6 +15114,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -14612,28 +15126,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 612, + "guard": 675, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 566, + "poise": 597, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 58, + "head": 7, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 29, - "swelling": 14 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -14649,27 +15164,16 @@ "tick": 110 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 110 }, { - "attack_one": { - "active": 2, - "age": 11, - "class": "jab", - "combo_bonus": 0, - "hand": "left", - "power": "normal", - "recovery": 7, - "resolved": true, - "startup": 3, - "target": "head" - }, + "attack_one": null, "attack_two": null, "snapshot": { "payload": { - "checksum": "a9e2bacdbc5afeb9b0fa559e50a121795dcc0dfd62821e945986e853b395dd62", + "checksum": "7126d49f711e2e00d4a6b531aabc868a6982903e8a3573ab16e798fe34426c38", "events": [], "fighters": [ { @@ -14680,13 +15184,14 @@ "action_id": "one@100", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 100, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 994, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -14707,9 +15212,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 969, + "stamina": 972, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -14742,6 +15248,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -14753,28 +15260,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 612, + "guard": 675, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 567, + "poise": 597, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 58, + "head": 7, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 29, - "swelling": 14 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -14790,7 +15298,7 @@ "tick": 111 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 111 }, @@ -14799,7 +15307,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "0a0a113fba1e4d69dd08efc723ce43191839756fae21bb305353ceeb7e46bece", + "checksum": "fc40cdfe1b881dd1f8e1eabf75f215375355ebe50b949e936863a9ad8c1897b6", "events": [], "fighters": [ { @@ -14810,13 +15318,14 @@ "action_id": "one@100", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 100, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 994, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -14837,9 +15346,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 973, + "stamina": 976, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -14872,6 +15382,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -14883,28 +15394,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 612, + "guard": 678, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 568, + "poise": 598, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 58, + "head": 7, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 29, - "swelling": 14 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -14920,7 +15432,7 @@ "tick": 112 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 112 }, @@ -14929,7 +15441,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "5596592f5cf8d24d605e17e7ae143f983e135d31995b6e2e00d6fa456dea6023", + "checksum": "404bac5499d70aa9f4f356825378e6db718185bd9eeaa9ca63a0564e00cf981f", "events": [], "fighters": [ { @@ -14940,13 +15452,14 @@ "action_id": "one@100", "action_key": "jab:left:head:normal", "action_power": "normal", - "action_recovery_ticks": 7, + "action_recovery_ticks": 6, "action_start_tick": 100, "action_startup_ticks": 3, "action_target": "head", "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 994, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -14967,9 +15480,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 977, + "stamina": 980, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -15002,6 +15516,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "guard_high", "facing": -1, @@ -15013,28 +15528,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 612, + "guard": 678, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 569, + "poise": 598, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 58, + "head": 7, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 29, - "swelling": 14 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -15050,7 +15566,7 @@ "tick": 113 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 113 }, @@ -15070,7 +15586,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "d6dab527b3da5aad0862f4bbf2dbb07c5d7fc4241353e6d96f25501f16ffd784", + "checksum": "cea0a57110d1aac2a5ada94be94432fd347222d098bdec1e091efcd698c7523e", "events": [ { "action_id": "one@114", @@ -15101,6 +15617,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -15121,9 +15638,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 920, + "stamina": 923, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -15156,6 +15674,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -15167,7 +15686,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 615, + "guard": 681, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, @@ -15179,16 +15698,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 58, + "head": 7, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 29, - "swelling": 14 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -15204,7 +15724,7 @@ "tick": 114 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 114 }, @@ -15224,7 +15744,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "337cd3e7f71e70ddbe17541d75fd6e94e0a0e4c2c50999eb2b22a2e64d5dfcd6", + "checksum": "45ebff5146ff2b4285a50f401c39652fdc6a5e9806f5e818516067a8fb50b6ea", "events": [], "fighters": [ { @@ -15242,6 +15762,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -15262,9 +15783,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 921, + "stamina": 924, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -15297,6 +15819,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -15308,28 +15831,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 615, + "guard": 681, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 3, + "poise": 2, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 58, + "head": 7, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 29, - "swelling": 14 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -15345,7 +15869,7 @@ "tick": 115 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 115 }, @@ -15365,7 +15889,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "e38e479eda4486659324dbe46cbac638963d39e288d0fdf4df637a762469a6fb", + "checksum": "aae9ee59b382156d4169d388dda1a0c624e44a00712e152f48e424ceee95d39f", "events": [], "fighters": [ { @@ -15383,6 +15907,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -15403,9 +15928,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 922, + "stamina": 925, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -15438,6 +15964,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -15449,28 +15976,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 618, + "guard": 684, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 4, + "poise": 3, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 58, + "head": 7, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 29, - "swelling": 14 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -15486,7 +16014,7 @@ "tick": 116 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 116 }, @@ -15506,7 +16034,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "dcee5558c6d1a827286a9582487bf410b7ca841a8a3f143a8dc2ba169b38704c", + "checksum": "d782e0dd79a43f0baab1f220e422d797726f586a3572e8ce78d55d84d6768c34", "events": [], "fighters": [ { @@ -15524,6 +16052,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -15544,9 +16073,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 923, + "stamina": 926, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -15579,6 +16109,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -15590,28 +16121,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 618, + "guard": 684, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 5, + "poise": 3, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 58, + "head": 7, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 29, - "swelling": 14 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -15627,7 +16159,7 @@ "tick": 117 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 117 }, @@ -15647,7 +16179,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "f34a27f245c1b1b518af27f203fdd4ce53b836b892b8b76c855cedb7cd311505", + "checksum": "d1a73a2f8bd8095a8a3fb6f31b8975b0bc03d1e68dbc577a7c9a2d8316a4e746", "events": [], "fighters": [ { @@ -15665,6 +16197,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -15685,9 +16218,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 924, + "stamina": 927, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -15720,6 +16254,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -15731,28 +16266,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 621, + "guard": 687, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 6, + "poise": 4, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 58, + "head": 7, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 29, - "swelling": 14 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -15768,7 +16304,7 @@ "tick": 118 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 118 }, @@ -15788,7 +16324,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "16485b15ada4588ab428c58c79acee58a305447b24542d36996ea45d81571fb1", + "checksum": "874aed9afe31e275f1b0c1c6e1edee90fb4729a364cc32703e7809d2eff5f1d2", "events": [], "fighters": [ { @@ -15806,6 +16342,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -15826,9 +16363,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 925, + "stamina": 928, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -15861,6 +16399,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -15872,28 +16411,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 621, + "guard": 687, "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 0, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 7, + "poise": 4, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 58, + "head": 7, "left_cut": 0, "left_eye": 0, "right_cut": 0, - "right_eye": 29, - "swelling": 14 + "right_eye": 0, + "swelling": 0 }, "velocity_x": 0, "velocity_y": 0, @@ -15909,7 +16449,7 @@ "tick": 119 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 119 }, @@ -15918,13 +16458,13 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "ef3300eca87a0a4a0ae6d314e328d4314adeabc9bcb2aab21021ed5cf04c9c85", + "checksum": "af7cfe5fc9c876219d30d28ba93880dbab45cec4d354d9467b599ea4f1011003", "events": [ { "action_id": "one@114", "actor_id": "one", - "amount": 66, - "blood": 16, + "amount": 63, + "blood": 15, "detail": "straight:head", "direction": 1, "event_id": 8, @@ -15961,6 +16501,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, @@ -15981,9 +16522,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -16016,6 +16558,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -16027,7 +16570,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -16039,16 +16582,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -16064,7 +16608,7 @@ "tick": 120 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 120 }, @@ -16073,7 +16617,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "b2e3d9d1849e4abcca3a0b4e3efed8f598f5326dda2de89cb4e15f7843b63669", + "checksum": "e79a44f40512ac21fd02a050620f3bed948fd943a0d97f4150447f2e358ef7ac", "events": [], "fighters": [ { @@ -16091,11 +16635,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, "facing_x": 1000, - "facing_y": 0, + "facing_y": -17, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -16111,9 +16656,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -16126,10 +16672,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, "x": 186, - "y": 0 + "y": 2 }, { "action": null, @@ -16146,6 +16692,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -16157,7 +16704,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -16169,16 +16716,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -16194,7 +16742,7 @@ "tick": 121 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 121 }, @@ -16203,7 +16751,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "1174eaa6f18a72f942c80626b36bdefc685b472365f8e33b247ecd93ae331ecb", + "checksum": "f5d3192a244ec9b0ed2114f4528f146055afc289aded35d18a09f85875f013e9", "events": [], "fighters": [ { @@ -16221,11 +16769,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, "facing_x": 1000, - "facing_y": 0, + "facing_y": -42, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -16241,9 +16790,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -16256,10 +16806,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, "x": 182, - "y": 0 + "y": 5 }, { "action": null, @@ -16276,6 +16826,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -16287,7 +16838,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -16299,16 +16850,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -16324,7 +16876,7 @@ "tick": 122 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 122 }, @@ -16333,7 +16885,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "d65b07f2473a8cd65fe2ce4c65b710f6f0e42195e1797b5c4d55480e4a7cbdaf", + "checksum": "b835e04096b3b4b7f51bb4bb9e92badeab0d8656812c529af48a07b22c862b4e", "events": [], "fighters": [ { @@ -16351,11 +16903,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, "facing_x": 1000, - "facing_y": 0, + "facing_y": -65, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -16371,9 +16924,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -16386,10 +16940,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, "x": 178, - "y": 0 + "y": 8 }, { "action": null, @@ -16406,6 +16960,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -16417,7 +16972,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -16429,16 +16984,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -16454,7 +17010,7 @@ "tick": 123 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 123 }, @@ -16463,7 +17019,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "2addde5d8de5fc0033c49826168b9773e3e47a061cbf78cd679554a89c660330", + "checksum": "10c4a0e22700fe04d87ccc61d547ee1d3ea656ca0e0d64c442a67f5380e30d7d", "events": [], "fighters": [ { @@ -16481,11 +17037,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, "facing_x": 1000, - "facing_y": 0, + "facing_y": -87, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -16501,9 +17058,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -16516,10 +17074,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, "x": 174, - "y": 0 + "y": 11 }, { "action": null, @@ -16536,6 +17094,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -16547,7 +17106,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -16559,16 +17118,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -16584,7 +17144,7 @@ "tick": 124 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 124 }, @@ -16593,7 +17153,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "74820874a91cfb77959465bf7eaf25f2d0c6edd2167eac2cbc42058e13d2d7f9", + "checksum": "42c0d6973666ab0b4403ee3534e51473869ff7dd266752977d4899140177ffef", "events": [], "fighters": [ { @@ -16611,11 +17171,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, "facing_x": 1000, - "facing_y": 0, + "facing_y": -100, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -16631,9 +17192,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -16646,10 +17208,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, "x": 170, - "y": 0 + "y": 13 }, { "action": null, @@ -16666,6 +17228,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -16677,7 +17240,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -16689,16 +17252,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -16714,7 +17278,7 @@ "tick": 125 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 125 }, @@ -16723,7 +17287,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "50d8cb6754a93f0c8e50e83b25f2a4845044469df6e5e2d4a98c6d74b3e6b39c", + "checksum": "441373cffa61d2f74e0d46129319db441fc2349904ec0a9ff4a4cba012fc8e66", "events": [], "fighters": [ { @@ -16741,11 +17305,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, "facing_x": 1000, - "facing_y": 0, + "facing_y": -118, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -16761,9 +17326,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -16776,10 +17342,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 166, - "y": 0 + "x": 165, + "y": 16 }, { "action": null, @@ -16796,6 +17362,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -16807,7 +17374,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -16819,16 +17386,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -16844,7 +17412,7 @@ "tick": 126 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 126 }, @@ -16853,7 +17421,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "c7088f657933db140de36c742017e2fecb6366126b7031d02ee03e31a092ed6f", + "checksum": "5ee59dff45db7040b47766ba863bcd6b9b95bfb6c5c737b67c45277526f079a1", "events": [], "fighters": [ { @@ -16871,11 +17439,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 992, + "facing_y": -135, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -16891,9 +17460,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -16906,10 +17476,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 162, - "y": 0 + "x": 161, + "y": 19 }, { "action": null, @@ -16926,6 +17496,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -16937,7 +17508,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -16949,16 +17520,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -16974,7 +17546,7 @@ "tick": 127 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 127 }, @@ -16983,7 +17555,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "597ba68542cebf6d3813906480b505d81f3cb14bcd3f074d417ebe7fdc002760", + "checksum": "f047c9e80998878e5d2e7ddeabee1879f061a77ae965e92c2a3a6767484ac6d6", "events": [], "fighters": [ { @@ -17001,11 +17573,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 993, + "facing_y": -145, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -17021,9 +17594,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -17036,10 +17610,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 158, - "y": 0 + "x": 157, + "y": 21 }, { "action": null, @@ -17056,6 +17630,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -17067,7 +17642,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -17079,16 +17654,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -17104,7 +17680,7 @@ "tick": 128 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 128 }, @@ -17113,7 +17689,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "6945fe681a2293eabad2515b785f65514ca6a437202d21123ea2e4b92e26c6fa", + "checksum": "2297c24bfdc66fed8da1f59434d00dcf9cd9777862a8a66e93ffe8315493a460", "events": [], "fighters": [ { @@ -17131,11 +17707,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 993, + "facing_y": -162, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -17151,9 +17728,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -17166,10 +17744,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 154, - "y": 0 + "x": 153, + "y": 24 }, { "action": null, @@ -17186,6 +17764,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -17197,7 +17776,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -17209,16 +17788,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -17234,7 +17814,7 @@ "tick": 129 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 129 }, @@ -17243,7 +17823,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "c537ff465d237d4de526ed55bc38ec5a53694bf1455df9433d8c2e2bb10cb3db", + "checksum": "304640aad5b259e0b846321fdfc72505e19a43ead8f1e2a5a3ac5b4f5ec37693", "events": [], "fighters": [ { @@ -17261,11 +17841,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 986, + "facing_y": -176, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -17281,9 +17862,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -17296,10 +17878,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 150, - "y": 0 + "x": 149, + "y": 27 }, { "action": null, @@ -17316,6 +17898,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -17327,7 +17910,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -17339,16 +17922,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -17364,7 +17948,7 @@ "tick": 130 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 130 }, @@ -17373,7 +17957,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "4e038ea8df16548909dda5dbcc393c241976797cce99f29f68b435e281ab5e5d", + "checksum": "81931ad2ed9eb371169abcae69ef0a431bf22e2f2c22a12c07193e8d34e552ec", "events": [], "fighters": [ { @@ -17391,11 +17975,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 987, + "facing_y": -189, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -17411,9 +17996,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -17426,10 +18012,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 146, - "y": 0 + "x": 144, + "y": 30 }, { "action": null, @@ -17446,6 +18032,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -17457,7 +18044,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -17469,16 +18056,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -17494,7 +18082,7 @@ "tick": 131 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 131 }, @@ -17503,7 +18091,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "3e73d48af7e099a16ae4844b53ca04fb4b4ccfb1bc699fd88001e99bd3c1db73", + "checksum": "89a981ce93eb5d3e7e1ed33572ba289945355ca64b9eed386e9848300b480c07", "events": [], "fighters": [ { @@ -17521,11 +18109,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 981, + "facing_y": -196, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -17541,9 +18130,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -17556,10 +18146,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 142, - "y": 0 + "x": 140, + "y": 32 }, { "action": null, @@ -17576,6 +18166,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -17587,7 +18178,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -17599,16 +18190,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -17624,7 +18216,7 @@ "tick": 132 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 132 }, @@ -17633,7 +18225,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "7c2cd5464b11dfac118eb92520143427be8310c65d4796585db4f19aed20b00e", + "checksum": "6a0e3d855fdcf9ea1e25f9b7e4cc3dec71d13d46c084bb18f0c4a7769cb93967", "events": [], "fighters": [ { @@ -17651,11 +18243,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 982, + "facing_y": -209, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -17671,9 +18264,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -17686,10 +18280,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 138, - "y": 0 + "x": 136, + "y": 35 }, { "action": null, @@ -17706,6 +18300,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -17717,7 +18312,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -17729,16 +18324,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -17754,7 +18350,7 @@ "tick": 133 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 133 }, @@ -17763,7 +18359,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "cfa0f946444720c438845326e996f5e30b87b3b6a4a8e292850cc89fb89ec32b", + "checksum": "2e38744d9a676570581a5e2c1ef8659098da7faee94f49388a036a56595a276d", "events": [], "fighters": [ { @@ -17781,11 +18377,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 976, + "facing_y": -220, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -17801,9 +18398,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -17816,10 +18414,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 134, - "y": 0 + "x": 132, + "y": 38 }, { "action": null, @@ -17836,6 +18434,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -17847,7 +18446,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -17859,16 +18458,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -17884,7 +18484,7 @@ "tick": 134 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 134 }, @@ -17893,7 +18493,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "477b30da03c6a7cdc98dd85adf5784fd6ade85e352c4112fde7d227f71d9db15", + "checksum": "5fe9f2ff6b727ba0ae7b7e9a8f53d85f77dc09e0d8600e239eec907af83597ab", "events": [], "fighters": [ { @@ -17911,11 +18511,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 977, + "facing_y": -232, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -17931,9 +18532,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -17946,10 +18548,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 130, - "y": 0 + "x": 128, + "y": 41 }, { "action": null, @@ -17966,6 +18568,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -17977,7 +18580,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -17989,16 +18592,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -18014,7 +18618,7 @@ "tick": 135 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 135 }, @@ -18023,7 +18627,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "8c09645e5734dbde61a3149e94dae7426798d675bfef5ea367bb3e071f3e29e2", + "checksum": "a7b6516397f42c9dd1bc116327182c787d44ba1f74c207f06ce83587e13214ed", "events": [], "fighters": [ { @@ -18041,11 +18645,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 972, + "facing_y": -237, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -18061,9 +18666,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -18076,10 +18682,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 126, - "y": 0 + "x": 124, + "y": 43 }, { "action": null, @@ -18096,6 +18702,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -18107,7 +18714,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -18119,16 +18726,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -18144,7 +18752,7 @@ "tick": 136 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 136 }, @@ -18153,7 +18761,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "77f514433307710e56f00f7c894bec2a63a1f5a0ab4a15867a83934bd1a36008", + "checksum": "7d742f4e416c0dc6930603e338e6b154fb02dfee04ce9aa4370fb4052a750d8c", "events": [], "fighters": [ { @@ -18171,11 +18779,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 973, + "facing_y": -247, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -18191,9 +18800,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -18206,10 +18816,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 122, - "y": 0 + "x": 119, + "y": 46 }, { "action": null, @@ -18226,6 +18836,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -18237,7 +18848,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -18249,16 +18860,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -18274,7 +18886,7 @@ "tick": 137 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 137 }, @@ -18283,7 +18895,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "0e3737ddec344b104a48f557c44d0d361c892b02934245e66f310250988b9b50", + "checksum": "66d2736e203fc7fb812ad7829e8ac2abb95af4877fe25477e2e8f1e3005c2c3b", "events": [], "fighters": [ { @@ -18301,11 +18913,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 968, + "facing_y": -256, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -18321,9 +18934,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -18336,10 +18950,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 118, - "y": 0 + "x": 115, + "y": 49 }, { "action": null, @@ -18356,6 +18970,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -18367,7 +18982,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -18379,16 +18994,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -18404,7 +19020,7 @@ "tick": 138 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 138 }, @@ -18413,7 +19029,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "ebde7ecdbe4ddb900ff714cf9de9df0ddb3c6342e978d7f6085207c2bd31f521", + "checksum": "015c8f7162933b0f472d8bd9642151287def64e86a3d43c04fd438d290ce9b21", "events": [], "fighters": [ { @@ -18431,11 +19047,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 964, + "facing_y": -265, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -18451,9 +19068,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -18466,10 +19084,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 114, - "y": 0 + "x": 111, + "y": 52 }, { "action": null, @@ -18486,6 +19104,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -18497,7 +19116,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -18509,16 +19128,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -18534,7 +19154,7 @@ "tick": 139 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 139 }, @@ -18543,7 +19163,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "76029bb5d2738ede74d36ea42daa05f9f0ac29690a8976226a2f8231eb5a5943", + "checksum": "f0586b76709d72e2782085c474981665f8e46bc313a30172d0c38d536685ad5d", "events": [], "fighters": [ { @@ -18561,11 +19181,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 965, + "facing_y": -270, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -18581,9 +19202,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -18596,10 +19218,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 110, - "y": 0 + "x": 107, + "y": 54 }, { "action": null, @@ -18616,6 +19238,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -18627,7 +19250,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -18639,16 +19262,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -18664,7 +19288,7 @@ "tick": 140 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 140 }, @@ -18673,7 +19297,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "fa877bdf0db2d76b2ef6087116d776cc5008df4375820bc6131a6134300bb194", + "checksum": "3b07d2cc8b609d1bdd7e4cc505814a3d92233e7e0b71426ab056d8d236d1e7c9", "events": [], "fighters": [ { @@ -18691,11 +19315,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 960, + "facing_y": -278, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -18711,9 +19336,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -18726,10 +19352,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 106, - "y": 0 + "x": 103, + "y": 57 }, { "action": null, @@ -18746,6 +19372,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -18757,7 +19384,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -18769,16 +19396,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -18794,7 +19422,7 @@ "tick": 141 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 141 }, @@ -18803,7 +19431,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "25fc0a37cac2871da5379944218facc163d2ca47ccb1b59635f073c34298663a", + "checksum": "4533d079b46f21801a4868807fd8b275e4839bb7167c432ef40ee79e20fa2f54", "events": [], "fighters": [ { @@ -18821,11 +19449,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 961, + "facing_y": -287, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -18841,9 +19470,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -18856,10 +19486,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 102, - "y": 0 + "x": 99, + "y": 60 }, { "action": null, @@ -18876,6 +19506,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -18887,7 +19518,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -18899,16 +19530,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -18924,7 +19556,7 @@ "tick": 142 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 142 }, @@ -18933,7 +19565,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "1b6cebe67b1196786dca1506337e0061ca6a483cd2cc8448b2523c3f4eba114e", + "checksum": "61412b86b3204d7ffbf67b8776ca5862483d330eaec6142db32a50c2f75517fb", "events": [], "fighters": [ { @@ -18951,11 +19583,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 958, + "facing_y": -293, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -18971,9 +19604,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -18986,10 +19620,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 98, - "y": 0 + "x": 94, + "y": 63 }, { "action": null, @@ -19006,6 +19640,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -19017,7 +19652,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -19029,16 +19664,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -19054,7 +19690,7 @@ "tick": 143 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 143 }, @@ -19063,7 +19699,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "7101bed01789c9119012cc06bb94f3e83bada9e68495f9d52c4ac3e41c84a9bf", + "checksum": "d332b880c542342f89caebf22c6e1a6ebbb229fb13c5c57c98d3818a9c732e78", "events": [], "fighters": [ { @@ -19081,11 +19717,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 958, + "facing_y": -296, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -19101,9 +19738,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -19116,10 +19754,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 94, - "y": 0 + "x": 90, + "y": 65 }, { "action": null, @@ -19136,6 +19774,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -19147,7 +19786,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -19159,16 +19798,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -19184,7 +19824,7 @@ "tick": 144 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 144 }, @@ -19193,7 +19833,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "5631f87269d3e6bba9189a84e491c1096c36b6cab4f729a8c2f9581cf98cf2a9", + "checksum": "9cca1817a773c68787afb62c25072e6311c8d499d0320c2b6f37df495349d30f", "events": [], "fighters": [ { @@ -19211,11 +19851,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 955, + "facing_y": -303, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -19231,9 +19872,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -19246,10 +19888,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 90, - "y": 0 + "x": 86, + "y": 68 }, { "action": null, @@ -19266,6 +19908,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -19277,7 +19920,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -19289,16 +19932,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -19314,7 +19958,7 @@ "tick": 145 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 145 }, @@ -19323,7 +19967,7 @@ "attack_two": null, "snapshot": { "payload": { - "checksum": "13a62360b3f9b9b70098ca4c185fa9e36880a90aef0c34df962b4f6e5bd60fe7", + "checksum": "e8a555651b5e517a3b766687c9c778a03765e385666634ba4a6a7dafde0a14bf", "events": [], "fighters": [ { @@ -19341,11 +19985,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 951, + "facing_y": -310, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -19361,9 +20006,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -19376,140 +20022,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, - "warnings": 0, - "x": 86, - "y": 0 - }, - { - "action": null, - "action_active_ticks": 0, - "action_contact_tick": null, - "action_hand": null, - "action_id": null, - "action_key": null, - "action_power": null, - "action_recovery_ticks": 0, - "action_start_tick": 0, - "action_startup_ticks": 0, - "action_target": null, - "clinch_startup_ticks": 0, - "clinch_ticks": 0, - "conditioning": 1000, - "deductions": 0, - "defense": "none", - "facing": -1, - "facing_x": -1000, - "facing_y": 0, - "get_up_count": 0, - "get_up_meter": 0, - "get_up_prompt": null, - "get_up_required": 0, - "get_up_window_end_tick": 0, - "get_up_window_start_tick": 0, - "guard": 624, - "is_downed": true, - "is_foul_recovery_target": false, - "knockdowns": 1, - "last_input_sequence": -1, - "maximum_stamina": 1000, - "player_id": "two", - "poise": 1, - "queued_actions": 0, - "stamina": 1000, - "stance": "orthodox", - "stunned_ticks": 0, - "taunt_ticks": 0, - "trauma": { - "bleeding": 0, - "body": 0, - "head": 190, - "left_cut": 0, - "left_eye": 66, - "right_cut": 0, - "right_eye": 29, - "swelling": 47 - }, - "velocity_x": 0, - "velocity_y": 0, - "warnings": 0, - "x": 300, - "y": 0 - } - ], - "phase": "knockdown", - "phase_ticks_remaining": 274, - "result": null, - "round_number": 1, - "tick": 146 - }, - "type": "snapshot", - "version": 3 - }, - "tick": 146 - }, - { - "attack_one": null, - "attack_two": null, - "snapshot": { - "payload": { - "checksum": "886fbd4216a22b966e729cffee9ef280eee917eeab785017a0d62963c285ef77", - "events": [], - "fighters": [ - { - "action": null, - "action_active_ticks": 0, - "action_contact_tick": null, - "action_hand": null, - "action_id": null, - "action_key": null, - "action_power": null, - "action_recovery_ticks": 0, - "action_start_tick": 0, - "action_startup_ticks": 0, - "action_target": null, - "clinch_startup_ticks": 0, - "clinch_ticks": 0, - "conditioning": 990, - "deductions": 0, - "defense": "none", - "facing": 1, - "facing_x": 1000, - "facing_y": 0, - "get_up_count": 0, - "get_up_meter": 0, - "get_up_prompt": null, - "get_up_required": 34, - "get_up_window_end_tick": 0, - "get_up_window_start_tick": 0, - "guard": 700, - "is_downed": false, - "is_foul_recovery_target": false, - "knockdowns": 0, - "last_input_sequence": 84, - "maximum_stamina": 996, - "player_id": "one", - "poise": 600, - "queued_actions": 0, - "stamina": 929, - "stance": "orthodox", - "stunned_ticks": 0, - "taunt_ticks": 0, - "trauma": { - "bleeding": 0, - "body": 0, - "head": 0, - "left_cut": 0, - "left_eye": 0, - "right_cut": 0, - "right_eye": 0, - "swelling": 0 - }, - "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, "x": 82, - "y": 0 + "y": 71 }, { "action": null, @@ -19526,6 +20042,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -19537,7 +20054,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -19549,16 +20066,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -19568,22 +20086,22 @@ } ], "phase": "knockdown", - "phase_ticks_remaining": 273, + "phase_ticks_remaining": 274, "result": null, "round_number": 1, - "tick": 147 + "tick": 146 }, "type": "snapshot", - "version": 3 + "version": 4 }, - "tick": 147 + "tick": 146 }, { "attack_one": null, "attack_two": null, "snapshot": { "payload": { - "checksum": "27c364905e99c293690c5696799a3369f37aca30a2c36448e8c056fa3bd2872a", + "checksum": "b72e554dd0031aaeb96364a54124b51688735442179091346c4aa62faf5d0a1c", "events": [], "fighters": [ { @@ -19601,11 +20119,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 948, + "facing_y": -316, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -19621,9 +20140,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -19636,10 +20156,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, "x": 78, - "y": 0 + "y": 74 }, { "action": null, @@ -19656,6 +20176,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -19667,7 +20188,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -19679,16 +20200,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -19698,22 +20220,22 @@ } ], "phase": "knockdown", - "phase_ticks_remaining": 272, + "phase_ticks_remaining": 273, "result": null, "round_number": 1, - "tick": 148 + "tick": 147 }, "type": "snapshot", - "version": 3 + "version": 4 }, - "tick": 148 + "tick": 147 }, { "attack_one": null, "attack_two": null, "snapshot": { "payload": { - "checksum": "730ee0710a7eb528a7630ca04dfc5de3560633a73498ebba7fbfa72e476a0158", + "checksum": "a4e80a4bea893983578ee791d26063c903157bad4a8054202de568f1802c2253", "events": [], "fighters": [ { @@ -19731,11 +20253,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 949, + "facing_y": -317, "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, @@ -19751,9 +20274,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -19766,10 +20290,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 74, - "y": 0 + "x": 73, + "y": 76 }, { "action": null, @@ -19786,6 +20310,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -19797,7 +20322,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -19809,16 +20334,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -19828,36 +20354,23 @@ } ], "phase": "knockdown", - "phase_ticks_remaining": 271, + "phase_ticks_remaining": 272, "result": null, "round_number": 1, - "tick": 149 + "tick": 148 }, "type": "snapshot", - "version": 3 + "version": 4 }, - "tick": 149 + "tick": 148 }, { "attack_one": null, "attack_two": null, "snapshot": { "payload": { - "checksum": "59b81f779e26816182cae754449cc4ed4d2b469131ad05dae7aed6c73b3b1b85", - "events": [ - { - "action_id": null, - "actor_id": null, - "amount": 1, - "blood": 0, - "detail": "", - "direction": 0, - "event_id": 10, - "kind": "count", - "target_id": "two", - "tick": 150 - } - ], + "checksum": "a9e847cdd236f712275caba0b425aa4db526987281ef72a5ada0114ba5ee0cbb", + "events": [], "fighters": [ { "action": null, @@ -19874,12 +20387,13 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, - "get_up_count": 1, + "facing_x": 946, + "facing_y": -323, + "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, "get_up_required": 34, @@ -19894,9 +20408,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -19909,10 +20424,10 @@ "swelling": 0 }, "velocity_x": -4, - "velocity_y": 0, + "velocity_y": 3, "warnings": 0, - "x": 70, - "y": 0 + "x": 69, + "y": 79 }, { "action": null, @@ -19929,18 +20444,19 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, "facing_x": -1000, "facing_y": 0, - "get_up_count": 1, + "get_up_count": 0, "get_up_meter": 0, "get_up_prompt": null, "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -19952,16 +20468,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -19971,23 +20488,36 @@ } ], "phase": "knockdown", - "phase_ticks_remaining": 270, + "phase_ticks_remaining": 271, "result": null, "round_number": 1, - "tick": 150 + "tick": 149 }, "type": "snapshot", - "version": 3 + "version": 4 }, - "tick": 150 + "tick": 149 }, { "attack_one": null, "attack_two": null, "snapshot": { "payload": { - "checksum": "a3f1c18fcc3213ceb1509035909e615e89745df7203891e3e39450db7bd52f81", - "events": [], + "checksum": "920f8551a2efbef33ca103e524f5afc085881cc634ba0cea7eb5bdc1403c3285", + "events": [ + { + "action_id": null, + "actor_id": null, + "amount": 1, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 12, + "kind": "count", + "target_id": "two", + "tick": 150 + } + ], "fighters": [ { "action": null, @@ -20004,11 +20534,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 947, + "facing_y": -330, "get_up_count": 1, "get_up_meter": 0, "get_up_prompt": null, @@ -20024,9 +20555,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -20038,11 +20570,11 @@ "right_eye": 0, "swelling": 0 }, - "velocity_x": 0, - "velocity_y": 0, + "velocity_x": -4, + "velocity_y": 3, "warnings": 0, - "x": 70, - "y": 0 + "x": 65, + "y": 82 }, { "action": null, @@ -20059,6 +20591,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -20070,7 +20603,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -20082,16 +20615,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -20101,22 +20635,22 @@ } ], "phase": "knockdown", - "phase_ticks_remaining": 269, + "phase_ticks_remaining": 270, "result": null, "round_number": 1, - "tick": 151 + "tick": 150 }, "type": "snapshot", - "version": 3 + "version": 4 }, - "tick": 151 + "tick": 150 }, { "attack_one": null, "attack_two": null, "snapshot": { "payload": { - "checksum": "8f6fee699ff7bcde333292125408c93e477777eb5d4be9e3450532e2ca0529a0", + "checksum": "ace0ccaf5cd91bebdd17a6a1e9ede953cffe4f79af0dae8175d56ec8e7d09cb3", "events": [], "fighters": [ { @@ -20134,11 +20668,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 944, + "facing_y": -335, "get_up_count": 1, "get_up_meter": 0, "get_up_prompt": null, @@ -20154,9 +20689,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -20168,11 +20704,11 @@ "right_eye": 0, "swelling": 0 }, - "velocity_x": 0, - "velocity_y": 0, + "velocity_x": -4, + "velocity_y": 3, "warnings": 0, - "x": 70, - "y": 0 + "x": 61, + "y": 85 }, { "action": null, @@ -20189,6 +20725,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -20200,7 +20737,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -20212,16 +20749,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -20231,22 +20769,22 @@ } ], "phase": "knockdown", - "phase_ticks_remaining": 268, + "phase_ticks_remaining": 269, "result": null, "round_number": 1, - "tick": 152 + "tick": 151 }, "type": "snapshot", - "version": 3 + "version": 4 }, - "tick": 152 + "tick": 151 }, { "attack_one": null, "attack_two": null, "snapshot": { "payload": { - "checksum": "36e0bc8c191c95adae86989087ae72ed78b2044e3d921d160a078e4d46dc6886", + "checksum": "4adef4b53fe7e40bee8bd8507350215ae50106ad02d48ff7cd8cd143c0a3a0a2", "events": [], "fighters": [ { @@ -20264,11 +20802,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 941, + "facing_y": -337, "get_up_count": 1, "get_up_meter": 0, "get_up_prompt": null, @@ -20284,9 +20823,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -20298,11 +20838,11 @@ "right_eye": 0, "swelling": 0 }, - "velocity_x": 0, - "velocity_y": 0, + "velocity_x": -4, + "velocity_y": 3, "warnings": 0, - "x": 70, - "y": 0 + "x": 57, + "y": 87 }, { "action": null, @@ -20319,6 +20859,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -20330,7 +20871,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -20342,16 +20883,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -20361,22 +20903,22 @@ } ], "phase": "knockdown", - "phase_ticks_remaining": 267, + "phase_ticks_remaining": 268, "result": null, "round_number": 1, - "tick": 153 + "tick": 152 }, "type": "snapshot", - "version": 3 + "version": 4 }, - "tick": 153 + "tick": 152 }, { "attack_one": null, "attack_two": null, "snapshot": { "payload": { - "checksum": "31a8018b3a11cc2db8aaf6162afba5b282c68612543bfe2396b07d03b0499c2c", + "checksum": "8f8ebd6b9ea0b101aa8b9572eff7a8840d4e171172082f672e9919927b7315d7", "events": [], "fighters": [ { @@ -20394,11 +20936,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 942, + "facing_y": -343, "get_up_count": 1, "get_up_meter": 0, "get_up_prompt": null, @@ -20414,9 +20957,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -20428,11 +20972,11 @@ "right_eye": 0, "swelling": 0 }, - "velocity_x": 0, - "velocity_y": 0, + "velocity_x": -4, + "velocity_y": 3, "warnings": 0, - "x": 70, - "y": 0 + "x": 53, + "y": 90 }, { "action": null, @@ -20449,6 +20993,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -20460,7 +21005,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -20472,16 +21017,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -20491,22 +21037,22 @@ } ], "phase": "knockdown", - "phase_ticks_remaining": 266, + "phase_ticks_remaining": 267, "result": null, "round_number": 1, - "tick": 154 + "tick": 153 }, "type": "snapshot", - "version": 3 + "version": 4 }, - "tick": 154 + "tick": 153 }, { "attack_one": null, "attack_two": null, "snapshot": { "payload": { - "checksum": "0528432162965c90a6446e8e12ea6a87c6234a3fb747d1cdf2c1d0a5b8103e01", + "checksum": "2e8006520f9250178b5e4467822db72ee529a476142b17e01ca0dcb0cac66c80", "events": [], "fighters": [ { @@ -20524,11 +21070,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 940, + "facing_y": -347, "get_up_count": 1, "get_up_meter": 0, "get_up_prompt": null, @@ -20544,9 +21091,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -20558,11 +21106,11 @@ "right_eye": 0, "swelling": 0 }, - "velocity_x": 0, - "velocity_y": 0, + "velocity_x": -4, + "velocity_y": 3, "warnings": 0, - "x": 70, - "y": 0 + "x": 48, + "y": 93 }, { "action": null, @@ -20579,6 +21127,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -20590,7 +21139,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -20602,16 +21151,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -20621,22 +21171,22 @@ } ], "phase": "knockdown", - "phase_ticks_remaining": 265, + "phase_ticks_remaining": 266, "result": null, "round_number": 1, - "tick": 155 + "tick": 154 }, "type": "snapshot", - "version": 3 + "version": 4 }, - "tick": 155 + "tick": 154 }, { "attack_one": null, "attack_two": null, "snapshot": { "payload": { - "checksum": "c7dac0663e3a13f067e28e335b2962f2fc87c921d8350c9b0f12493de110c4fd", + "checksum": "cc7fc5de3743fbcd22dae57afe187e8f2df43da2719bb4ebda04fe45c29eae97", "events": [], "fighters": [ { @@ -20654,11 +21204,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 937, + "facing_y": -351, "get_up_count": 1, "get_up_meter": 0, "get_up_prompt": null, @@ -20674,9 +21225,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -20688,11 +21240,11 @@ "right_eye": 0, "swelling": 0 }, - "velocity_x": 0, - "velocity_y": 0, + "velocity_x": -4, + "velocity_y": 3, "warnings": 0, - "x": 70, - "y": 0 + "x": 44, + "y": 96 }, { "action": null, @@ -20709,6 +21261,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -20720,7 +21273,7 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, + "guard": 690, "is_downed": true, "is_foul_recovery_target": false, "knockdowns": 1, @@ -20732,16 +21285,17 @@ "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -20751,23 +21305,36 @@ } ], "phase": "knockdown", - "phase_ticks_remaining": 264, + "phase_ticks_remaining": 265, "result": null, "round_number": 1, - "tick": 156 + "tick": 155 }, "type": "snapshot", - "version": 3 + "version": 4 }, - "tick": 156 + "tick": 155 }, { "attack_one": null, "attack_two": null, "snapshot": { "payload": { - "checksum": "639bc6953b7dd799bf995b60af6d8c7d2a9e3948e1aa128e55c6ac09e44e8c59", - "events": [], + "checksum": "5cdce839302d930102a29ebfcd43b1065caa4a087461c3d23397b0ebe97aa931", + "events": [ + { + "action_id": null, + "actor_id": "two", + "amount": 1, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 14, + "kind": "get_up", + "target_id": null, + "tick": 156 + } + ], "fighters": [ { "action": null, @@ -20784,11 +21351,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 938, + "facing_y": -353, "get_up_count": 1, "get_up_meter": 0, "get_up_prompt": null, @@ -20804,9 +21372,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -20818,11 +21387,11 @@ "right_eye": 0, "swelling": 0 }, - "velocity_x": 0, - "velocity_y": 0, + "velocity_x": -4, + "velocity_y": 3, "warnings": 0, - "x": 70, - "y": 0 + "x": 40, + "y": 98 }, { "action": null, @@ -20839,6 +21408,7 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, @@ -20850,28 +21420,29 @@ "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, - "is_downed": true, + "guard": 690, + "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 1, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 1, + "poise": 300, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -20881,22 +21452,22 @@ } ], "phase": "knockdown", - "phase_ticks_remaining": 263, + "phase_ticks_remaining": 264, "result": null, "round_number": 1, - "tick": 157 + "tick": 156 }, "type": "snapshot", - "version": 3 + "version": 4 }, - "tick": 157 + "tick": 156 }, { "attack_one": null, "attack_two": null, "snapshot": { "payload": { - "checksum": "4e31b967cc06ad097162bb6e48254df8340e74b85dbf2e74a6f70c01e71b2088", + "checksum": "cb1b78df22eb2f35e3006864aa4302f88a6aa3b0b1e5bebbeaff4670205e9eee", "events": [], "fighters": [ { @@ -20914,11 +21485,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 936, + "facing_y": -358, "get_up_count": 1, "get_up_meter": 0, "get_up_prompt": null, @@ -20934,9 +21506,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -20948,11 +21521,11 @@ "right_eye": 0, "swelling": 0 }, - "velocity_x": 0, - "velocity_y": 0, + "velocity_x": -4, + "velocity_y": 3, "warnings": 0, - "x": 70, - "y": 0 + "x": 36, + "y": 101 }, { "action": null, @@ -20969,39 +21542,41 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, - "facing_x": -1000, - "facing_y": 0, + "facing_x": -999, + "facing_y": 44, "get_up_count": 1, "get_up_meter": 0, "get_up_prompt": null, "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, - "is_downed": true, + "guard": 690, + "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 1, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 1, + "poise": 300, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -21011,22 +21586,22 @@ } ], "phase": "knockdown", - "phase_ticks_remaining": 262, + "phase_ticks_remaining": 263, "result": null, "round_number": 1, - "tick": 158 + "tick": 157 }, "type": "snapshot", - "version": 3 + "version": 4 }, - "tick": 158 + "tick": 157 }, { "attack_one": null, "attack_two": null, "snapshot": { "payload": { - "checksum": "fb1c58018ff89ef200def3885fe7af3c549690324bbea4847393f6e2adaeaf6c", + "checksum": "e3cf9cc054d90e273711d296b0d4022150b7513b9d4e02751a044be7634a0d96", "events": [], "fighters": [ { @@ -21044,11 +21619,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 933, + "facing_y": -362, "get_up_count": 1, "get_up_meter": 0, "get_up_prompt": null, @@ -21064,9 +21640,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -21078,11 +21655,11 @@ "right_eye": 0, "swelling": 0 }, - "velocity_x": 0, - "velocity_y": 0, + "velocity_x": -4, + "velocity_y": 3, "warnings": 0, - "x": 70, - "y": 0 + "x": 32, + "y": 104 }, { "action": null, @@ -21099,39 +21676,41 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, - "facing_x": -1000, - "facing_y": 0, + "facing_x": -996, + "facing_y": 88, "get_up_count": 1, "get_up_meter": 0, "get_up_prompt": null, "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, - "is_downed": true, + "guard": 690, + "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 1, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 1, + "poise": 300, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -21141,22 +21720,22 @@ } ], "phase": "knockdown", - "phase_ticks_remaining": 261, + "phase_ticks_remaining": 262, "result": null, "round_number": 1, - "tick": 159 + "tick": 158 }, "type": "snapshot", - "version": 3 + "version": 4 }, - "tick": 159 + "tick": 158 }, { "attack_one": null, "attack_two": null, "snapshot": { "payload": { - "checksum": "0f4aa8b6a1528a7df67e84927dbcc77c4c852d616094bfe20777b72a811cbec9", + "checksum": "fbe27f56f776199e75e28513335876e0a83608039be1b9f5dc2a24df36f2baee", "events": [], "fighters": [ { @@ -21174,11 +21753,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 931, + "facing_y": -366, "get_up_count": 1, "get_up_meter": 0, "get_up_prompt": null, @@ -21194,9 +21774,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -21208,11 +21789,11 @@ "right_eye": 0, "swelling": 0 }, - "velocity_x": 0, - "velocity_y": 0, + "velocity_x": -4, + "velocity_y": 3, "warnings": 0, - "x": 70, - "y": 0 + "x": 28, + "y": 107 }, { "action": null, @@ -21229,39 +21810,41 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, - "facing_x": -1000, - "facing_y": 0, + "facing_x": -991, + "facing_y": 131, "get_up_count": 1, "get_up_meter": 0, "get_up_prompt": null, "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, - "is_downed": true, + "guard": 690, + "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 1, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 1, + "poise": 300, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -21271,22 +21854,22 @@ } ], "phase": "knockdown", - "phase_ticks_remaining": 260, + "phase_ticks_remaining": 261, "result": null, "round_number": 1, - "tick": 160 + "tick": 159 }, "type": "snapshot", - "version": 3 + "version": 4 }, - "tick": 160 + "tick": 159 }, { "attack_one": null, "attack_two": null, "snapshot": { "payload": { - "checksum": "3509f66a4f71a6054d0ca754fb42dec21759bd34f0e53302287e75c243f0c8b7", + "checksum": "adf5531f72906628b27d9d840550c78980760b6e3b5c474b4f5de61f1c4cf017", "events": [], "fighters": [ { @@ -21304,11 +21887,12 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 990, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": 1, - "facing_x": 1000, - "facing_y": 0, + "facing_x": 932, + "facing_y": -367, "get_up_count": 1, "get_up_meter": 0, "get_up_prompt": null, @@ -21324,9 +21908,10 @@ "player_id": "one", "poise": 600, "queued_actions": 0, - "stamina": 929, + "stamina": 932, "stance": "orthodox", "stunned_ticks": 0, + "style": "boxer", "taunt_ticks": 0, "trauma": { "bleeding": 0, @@ -21338,11 +21923,145 @@ "right_eye": 0, "swelling": 0 }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": 23, + "y": 109 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -985, + "facing_y": 174, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, "velocity_x": 0, "velocity_y": 0, "warnings": 0, - "x": 70, + "x": 300, "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 260, + "result": null, + "round_number": 1, + "tick": 160 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 160 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0ce934fd27806fd353c4667250b6415d7f6c39ea599e453fe29eca3d903bc99a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 930, + "facing_y": -370, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": 19, + "y": 112 }, { "action": null, @@ -21359,39 +22078,41 @@ "clinch_startup_ticks": 0, "clinch_ticks": 0, "conditioning": 1000, + "corner_choice": null, "deductions": 0, "defense": "none", "facing": -1, - "facing_x": -1000, - "facing_y": 0, + "facing_x": -976, + "facing_y": 217, "get_up_count": 1, "get_up_meter": 0, "get_up_prompt": null, "get_up_required": 0, "get_up_window_end_tick": 0, "get_up_window_start_tick": 0, - "guard": 624, - "is_downed": true, + "guard": 690, + "is_downed": false, "is_foul_recovery_target": false, "knockdowns": 1, "last_input_sequence": -1, "maximum_stamina": 1000, "player_id": "two", - "poise": 1, + "poise": 300, "queued_actions": 0, "stamina": 1000, "stance": "orthodox", "stunned_ticks": 0, + "style": "slugger", "taunt_ticks": 0, "trauma": { "bleeding": 0, "body": 0, - "head": 190, + "head": 70, "left_cut": 0, - "left_eye": 66, + "left_eye": 44, "right_cut": 0, - "right_eye": 29, - "swelling": 47 + "right_eye": 0, + "swelling": 20 }, "velocity_x": 0, "velocity_y": 0, @@ -21407,9 +22128,133495 @@ "tick": 161 }, "type": "snapshot", - "version": 3 + "version": 4 }, "tick": 161 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0652c65e624f47de54bbc3d82acdda257e4300a4b1dea69bc8794e8e1840bc2b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 928, + "facing_y": -374, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": 15, + "y": 115 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -966, + "facing_y": 259, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 258, + "result": null, + "round_number": 1, + "tick": 162 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 162 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "17184901f928444f081aeb8e62ebc8064266af26d729d752c346e58e7042d865", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 926, + "facing_y": -378, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": 11, + "y": 118 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -954, + "facing_y": 301, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 257, + "result": null, + "round_number": 1, + "tick": 163 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 163 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "21d474eda7dd49027361147515894e6b57116886cb0d09a8d4b5846a6c071bd7", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 927, + "facing_y": -379, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": 7, + "y": 120 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -940, + "facing_y": 342, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 256, + "result": null, + "round_number": 1, + "tick": 164 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 164 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9f5910d8d0a235b852f68928c8ea030475a4651e2eeabd418065b4605f2420fc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 925, + "facing_y": -381, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": 2, + "y": 123 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -925, + "facing_y": 381, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 255, + "result": null, + "round_number": 1, + "tick": 165 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 165 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f4b6c5ac7482eef3e7a31a75d91db2ad928bd412d9032d58a22c36ac45cf4d57", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 923, + "facing_y": -385, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -2, + "y": 126 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -923, + "facing_y": 385, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 254, + "result": null, + "round_number": 1, + "tick": 166 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 166 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fdc28829777945677e6b64db34760843b57e417e1583edbdee06baf0988ea5b0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 921, + "facing_y": -388, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -6, + "y": 129 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -921, + "facing_y": 388, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 253, + "result": null, + "round_number": 1, + "tick": 167 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 167 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "851579225b60953d84cf98ce1fb3ade54ccd32c0b97f50ab5e72cd9603ac4eae", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 922, + "facing_y": -389, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -10, + "y": 131 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -922, + "facing_y": 389, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 252, + "result": null, + "round_number": 1, + "tick": 168 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 168 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "476e00bc7b2a70ec9708bc685291369f477ebe47c46e3d30fe77169a5c422c2b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 920, + "facing_y": -392, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -14, + "y": 134 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -920, + "facing_y": 392, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 251, + "result": null, + "round_number": 1, + "tick": 169 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 169 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1ca7097957919dbb51eff0a106218f243cbc1811c02c6922c6d49c123ba538fa", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 919, + "facing_y": -395, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -18, + "y": 137 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -919, + "facing_y": 395, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 250, + "result": null, + "round_number": 1, + "tick": 170 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 170 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "893dce2270aed5e7007c647a184afeaacca7352a1f2a9bb1f6fb5570dc3d9d55", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 917, + "facing_y": -397, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -23, + "y": 140 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -917, + "facing_y": 397, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 249, + "result": null, + "round_number": 1, + "tick": 171 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 171 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "393bec0ba1ed53c10e07bff2e6b04c552ee55a4b8d32496a72a8f602997c54d6", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 918, + "facing_y": -398, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -27, + "y": 142 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -918, + "facing_y": 398, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 248, + "result": null, + "round_number": 1, + "tick": 172 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 172 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f37acaeccb340d8b9a975106fa8902244d646d63bace0b7521e2fd37cb8d7a66", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 916, + "facing_y": -401, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -31, + "y": 145 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -916, + "facing_y": 401, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 247, + "result": null, + "round_number": 1, + "tick": 173 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 173 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "51e8774dec7ee98338771ad3332a1668d0a2fa0fa55737acc9ee19618331db09", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 915, + "facing_y": -404, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -35, + "y": 148 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -915, + "facing_y": 404, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 246, + "result": null, + "round_number": 1, + "tick": 174 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 174 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c5020c5ddffa32500542e1c8a52fad490278cb1c664ef7655e4ffaf16d940e1a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 913, + "facing_y": -407, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -39, + "y": 151 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -913, + "facing_y": 407, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 245, + "result": null, + "round_number": 1, + "tick": 175 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 175 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2716d26c8c02d1bdc64e9c9b50bf70e3f67712a91c7de93a47a94a943b08ec48", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 914, + "facing_y": -406, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -44, + "y": 153 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -914, + "facing_y": 406, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 244, + "result": null, + "round_number": 1, + "tick": 176 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 176 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7d859f83577aa3d71c8f4b146b1cd04ff586b1ccb6eedebdb5439149387445f6", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 913, + "facing_y": -409, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -48, + "y": 156 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -913, + "facing_y": 409, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 243, + "result": null, + "round_number": 1, + "tick": 177 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 177 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5765d50347d759a9fc9b0351d7fbc8e1ab8c13e29f189b5e45e1a04dbad5fa94", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 911, + "facing_y": -411, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -52, + "y": 159 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -911, + "facing_y": 411, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 242, + "result": null, + "round_number": 1, + "tick": 178 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 178 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9bdefec3646dd3eed2f56f5ac4251de4c03be53db2044cf4dfb67872fb633fa9", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 910, + "facing_y": -414, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -56, + "y": 162 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -910, + "facing_y": 414, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 241, + "result": null, + "round_number": 1, + "tick": 179 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 179 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4c15d7df746aa9d087233f106a3fe55a69013771bc24fae1512b4d3ca105e9a2", + "events": [ + { + "action_id": null, + "actor_id": null, + "amount": 2, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 15, + "kind": "count", + "target_id": "two", + "tick": 180 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 909, + "facing_y": -416, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -60, + "y": 165 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -909, + "facing_y": 416, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 240, + "result": null, + "round_number": 1, + "tick": 180 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 180 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "34406f8a07012bea927516e774b62eca1ff90fb8bd6349bd2d66138ca51bdd8c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 910, + "facing_y": -417, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -64, + "y": 167 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -910, + "facing_y": 417, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 239, + "result": null, + "round_number": 1, + "tick": 181 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 181 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f3e223c4c923e9ac1f2747f7afb464a493359f7e3d810d04330c3af160a6fdb0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 908, + "facing_y": -418, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -69, + "y": 170 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -908, + "facing_y": 418, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 238, + "result": null, + "round_number": 1, + "tick": 182 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 182 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "059f7151593288f8f317755f66694fa3110f57f1a6c81693e823dfd57670ac61", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 907, + "facing_y": -420, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -73, + "y": 173 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -907, + "facing_y": 420, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 237, + "result": null, + "round_number": 1, + "tick": 183 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 183 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f26c873479ad6d68c8302bda9f90ce13e21721da22a7efdfe4317f26f7f1e538", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 906, + "facing_y": -423, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -77, + "y": 176 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -906, + "facing_y": 423, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 236, + "result": null, + "round_number": 1, + "tick": 184 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 184 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "93b18980e9fc21686428f433425267edb300fe2462f6c1b36aec33882172713b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 907, + "facing_y": -423, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -81, + "y": 178 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -907, + "facing_y": 423, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 235, + "result": null, + "round_number": 1, + "tick": 185 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 185 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "082da852b1a5af376473f3943349bb9f620d39d74a95e3d20ca93b2a4d77cddc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 905, + "facing_y": -425, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -85, + "y": 181 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -905, + "facing_y": 425, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 234, + "result": null, + "round_number": 1, + "tick": 186 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 186 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a0c12ff395c6cb336cca98220fd6e4f519d8488ea2e1e031defc476dc457e2e7", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 904, + "facing_y": -426, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -90, + "y": 184 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -904, + "facing_y": 426, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 233, + "result": null, + "round_number": 1, + "tick": 187 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 187 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "306a15b6e922cdcd9e324b326d3d8c1ce4a3cdfdeff3c13f896f7dfb221134e2", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 905, + "facing_y": -427, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -94, + "y": 186 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -905, + "facing_y": 427, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 232, + "result": null, + "round_number": 1, + "tick": 188 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 188 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9b7a16d2e42f757391a7a1c388f6170b6659fa62920b7d59079c9a2b16f60233", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 904, + "facing_y": -429, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -98, + "y": 189 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -904, + "facing_y": 429, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 231, + "result": null, + "round_number": 1, + "tick": 189 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 189 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d1dabac97c0796235601ecfb44cc54b2e8302bb73eb162670229676993ff1fab", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 903, + "facing_y": -431, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -102, + "y": 192 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -903, + "facing_y": 431, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 230, + "result": null, + "round_number": 1, + "tick": 190 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 190 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "62fb3de6064aab6a8d4b37b99ff577b461cd2000376005bef7ec5dba4d980dce", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 902, + "facing_y": -433, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -106, + "y": 195 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -902, + "facing_y": 433, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 229, + "result": null, + "round_number": 1, + "tick": 191 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 191 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "83549f57eff9de933785c2858b843676facdb7daf66b79f43cd393a9f1912fb9", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 901, + "facing_y": -435, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -110, + "y": 198 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -901, + "facing_y": 435, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 228, + "result": null, + "round_number": 1, + "tick": 192 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 192 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "038b354ffddcc4ae8ba7a2af04e2f4bf5f5d175772558241d52afc1baeada171", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 902, + "facing_y": -434, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -115, + "y": 200 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -902, + "facing_y": 434, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 227, + "result": null, + "round_number": 1, + "tick": 193 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 193 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b14bbb2244860917e3bacea7e5a64fd5b226a9f7b194b942a05e5d8e2ea8c42b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 901, + "facing_y": -436, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -119, + "y": 203 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -901, + "facing_y": 436, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 226, + "result": null, + "round_number": 1, + "tick": 194 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 194 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "876b3bd470668c441dd2ba881979a751441d30a51f4724d7e313b6f8a9c76e77", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 900, + "facing_y": -438, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -123, + "y": 206 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -900, + "facing_y": 438, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 225, + "result": null, + "round_number": 1, + "tick": 195 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 195 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b25d654edfff0f8be455a30e912ec7ede33b001eed8d2aa2d8801f6182f89f22", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 898, + "facing_y": -440, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -127, + "y": 209 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -898, + "facing_y": 440, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 224, + "result": null, + "round_number": 1, + "tick": 196 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 196 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5a7be1a98a9fdceec1c776c04cc1b85c7d3e13450a9367036f0a2953dcb3839f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 899, + "facing_y": -440, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -131, + "y": 211 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -899, + "facing_y": 440, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 223, + "result": null, + "round_number": 1, + "tick": 197 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 197 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "eee9c2671186b9e17996fada442518188c67e30539b80c0a5c7fddc7117330e1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 898, + "facing_y": -441, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -136, + "y": 214 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -898, + "facing_y": 441, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 222, + "result": null, + "round_number": 1, + "tick": 198 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 198 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "03294ea39be9fdf8043d2206c721659ce731e63966d80afec5e4e78d64398143", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 897, + "facing_y": -442, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -140, + "y": 217 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -897, + "facing_y": 442, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 221, + "result": null, + "round_number": 1, + "tick": 199 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 199 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f992c4cf302664ecb032a9008751da09913352bab3e6b40650f03fb62c50f8f8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 896, + "facing_y": -444, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -144, + "y": 220 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -896, + "facing_y": 444, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 220, + "result": null, + "round_number": 1, + "tick": 200 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 200 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1b12b557e36836dea846d175b5f88402d1e539f6262a040d280649afe90f115b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 897, + "facing_y": -444, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -148, + "y": 222 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -897, + "facing_y": 444, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 219, + "result": null, + "round_number": 1, + "tick": 201 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 201 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9daad6824fe6da942556ff1380d41c8576690c4c84c7e1cdbedeb8d4cd1d34e0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 896, + "facing_y": -446, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -152, + "y": 225 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -896, + "facing_y": 446, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 218, + "result": null, + "round_number": 1, + "tick": 202 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 202 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f4d6afcf699b92660f6a588f44fc771ae787b6e247519a0b95bb9564f6aadcd8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 895, + "facing_y": -447, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -156, + "y": 228 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -895, + "facing_y": 447, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 217, + "result": null, + "round_number": 1, + "tick": 203 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 203 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e19411fa75d5524a24bcf1eba8e32a493e555b0f9164ce249567125b442764ac", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 895, + "facing_y": -448, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -161, + "y": 231 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -895, + "facing_y": 448, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 216, + "result": null, + "round_number": 1, + "tick": 204 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 204 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "653461f3ceedd17728a8456db3fb7ac0a46a2d666fa0e556a78c852b22a16695", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 894, + "facing_y": -448, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -165, + "y": 233 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -894, + "facing_y": 448, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 215, + "result": null, + "round_number": 1, + "tick": 205 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 205 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5eb4a56731d61f504cc458c44a5e0b17d45e1a929311c3a5f34164e4019e44ba", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 893, + "facing_y": -449, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -169, + "y": 236 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -893, + "facing_y": 449, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 214, + "result": null, + "round_number": 1, + "tick": 206 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 206 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9d6ecbc5f1ab19908c86882e94e3a665e9f0dbaf0df862298147749c770c3056", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 894, + "facing_y": -451, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -173, + "y": 239 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -894, + "facing_y": 451, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 213, + "result": null, + "round_number": 1, + "tick": 207 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 207 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d19f093c79af813cb9133504cada6050bbf9ac7aaa4552d6f72b04c0e31b54df", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 893, + "facing_y": -453, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -177, + "y": 242 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -893, + "facing_y": 453, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 212, + "result": null, + "round_number": 1, + "tick": 208 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 208 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a021ea4b93620151a1b74679a48942b1bbbc904c6145de7aa50c4079aa887f57", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 892, + "facing_y": -451, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -182, + "y": 244 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -892, + "facing_y": 451, + "get_up_count": 2, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 211, + "result": null, + "round_number": 1, + "tick": 209 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 209 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5729f34425be9291eb34430dafc02e447e6da41795c5caefe9dc28733c7de8d4", + "events": [ + { + "action_id": null, + "actor_id": null, + "amount": 3, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 16, + "kind": "count", + "target_id": "two", + "tick": 210 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 891, + "facing_y": -453, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -186, + "y": 247 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -891, + "facing_y": 453, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 210, + "result": null, + "round_number": 1, + "tick": 210 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 210 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "865df050112d466c5805b072a22f533e9550abded499c7485e42fb41289af7b4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 890, + "facing_y": -454, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -190, + "y": 250 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -890, + "facing_y": 454, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 209, + "result": null, + "round_number": 1, + "tick": 211 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 211 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1cde72cfec8151ffc972eb1fe52765a142d4858a72497f4161a4416bbd147e25", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 890, + "facing_y": -455, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -194, + "y": 253 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -890, + "facing_y": 455, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 208, + "result": null, + "round_number": 1, + "tick": 212 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 212 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c5584d8af84c346275204d56618b8572132cfd388d691d394646828f290636f4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 890, + "facing_y": -456, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -198, + "y": 255 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -890, + "facing_y": 456, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 207, + "result": null, + "round_number": 1, + "tick": 213 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 213 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b8a5f9585521bcfd31903762a45907c8bf32fc07e696086539ced2e29ba5d96c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 890, + "facing_y": -456, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -203, + "y": 258 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -890, + "facing_y": 456, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 206, + "result": null, + "round_number": 1, + "tick": 214 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 214 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ed44b35bc2f473f025f5649a09c2303f552e6bf4817c9dae4296dd46307fb2ee", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 889, + "facing_y": -457, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -207, + "y": 261 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -889, + "facing_y": 457, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 205, + "result": null, + "round_number": 1, + "tick": 215 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 215 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "20dac6f0a365815cf58ab12fde73f6200d7e77212330392aa28a8c5f6d5ed90f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 888, + "facing_y": -459, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -211, + "y": 264 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -888, + "facing_y": 459, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 204, + "result": null, + "round_number": 1, + "tick": 216 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 216 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "55852e1a042a4d4e1960f58a185fc0cd2b2759d6b6ba3843adcbfe8dbc6608b9", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 889, + "facing_y": -459, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -215, + "y": 266 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -889, + "facing_y": 459, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 203, + "result": null, + "round_number": 1, + "tick": 217 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 217 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "527b87dfc2c4d7faa5ef38467cfe392f12eb65f0a2c78ea4cb8e52abcf6098cb", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 888, + "facing_y": -460, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -219, + "y": 269 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -888, + "facing_y": 460, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 202, + "result": null, + "round_number": 1, + "tick": 218 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 218 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ccf6645eb9df455ac0236ecc3a7830eb4e97b352207bc6648a5e6be7d32a92b8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 887, + "facing_y": -461, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -223, + "y": 272 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -887, + "facing_y": 461, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 201, + "result": null, + "round_number": 1, + "tick": 219 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 219 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "aff58b6d010950e4a5b2d3b7013aefb9a98f09c6ac7cc1341359704321571847", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 887, + "facing_y": -462, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -228, + "y": 275 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -887, + "facing_y": 462, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 200, + "result": null, + "round_number": 1, + "tick": 220 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 220 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bfbdb326cce0eb19b342e7c216d1dcf3120f8668493f9598a9895436153279fc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 888, + "facing_y": -462, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -232, + "y": 277 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -888, + "facing_y": 462, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 199, + "result": null, + "round_number": 1, + "tick": 221 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 221 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b11f39359bc16dc435a7506b2e2af0fe04b4d343ffa14a0cd999af9899cd3f08", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 887, + "facing_y": -463, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -236, + "y": 280 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -887, + "facing_y": 463, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 198, + "result": null, + "round_number": 1, + "tick": 222 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 222 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f533f604df81f0e69fca658ac465a53f8e96b30eb01d49c4fc94ea623f792d32", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 886, + "facing_y": -464, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -240, + "y": 283 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -886, + "facing_y": 464, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 197, + "result": null, + "round_number": 1, + "tick": 223 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 223 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3a77442f8eb6db3cadbcac9f41fe7a11958b9f4a77cb39f6fc528eb88b84a2af", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 885, + "facing_y": -465, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -244, + "y": 286 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -885, + "facing_y": 465, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 196, + "result": null, + "round_number": 1, + "tick": 224 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 224 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "da5264c2a3b9516b662dad84c60d30b6c3fb0dbff34215559a4b33016cd3d56f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 886, + "facing_y": -465, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -249, + "y": 288 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -886, + "facing_y": 465, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 195, + "result": null, + "round_number": 1, + "tick": 225 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 225 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "73d90ed48ab43832793274a940bd8184cf2b109b9c747a594da0baa5ee0f1d0d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 886, + "facing_y": -466, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -253, + "y": 291 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -886, + "facing_y": 466, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 194, + "result": null, + "round_number": 1, + "tick": 226 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 226 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "928c1201db76297fe291cc7d6af7d54bc92453402effa79273a302408fa46333", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 885, + "facing_y": -467, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -257, + "y": 294 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -885, + "facing_y": 467, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 193, + "result": null, + "round_number": 1, + "tick": 227 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 227 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "167286039c349cc3c57e17d9480a5a709edde18fb2463489e98a1843f7d76bae", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 884, + "facing_y": -468, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -261, + "y": 297 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -884, + "facing_y": 468, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 192, + "result": null, + "round_number": 1, + "tick": 228 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 228 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f113bed5eabaf2aeef80d7700b4d53a599333dbb3a146fecae566b584a88ae7c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 884, + "facing_y": -467, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -265, + "y": 299 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -884, + "facing_y": 467, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 191, + "result": null, + "round_number": 1, + "tick": 229 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 229 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a6441f4a487cf8a9549daa01a3ea4cd180f05bc00bb3e189f1291511f25cfc99", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 883, + "facing_y": -468, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -270, + "y": 302 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -883, + "facing_y": 468, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 190, + "result": null, + "round_number": 1, + "tick": 230 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 230 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "22187d4b89f1526beffd97c2130f6c7a5c859c3d7e5b0425d0d71c7e5fe43e14", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 883, + "facing_y": -469, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -274, + "y": 305 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -883, + "facing_y": 469, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 189, + "result": null, + "round_number": 1, + "tick": 231 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 231 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f8c09cb160f6e13fa20ffddea06507d1a1937d68f2425575f0834d36538c4407", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 883, + "facing_y": -470, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -278, + "y": 308 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -883, + "facing_y": 470, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 188, + "result": null, + "round_number": 1, + "tick": 232 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 232 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "54d6bfad70b30ebd2a1fb0a409d14b14d6f53a2aa167dd0c70949f3afce69582", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 883, + "facing_y": -470, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -282, + "y": 310 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -883, + "facing_y": 470, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 187, + "result": null, + "round_number": 1, + "tick": 233 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 233 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e28e40883cff618c6cbdb7b45d882076a74d4b11a8a975654e4a3c4ea9a61ea9", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 882, + "facing_y": -471, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -286, + "y": 313 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -882, + "facing_y": 471, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 186, + "result": null, + "round_number": 1, + "tick": 234 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 234 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f85c25b99da335a8ce08cd1a328efc779e7c8745bf23d17e640a6b2052357750", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 881, + "facing_y": -472, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -290, + "y": 316 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -881, + "facing_y": 472, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 185, + "result": null, + "round_number": 1, + "tick": 235 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 235 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a5610a57b34aa10825f7cc73be3d5668c3b7c72c43d02d13f81d71837882013e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 881, + "facing_y": -472, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -295, + "y": 319 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -881, + "facing_y": 472, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 184, + "result": null, + "round_number": 1, + "tick": 236 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 236 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2c63e9bb5b871c60c67e564c0f6393565a41f6c6be25f6529cff1ef9b9c0e1ad", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 882, + "facing_y": -472, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -299, + "y": 321 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -882, + "facing_y": 472, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 183, + "result": null, + "round_number": 1, + "tick": 237 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 237 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a1cf7a6fa887accab0e188accc5ba27fcd378048fa02b7a1fd2c2417061bebe7", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 881, + "facing_y": -473, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -303, + "y": 324 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -881, + "facing_y": 473, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 182, + "result": null, + "round_number": 1, + "tick": 238 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 238 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1432cea6fcd431ed4cdaae9bcc25a2483575493279fd7ed0a73a6318bfccdc9e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 880, + "facing_y": -474, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -307, + "y": 327 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -880, + "facing_y": 474, + "get_up_count": 3, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 181, + "result": null, + "round_number": 1, + "tick": 239 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 239 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a9ef3d098d94b69ebe1d500dcb64aa1c7e9df4335bfe88dd6e5c5af344574997", + "events": [ + { + "action_id": null, + "actor_id": null, + "amount": 4, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 17, + "kind": "count", + "target_id": "two", + "tick": 240 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 880, + "facing_y": -475, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -311, + "y": 330 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -880, + "facing_y": 475, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 180, + "result": null, + "round_number": 1, + "tick": 240 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 240 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a1141dee6ebaba25ddce2cd2b71e698eeea9522ab2b1d772ae2af8abf7ce0b13", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 881, + "facing_y": -474, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -316, + "y": 332 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -881, + "facing_y": 474, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 179, + "result": null, + "round_number": 1, + "tick": 241 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 241 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ac896b8460b618bc7b8c49a10039981a832334b9323b3e41654bbb71666de95e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 880, + "facing_y": -475, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -320, + "y": 335 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -880, + "facing_y": 475, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 178, + "result": null, + "round_number": 1, + "tick": 242 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 242 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "35924c4d49e20e49b3e2411bc976f2f48a6aac1d7baa4676fc70152baba0b9d0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 880, + "facing_y": -476, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -324, + "y": 338 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -880, + "facing_y": 476, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 177, + "result": null, + "round_number": 1, + "tick": 243 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 243 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f7a001d9e036fc5e12d5820e73d83f6f2e6c3542de7493bb98035bd258149e0f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 879, + "facing_y": -477, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -328, + "y": 341 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -879, + "facing_y": 477, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 176, + "result": null, + "round_number": 1, + "tick": 244 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 244 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "517b0e4024ca2f10091de455635650bfc024fd10e9dfe0ef3966516966feed01", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 878, + "facing_y": -478, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -332, + "y": 344 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -878, + "facing_y": 478, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 175, + "result": null, + "round_number": 1, + "tick": 245 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 245 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "aa50905b6040e4b89f953741bb7c68f750d85c91a3084385f8bcf32f96f06ba7", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 879, + "facing_y": -477, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -337, + "y": 346 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -879, + "facing_y": 477, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 174, + "result": null, + "round_number": 1, + "tick": 246 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 246 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dd0c32574bd68e0a9cdbeda037b9db218591c61ea054ba3e12d6407eb0c6d9bd", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 879, + "facing_y": -478, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -341, + "y": 349 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -879, + "facing_y": 478, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 173, + "result": null, + "round_number": 1, + "tick": 247 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 247 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4a8550ca5ba923a40aea071e67849f9af42222d21914d1290006c373217c4522", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 878, + "facing_y": -479, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -345, + "y": 352 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -878, + "facing_y": 479, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 172, + "result": null, + "round_number": 1, + "tick": 248 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 248 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "696af26d915de90a07d3af37ad613ba142df66718192b105675aad13ac49aa72", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 878, + "facing_y": -480, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -349, + "y": 355 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -878, + "facing_y": 480, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 171, + "result": null, + "round_number": 1, + "tick": 249 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 249 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "41dd4f6741fc31f469b5c83221f991a83f399883a478488959f14a62c47f57db", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -480, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -354, + "y": 358 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 480, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 170, + "result": null, + "round_number": 1, + "tick": 250 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 250 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "91b0f244bacd18a7a9a98c582667268ca63d505490731b8f2807e392153b15c6", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -480, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -358, + "y": 360 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 480, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 169, + "result": null, + "round_number": 1, + "tick": 251 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 251 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "cfb7a475ec60ca0ad0032d544f335826227319e4abd88b2fc2fc78c0700c085c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 3, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 168, + "result": null, + "round_number": 1, + "tick": 252 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 252 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d8dda6048c61a051a906017e558c19bfeafdd10a05176e6081e74f9f8a78d46f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 167, + "result": null, + "round_number": 1, + "tick": 253 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 253 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d6dd530d7b3e665cc90061791add203ffa3f1b53d21d1581cb396aa28fbf1ccc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 166, + "result": null, + "round_number": 1, + "tick": 254 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 254 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fc5d7f5d24c8af782e197e3a54f9db12aa298be40fde1063ae36e2648899cd88", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 165, + "result": null, + "round_number": 1, + "tick": 255 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 255 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0f7f249e1c0c562a79cf0c746403e79d2fe3a25997e5659a20b5c99ca71aad29", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 164, + "result": null, + "round_number": 1, + "tick": 256 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 256 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "40499ad4335de8d24effbe875d12008b3a12ff2ca967f04bf611811584ed4006", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 163, + "result": null, + "round_number": 1, + "tick": 257 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 257 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "074655fd3873b3e6defe1438a8be79e49da8481f1323fb6c63ee59a36b7df724", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 162, + "result": null, + "round_number": 1, + "tick": 258 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 258 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "25bdf174d9f9d7ea1556783e6166ef6a1c054949cdcbdb7aa9c9cd400df45e85", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 161, + "result": null, + "round_number": 1, + "tick": 259 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 259 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8acc027e784a30eaa4d815acd62ef1571ac8d322090bc4c65f1956bf98da75eb", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 160, + "result": null, + "round_number": 1, + "tick": 260 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 260 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a7ccee00be433010cc2b246dd1df3541887f65fa5471056dfa41f09b7be22c87", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 159, + "result": null, + "round_number": 1, + "tick": 261 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 261 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "63eaa5ad5aa62ffcec5466e92d8ee4f929fd8b6b64c8c960d13347ef1ee889c5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 158, + "result": null, + "round_number": 1, + "tick": 262 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 262 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e62882418c31c39d6e82f4721b950e39ecc7ddc89a83fe4744dcffa46889dc92", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 157, + "result": null, + "round_number": 1, + "tick": 263 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 263 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2ca79476b2f0fbc8c0746f1ba7ea1b56b50a9dbb36b1a51e68da6d3eec65597f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 156, + "result": null, + "round_number": 1, + "tick": 264 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 264 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2103b77b21b15d5c7ebf58c260102f4ba3e89ed362827f37cfc69b83ddc30e43", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 155, + "result": null, + "round_number": 1, + "tick": 265 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 265 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "47d503500cc4d84c4f6133e332d67ef8fc2d7d9c825ac89f6c92eca6e3cda218", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 154, + "result": null, + "round_number": 1, + "tick": 266 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 266 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3c6b932327085456a281bf0b090f4eb6625bde2eed0ddc915adf9189363c6ac9", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 153, + "result": null, + "round_number": 1, + "tick": 267 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 267 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "461efdb128195eefa2a4a193b448b9901bfeb49c121f3c2947cca5bcff8447b2", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 152, + "result": null, + "round_number": 1, + "tick": 268 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 268 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3654b7d43252e361d254f1ac06f31aa9b8de9551c640b84cab945dc62ceed7b3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 4, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 151, + "result": null, + "round_number": 1, + "tick": 269 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 269 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ab163992fef247d59a26edaf14b4602f1599cff377260c6b7c795f2d0cd1041a", + "events": [ + { + "action_id": null, + "actor_id": null, + "amount": 5, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 18, + "kind": "count", + "target_id": "two", + "tick": 270 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 150, + "result": null, + "round_number": 1, + "tick": 270 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 270 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5c4ae538140a1881c758860c0aa2e2689f52fd51f38cea7cbe69844c9f1daabc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 149, + "result": null, + "round_number": 1, + "tick": 271 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 271 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f9dfc7a624e0872afe4c3adb2946df6f2446e7b6d89a27c20c46afd556cd2c6d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 148, + "result": null, + "round_number": 1, + "tick": 272 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 272 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "cffe8673936cd0b20a9dacc02a6fdfb75693176e012f5cea6cc20ac0d2a7a90c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 147, + "result": null, + "round_number": 1, + "tick": 273 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 273 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "23bbf48026ce8809f3ea4e5f59149adbcf4ed99d6d2ecdf2130223b1043dae83", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 146, + "result": null, + "round_number": 1, + "tick": 274 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 274 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "78b755916938ab2ab09013e533b8a567fee6c8f802d9819b637240d017826430", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 145, + "result": null, + "round_number": 1, + "tick": 275 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 275 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "17df00f2a3458ff262b40f0a5bd6f2ec29e2f2a5810e97c206bab6dcc77712e4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 144, + "result": null, + "round_number": 1, + "tick": 276 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 276 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "98000f0cd08c84427cf53e1a9896f7561819b5d82c3ee5d7c26fa31cfa2bc477", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 143, + "result": null, + "round_number": 1, + "tick": 277 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 277 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "06d50974db6dab1db3dd2edc468b580665c40fd69451bc19b968c5b64188d2f0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 142, + "result": null, + "round_number": 1, + "tick": 278 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 278 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ea5ee5a0c5ff3fe05853e4031dc0e26b42aaf51ad379ca6c66f1acb639a68c5a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 141, + "result": null, + "round_number": 1, + "tick": 279 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 279 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "12cf2acdc33361221a9659ef991606d779120ee32c504b8edce06177c2146a54", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 140, + "result": null, + "round_number": 1, + "tick": 280 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 280 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "678a906d1a40e49900b51b37de1d60859bb65b30cfd24e9229b099f689429301", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 139, + "result": null, + "round_number": 1, + "tick": 281 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 281 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4bfaa3038aae046d58081e90c8ad07f44eaf6d0a9c88dfcf9eadbea89497bf26", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 138, + "result": null, + "round_number": 1, + "tick": 282 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 282 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4d94515505b96d1d07b2a00e0203c4a3c9e180bf5170cc52756facee9b550d0e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 137, + "result": null, + "round_number": 1, + "tick": 283 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 283 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "34d9f606fab06c79a17f3c83395e0b765dd5a92bb1743c537ac5d7f655cccdcc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 136, + "result": null, + "round_number": 1, + "tick": 284 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 284 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d7b4d37e77584dbc493224ba1e0e107ebb69197eea2ce3026fbbb06de88ca852", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 135, + "result": null, + "round_number": 1, + "tick": 285 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 285 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dfc26f9ab386b1d5de83d55c814f15093c791cf2b1197c72c29af7e42c04b9b1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 134, + "result": null, + "round_number": 1, + "tick": 286 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 286 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b3fe3b3439a8174b8cc046a183481c78962f2aa7517e19ebbf0a82d61fea8f4d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 133, + "result": null, + "round_number": 1, + "tick": 287 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 287 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dd059314114e92be6546651e0f902fd7a71ce11dc4f2cf7ae2069ae20e538947", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 132, + "result": null, + "round_number": 1, + "tick": 288 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 288 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "72b45df39505ae3353ee432858ea6a2cd8a7fc6e2ca78db4d79e7bc73296565e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 131, + "result": null, + "round_number": 1, + "tick": 289 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 289 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ee830a461e627eca1395c5581156563c8fcf335395466a0fe840d30bd441b854", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 130, + "result": null, + "round_number": 1, + "tick": 290 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 290 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bbcfe44c64bdc49ac4cda1bb68a0dd93066140856a5a64ebd8fa02f4c2a6fc9b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 129, + "result": null, + "round_number": 1, + "tick": 291 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 291 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0ea933c64487309d50a1949947eb94adfa1a279b58c31ad3c47073e938d3feee", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 128, + "result": null, + "round_number": 1, + "tick": 292 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 292 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4c0605317dc89824d77868b74fa1d8c761798501a7d76a287f28fdacde41bb6b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 127, + "result": null, + "round_number": 1, + "tick": 293 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 293 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2e3494e2f3b7fe2fff08e145817b1f419e1253777a339d30252b1a4904f22bc3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 126, + "result": null, + "round_number": 1, + "tick": 294 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 294 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a2402aa21feb9ea55146ddb65da62912ace0aed7f70ead2b0d5e592cf0654de8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 125, + "result": null, + "round_number": 1, + "tick": 295 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 295 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "287c28aab6fb2fd31d6a932184e0a8d26811c45ef699e2889d33d3e9ae3f39d5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 124, + "result": null, + "round_number": 1, + "tick": 296 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 296 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "33263c73a4607fe2f28408388d211d3c07c19d14e2582356611fdc4245a14484", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 123, + "result": null, + "round_number": 1, + "tick": 297 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 297 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f3fdd9a3d542c54c2d7207b36d67ebe11e2e00bcdc0b0cb8c4003bbe8055a1e5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 122, + "result": null, + "round_number": 1, + "tick": 298 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 298 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0135010a8873833df89a166896b64c77ec81cac14f6a1c54d4337496d019952a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 5, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 121, + "result": null, + "round_number": 1, + "tick": 299 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 299 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "460e519e79285df2f5b21813c0be7a0334d8f93a66de6c1be67bd89ad75b26d2", + "events": [ + { + "action_id": null, + "actor_id": null, + "amount": 6, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 19, + "kind": "count", + "target_id": "two", + "tick": 300 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 120, + "result": null, + "round_number": 1, + "tick": 300 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 300 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ba2ac12ca64934da42d7ad449139abe65136f449af7ba8b9473593ec4ff74080", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 119, + "result": null, + "round_number": 1, + "tick": 301 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 301 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "faddb8218b3e815480aa2be5e3b3ee5aa08a294df8a1e5227c61140a33f6e756", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 118, + "result": null, + "round_number": 1, + "tick": 302 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 302 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "175b6a2c13b191cd8ca9eea0ba4a975d2090e668ee5994045ed4935f3a7d1b73", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 117, + "result": null, + "round_number": 1, + "tick": 303 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 303 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4f495d9ff2f459cbf307f8fa1b7085eb83aff9dd154d8bccec0d6631c34700cf", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 116, + "result": null, + "round_number": 1, + "tick": 304 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 304 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fe6814ddd79b73ba021c146aeac25e72430b1e9ab092aa7a0c34ed7203469a63", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 115, + "result": null, + "round_number": 1, + "tick": 305 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 305 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "120248ea332dbdf6d7c424f118783aefab1736d2b267c406ac2a920e96a0c0a0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 114, + "result": null, + "round_number": 1, + "tick": 306 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 306 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "08bd625bb793f3e3967ab720cba1d2734ed38c0bda6bd6a3cf76ee338cafe7b1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 113, + "result": null, + "round_number": 1, + "tick": 307 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 307 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "07ddd2f2c670825c6ea929cd99222a279de8edd29cb9f1c85f3556c9ac1d6ddc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 112, + "result": null, + "round_number": 1, + "tick": 308 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 308 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0b7c98bbee9ece69c7c444817038c8ea97735eecc27e06b8eda18849712d51e6", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 111, + "result": null, + "round_number": 1, + "tick": 309 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 309 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "13443923482c4a9ac3f1bc00cc29ade29a66925364124be9d313dd5ce3008c38", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 110, + "result": null, + "round_number": 1, + "tick": 310 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 310 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6c943dce70e24f63de20d30c43e6c0293179ff26750edb5d76db91cbdcea9094", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 109, + "result": null, + "round_number": 1, + "tick": 311 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 311 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ee8a40e9cf52f2d907888a94b1c560ae6882a7c62ef80d3608e375bbd37038a8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 108, + "result": null, + "round_number": 1, + "tick": 312 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 312 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f4fa6b4f7e9ccb31b5d0609e3ff0ecd9b44d54a8222ffc08b87bb1a86ec13c80", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 107, + "result": null, + "round_number": 1, + "tick": 313 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 313 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6e8c6ed64e6f4e7b074bc94ce5c37e1fd9ca7a1e50b9129b155678e981f79fa3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 106, + "result": null, + "round_number": 1, + "tick": 314 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 314 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "870bf34cc4ecaee0570455949614866437a0aedcbf73a3701e847e4aeaeddbfd", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 105, + "result": null, + "round_number": 1, + "tick": 315 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 315 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3cbf2813eb3812c65723697e0275167a9d0323a8af8843f935e1fad4b8c5c84a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 104, + "result": null, + "round_number": 1, + "tick": 316 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 316 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4c520479a72bc0efaabb489487311f657758d340d73596d6c43aa4ff5790bbb3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 103, + "result": null, + "round_number": 1, + "tick": 317 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 317 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2892b7bd943c0b414020c1ed9ef32e5474295fc04d4f806cce170ce28c5c9532", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 102, + "result": null, + "round_number": 1, + "tick": 318 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 318 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e6168e2e27468ca3d5ef852cd1ffe368e2b30c64c2bcb6808e3983b7972e57ec", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 101, + "result": null, + "round_number": 1, + "tick": 319 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 319 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b4605488f7390fed09991d56b1a89d3984fcce043833d388366cf5725dcb8fb3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 100, + "result": null, + "round_number": 1, + "tick": 320 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 320 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "83907a4daec8c3fd3309c36b30786cfc3ca181b9d1a38554ecf7251972437018", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 99, + "result": null, + "round_number": 1, + "tick": 321 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 321 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "121152006820811508edaec665395d839ff083bac65a2c1d05d10d7e3b365e4f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 98, + "result": null, + "round_number": 1, + "tick": 322 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 322 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "115446924c4e1cffca399ee29135fd9edbffc92ae2b803d8054e6300cb32014d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 97, + "result": null, + "round_number": 1, + "tick": 323 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 323 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "547044cd2694503a22b99f35225b765418cb0854f8b9211323b23c5cab5d114e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 96, + "result": null, + "round_number": 1, + "tick": 324 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 324 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5742c8738d5c3fc22278ad5cd6209a0d760c03f237805e14dcaf688fabef06f6", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 95, + "result": null, + "round_number": 1, + "tick": 325 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 325 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dae9efe6bced9c8d115b2d87d5fc51b4a73a9f923d468b7d33ca938dd714cc06", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 94, + "result": null, + "round_number": 1, + "tick": 326 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 326 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "09ae5cfe92bf6f01225ae887834fbd2224b1a857dff8745ca29c649ee649b87b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 93, + "result": null, + "round_number": 1, + "tick": 327 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 327 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1bc94f389434afef99a399c017c19b3b0d6a69cf1a4134bd0fd9841a48ef1b47", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 92, + "result": null, + "round_number": 1, + "tick": 328 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 328 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a568fe81bbd683e6353eca6fea2c4df71420c6c830c91322ea557976956ae917", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 6, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 91, + "result": null, + "round_number": 1, + "tick": 329 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 329 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "af8fd005d129d9214150aa936b24f506548aec212f57750199aaafa08c405381", + "events": [ + { + "action_id": null, + "actor_id": null, + "amount": 7, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 20, + "kind": "count", + "target_id": "two", + "tick": 330 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 90, + "result": null, + "round_number": 1, + "tick": 330 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 330 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "13c464a3001ae295f67d353286f514b794307a5628109d81f368adf6a9f9892f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 89, + "result": null, + "round_number": 1, + "tick": 331 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 331 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ca6db413657a10765883177932aad17c67d4a0f83f16adbe7ea66674ccd350d2", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 88, + "result": null, + "round_number": 1, + "tick": 332 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 332 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8c76bb196149428919dd74780c00bfa6d81689da9957e7c25e7297eaf32aeb60", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 87, + "result": null, + "round_number": 1, + "tick": 333 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 333 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7271b278516f06cb409fec5919263bcb51ecb5712821fcef1b3042ee782349b6", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 86, + "result": null, + "round_number": 1, + "tick": 334 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 334 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3de71d19e9f71ee5c51c109a47cb32d7dea9666b6cd9618331ef9e6f924181b8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 85, + "result": null, + "round_number": 1, + "tick": 335 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 335 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2ebf2e2f1dbcfa51288258bbdab2dde8719fa8359ff917e87d972aa4f2814619", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 84, + "result": null, + "round_number": 1, + "tick": 336 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 336 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "065c997cf3387723ae632eac82eb63c85b70415f592657db456c9c05e040b6bf", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 83, + "result": null, + "round_number": 1, + "tick": 337 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 337 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "38eb6c4c054a525b13527967ae38574b95d5675adf4eee79df14d32cb8f2672d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 82, + "result": null, + "round_number": 1, + "tick": 338 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 338 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "67b70d2187f8a4f991ab7b8423f9aad083792770d04b7bab3616e8b16dbe2fdb", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 81, + "result": null, + "round_number": 1, + "tick": 339 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 339 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5c79719133166a8a598bcd484ec0095e7661862dddd059ed589a092881374f7e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 80, + "result": null, + "round_number": 1, + "tick": 340 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 340 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7adfc23d4194ed0d1313de58ce1c3457adae0ed0bac3324e48e7e5b8736c2673", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 79, + "result": null, + "round_number": 1, + "tick": 341 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 341 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6569d229be9b8f910a6f1fc9cc5d4dd99ef3f6825993664e28a62a9bc19b1b16", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 78, + "result": null, + "round_number": 1, + "tick": 342 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 342 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2dc9a24a3abfa20779df70474931e62f8a4d55f7fc6d9c5731768fc93e1ec803", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 77, + "result": null, + "round_number": 1, + "tick": 343 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 343 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "90fba9de4feea68cf77a7bda6729024699f561068bb979da15f22eecb45ca5ee", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 76, + "result": null, + "round_number": 1, + "tick": 344 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 344 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8e54590f07b406b2a764d1d109b52f347656b5c5617c31ab249707f331f07bc8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 75, + "result": null, + "round_number": 1, + "tick": 345 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 345 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "83c121180342665be2b11de6978c5c0c1ebdb78f205a6398e5934d766d34759c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 74, + "result": null, + "round_number": 1, + "tick": 346 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 346 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2fa7113ad232aa6ce05ff488a605aca1e808cba79758444ec95b7b92aeebeca1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 73, + "result": null, + "round_number": 1, + "tick": 347 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 347 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "67c3309ba659f61a6e95bf1341c48a74ab01c6c1d2dee7466659affba4b6a2f2", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 72, + "result": null, + "round_number": 1, + "tick": 348 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 348 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e3961829a38664c9afc3a27cffadf3fb4af545471f01edecb9f1bb20612bf913", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 71, + "result": null, + "round_number": 1, + "tick": 349 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 349 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ad269ba7d5594b7b11a1de50bc28f0e76205cc4134cb6b2b096f7960a4bc86db", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 70, + "result": null, + "round_number": 1, + "tick": 350 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 350 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "059c3b24226edeacca6e33428ba3d21e43a986d18ba70b10a4e2b08597fce06a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 69, + "result": null, + "round_number": 1, + "tick": 351 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 351 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6a355275986c350748c40d185cae0bd1a0bcb788e82197d7e9f49275126d240d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 68, + "result": null, + "round_number": 1, + "tick": 352 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 352 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e8fdc44b98fd484bd9d955e2df04c1df529d8ceefc968b19b00809f66c6afe5b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 67, + "result": null, + "round_number": 1, + "tick": 353 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 353 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1f3b4f64e4bf7bdd98b194cc645ccac683c76fc523b01075079d612c48bbfc15", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 66, + "result": null, + "round_number": 1, + "tick": 354 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 354 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "81b4145758cf4526cc4d64a2eda84cb87465d36e04332069b6b833489edc3f1e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 65, + "result": null, + "round_number": 1, + "tick": 355 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 355 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9ca2699486fc5ea9ecf1b14ac79e881c17b422c7215c63af0136c0539a0f0220", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 64, + "result": null, + "round_number": 1, + "tick": 356 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 356 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "58f0cfa2fce5475b436ac440cb499112a730f1d674f1ee8bfa9f9fd4355cfbbe", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 63, + "result": null, + "round_number": 1, + "tick": 357 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 357 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1c83127ccb4e7da4eba11b0771c2959236a0f8f96904d3c6ff2685fa38d0564b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 62, + "result": null, + "round_number": 1, + "tick": 358 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 358 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "be96865d491da93fdfdd9589527166ea775bd63ced00dc114a946423dcb2dd2b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 7, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 61, + "result": null, + "round_number": 1, + "tick": 359 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 359 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fdc5cab1d7d1248b30276c6b9d61271c1c78081704fe4c12c5b3fd5a2abc9128", + "events": [ + { + "action_id": null, + "actor_id": null, + "amount": 8, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 21, + "kind": "count", + "target_id": "two", + "tick": 360 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 60, + "result": null, + "round_number": 1, + "tick": 360 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 360 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "441a7fb3089eb6c0167e6ba460bbb6a4310a12f79cc89aa4ed255c2f441c398a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 59, + "result": null, + "round_number": 1, + "tick": 361 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 361 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0a222540e9785325cccb44de82d9090347c728f9340002b8df6e366e360e1231", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 58, + "result": null, + "round_number": 1, + "tick": 362 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 362 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "957daab7bcea9587baa010a0a8ad3612e3d6362ec59ad5955007b4149d621ff6", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 57, + "result": null, + "round_number": 1, + "tick": 363 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 363 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b5aaa18e0fcca2ec57c1194ac5deb946f01657c4e3a46d8abacd708714669487", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 56, + "result": null, + "round_number": 1, + "tick": 364 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 364 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f62a2177c491b529f765f996e4e45cce663aa3b360a8deae319249e8f6654323", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 55, + "result": null, + "round_number": 1, + "tick": 365 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 365 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6f453959472f16d05a8fb081a78c9c81f01434106943d235218795dca3a2becb", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 54, + "result": null, + "round_number": 1, + "tick": 366 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 366 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "75570967548ee9ea728581319f955ecc86ed6b9b5438ec7e2c3e8532f0e24380", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 53, + "result": null, + "round_number": 1, + "tick": 367 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 367 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ecf6c67a01c967da05d1d0021f3ced0a69a7bec7819f90b064b4c58617627573", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 52, + "result": null, + "round_number": 1, + "tick": 368 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 368 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "cc860ef791ee93c3ec86f1d91ae135668f6bd71c7c9fe3bd2e3b87ac19326a4e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 51, + "result": null, + "round_number": 1, + "tick": 369 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 369 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "63d666758fd42c8503887f56086501dcc363415352f768c0128457b957b04dc0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 50, + "result": null, + "round_number": 1, + "tick": 370 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 370 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6df02c7428f62718b4d2e15f5a74aa9c5859f102f3f9c277d245dd71fd174f25", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 49, + "result": null, + "round_number": 1, + "tick": 371 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 371 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "68b778e6d069c9d5c2d821cbe1acc79550157d8168607670442f6ac96015bfe2", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 48, + "result": null, + "round_number": 1, + "tick": 372 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 372 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8192ba9a4cc2368cab2fbb706ee3a709c1dc210828af3cc241b8d3b10e503af5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 47, + "result": null, + "round_number": 1, + "tick": 373 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 373 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0c6298ceb65b68dc4856f77e4f13f31daba9567c6806f93224d23a5afca9032f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 46, + "result": null, + "round_number": 1, + "tick": 374 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 374 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "12116d1707be2cb1b28346a011d3d3242e0997bba6cbd8ed874c66ef5120388c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 45, + "result": null, + "round_number": 1, + "tick": 375 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 375 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dbeaf9dc381e79d39027bf60d420b17f11a02f95d7744822cf870a4235b0af3e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 44, + "result": null, + "round_number": 1, + "tick": 376 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 376 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4f4aa5222d22a032ff798e0a85197c82bfd79c4bbfe70e363b378bee40900ad3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 43, + "result": null, + "round_number": 1, + "tick": 377 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 377 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "84ab08d6822c5aed7045bc62a8d7272dcc492327483846e5eaadefcf88370b29", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 42, + "result": null, + "round_number": 1, + "tick": 378 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 378 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bd228768537658c15a7d7f22c4034371d1c314f3d39b69dd470f0468296a7747", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 41, + "result": null, + "round_number": 1, + "tick": 379 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 379 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1eb5855beb0b3d280b1eb530c59041a9a14fe6de8d97e139e59a11a595510acb", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 40, + "result": null, + "round_number": 1, + "tick": 380 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 380 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "81d4921c774da7d3386b809aea3b0dd00e56dbcd47c38ee8dda3e0b424254cbf", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 39, + "result": null, + "round_number": 1, + "tick": 381 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 381 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "464127c077cdfd4b7f328be0a984e3a314fb8b6230ccd39de38ebcf5c0aebf89", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 38, + "result": null, + "round_number": 1, + "tick": 382 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 382 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7b8bbec785191250947ac1a94056595e6ee1ed6cf86fcb35d81ac18b4f9e1671", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 37, + "result": null, + "round_number": 1, + "tick": 383 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 383 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2bcca6d7f6164e9a59902f44e93defa79f8bf8a538de57d2c791fba86f6c5ebd", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 36, + "result": null, + "round_number": 1, + "tick": 384 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 384 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "931e95d6fb95d1903a7eaf14109457d7dcccda97d6ee1ace806968bfceb9216c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 35, + "result": null, + "round_number": 1, + "tick": 385 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 385 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e1e02380e2f7a029d21de64fdccf975546613c8ae642c6b2f8029a035bdf7885", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 34, + "result": null, + "round_number": 1, + "tick": 386 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 386 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "107003b6a11cbb92478044a4ea6da6ac9fa4e90e07fd5ff18b543bea0bb52121", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 33, + "result": null, + "round_number": 1, + "tick": 387 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 387 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "15ebbeaad4a7771768a9ff64c2f302f81eedba9480e91a8ae601714a8a61daa5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 32, + "result": null, + "round_number": 1, + "tick": 388 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 388 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d32ff9c86a1d2f8c4cb9e7f41546693052b5e2c65274f71c52f258207f78dc7f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 31, + "result": null, + "round_number": 1, + "tick": 389 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 389 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7ce5afaa1dddda0bbb2e83fd1be02f90ec018db5d9fa9b147b81964be70d4d7b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 30, + "result": null, + "round_number": 1, + "tick": 390 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 390 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2e9eea693dff854d47279487ff2c53ab062d27629ea024e952f51cf5c97a711b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 29, + "result": null, + "round_number": 1, + "tick": 391 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 391 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6400c8db14dd7ce6d9200876aa7604282e024d41cf79a84617531c48e93eb21e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 28, + "result": null, + "round_number": 1, + "tick": 392 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 392 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "760dbb650621b74cb2132054437321cc85c8ee1637037059bc6fd0ec1a40cd11", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 27, + "result": null, + "round_number": 1, + "tick": 393 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 393 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "16486b2e92116dbf35c5565e3addf6abacdc0decf94852d2dcb9ed1eb33e6c6e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 26, + "result": null, + "round_number": 1, + "tick": 394 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 394 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "878a73c38f54c603853b96537f6fc632670408b407974d10faff61bf1aa257fd", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 25, + "result": null, + "round_number": 1, + "tick": 395 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 395 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "084054a168aee588139bf93f1d07cc77a4df1df912da8807bda400efd18904ca", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 24, + "result": null, + "round_number": 1, + "tick": 396 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 396 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "37ece5761cf4f6d536bbc5a3502811c9412ad4dffdeae7e4e2f5bd96d5a034b2", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 23, + "result": null, + "round_number": 1, + "tick": 397 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 397 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5c043135ca5bccc8a0424bc45ff65b3d601b210800578e6c18c6fd7667718183", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 22, + "result": null, + "round_number": 1, + "tick": 398 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 398 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "72aa03144fbce55c2004dd1a3a1d5a4d8864d7564afb9def8f266bd020497572", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 21, + "result": null, + "round_number": 1, + "tick": 399 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 399 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c6c7b8f1c0371bf0efa66f11280d8ea8b46a5072c1c3d09a6c6091b7e63c94ca", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 20, + "result": null, + "round_number": 1, + "tick": 400 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 400 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d0684948fe63988db33074aecff2bd50fc343150a23648a86c5b8b6ddf1124a7", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 19, + "result": null, + "round_number": 1, + "tick": 401 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 401 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5ff07b941242687d1e116af66adc85c243da5900717f47be6581f5ad7852b099", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 18, + "result": null, + "round_number": 1, + "tick": 402 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 402 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a7015a6ca340d7de9e302a4f0a504b1009720563a54ff06751e51db8bbf05a5d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 17, + "result": null, + "round_number": 1, + "tick": 403 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 403 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "895f16130ca14159f88a2c571dc4987aa0a16e14f24d242bee5c8748f994379f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 16, + "result": null, + "round_number": 1, + "tick": 404 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 404 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "64c375bacf521d5d534e7d6f67008fd6f59b67f0333ea05a93e645afc24e20bd", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 15, + "result": null, + "round_number": 1, + "tick": 405 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 405 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "97c4037e92d518e4eb4bb83523b13bf68744ef803dcbf5093d800e2414bc7329", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 14, + "result": null, + "round_number": 1, + "tick": 406 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 406 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8b4a6daa3c364c79c98a9bfc06f2631ff680864d071430d905e3dce485239545", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 13, + "result": null, + "round_number": 1, + "tick": 407 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 407 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "773cdf287cf2f45d815503a4886fe6a5aabb40fad1deee9238fc607175580473", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 12, + "result": null, + "round_number": 1, + "tick": 408 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 408 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c56be768b50a02904c7382e6547c8ddf228eba4cf4d4e63f4cf3c3fce9074f1e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 11, + "result": null, + "round_number": 1, + "tick": 409 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 409 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1671a3cf8581e7602a209f731c8d9aac68eca0e7df6a1efa6449463bbabedd96", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 10, + "result": null, + "round_number": 1, + "tick": 410 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 410 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1270b471596269cbc0f189fc07134551838c15c16fa0e5e602de8cfa2cf14fd8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 9, + "result": null, + "round_number": 1, + "tick": 411 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 411 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8c45b9ff5b0265c70c2f5dee20df0e5a94611ddb02b2d33cdbca5f04f095f849", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 8, + "result": null, + "round_number": 1, + "tick": 412 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 412 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e2fd5d5d0e114bb2d18f6b233397aff0f6b1b07355b91f6c170fe19694fa4e75", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 7, + "result": null, + "round_number": 1, + "tick": 413 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 413 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e0fd739737a4c4ec9b0dcd0f1c9977cf7ecb25593eee3829f365ed81e184f2a5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 6, + "result": null, + "round_number": 1, + "tick": 414 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 414 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "aab7210092621c4cc1c524c81359a6402c95d1e90e66769e16e1636c53b6b954", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 5, + "result": null, + "round_number": 1, + "tick": 415 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 415 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6042f9bf81076dc86aa8efddcc0d4196f158e70de94d88f02296e2edd17ca3c0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 4, + "result": null, + "round_number": 1, + "tick": 416 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 416 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "46b8553794c6a610c3eb88a336ac3790f5e11fadf34902072b022e30cf698e8c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 3, + "result": null, + "round_number": 1, + "tick": 417 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 417 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "39c68e143fd3a7e9d6d4d1258923cecae9704a5a3047bb0ce8cc50f909e5389a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 2, + "result": null, + "round_number": 1, + "tick": 418 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 418 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "00f70b49c669f763417b10a42347d07e4eb11daba355583abfe8b763f40834ca", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 1, + "result": null, + "round_number": 1, + "tick": 419 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 419 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "712d80bf56ca7d7f4df1e9637d6a64f81d88d170914dafbcca2b65b09957ee34", + "events": [ + { + "action_id": null, + "actor_id": null, + "amount": 0, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 22, + "kind": "box", + "target_id": "two", + "tick": 420 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 20, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999880, + "result": null, + "round_number": 1, + "tick": 420 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 420 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4b29ea0f3e913b99ea2455e171a595b9eaf6d4e679fd03ed0c517ccee9970f0c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 84, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 936, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -362, + "y": 363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 19, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 300, + "y": 0 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999879, + "result": null, + "round_number": 1, + "tick": 421 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 421 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ff9c110b590c737676c5a1027fba4c93a4c520983eb47d8e184333e0e9e9765d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 85, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 939, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 3, + "velocity_y": -2, + "warnings": 0, + "x": -359, + "y": 362 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 18, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -2, + "velocity_y": 1, + "warnings": 0, + "x": 298, + "y": 1 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999878, + "result": null, + "round_number": 1, + "tick": 422 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 422 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "762e19b1a906cebe4bbebc64809155555087b8b8859d1e00b2709254551532ac", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 86, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 942, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 5, + "velocity_y": -3, + "warnings": 0, + "x": -355, + "y": 359 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 17, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -3, + "velocity_y": 2, + "warnings": 0, + "x": 295, + "y": 2 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999877, + "result": null, + "round_number": 1, + "tick": 423 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 423 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "cfbd68fa40d504150e632c336dce46da2a9adb1aa528637dcf36b18efc549e73", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 87, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 945, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 5, + "velocity_y": -3, + "warnings": 0, + "x": -349, + "y": 356 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 16, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -4, + "velocity_y": 2, + "warnings": 0, + "x": 292, + "y": 4 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999876, + "result": null, + "round_number": 1, + "tick": 424 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 424 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0bbd4de3921fbf05284f6292a56d59f38924b1166b4445027aa20197ce8a9224", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 88, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 948, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -344, + "y": 353 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 15, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -4, + "velocity_y": 2, + "warnings": 0, + "x": 288, + "y": 7 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999875, + "result": null, + "round_number": 1, + "tick": 425 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 425 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5e012e83703284714a31d2f5224bbc98294053bb9e28b8fbab1cb00c49a0894a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 89, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 951, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -338, + "y": 350 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 14, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -4, + "velocity_y": 2, + "warnings": 0, + "x": 284, + "y": 9 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999874, + "result": null, + "round_number": 1, + "tick": 426 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 426 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5f3bf340c1f1afe7c045562e75c2814b9b6b0de9a63d497d0b4cad8853662a55", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 90, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 954, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -332, + "y": 347 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 13, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -4, + "velocity_y": 2, + "warnings": 0, + "x": 279, + "y": 11 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999873, + "result": null, + "round_number": 1, + "tick": 427 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 427 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ee5fb3b066b1c6388a438e8940859cd9ab8bb0ae27b88374812f21b74cd96efa", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 876, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 91, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 957, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -326, + "y": 343 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -876, + "facing_y": 482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 12, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -4, + "velocity_y": 2, + "warnings": 0, + "x": 275, + "y": 13 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999872, + "result": null, + "round_number": 1, + "tick": 428 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 428 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0ca656d93c3d7d6c5fb1fc34e39af609c62b5e279572555d49e72f0fb836942b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 92, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 960, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -320, + "y": 340 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -878, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 11, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -4, + "velocity_y": 2, + "warnings": 0, + "x": 271, + "y": 16 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999871, + "result": null, + "round_number": 1, + "tick": 429 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 429 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "cb2767c0ac6128869b3d8226b38c3a01fc0635c696883095c58e0b6d6786c4bc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 876, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 93, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 963, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -313, + "y": 337 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 10, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -4, + "velocity_y": 2, + "warnings": 0, + "x": 267, + "y": 18 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999870, + "result": null, + "round_number": 1, + "tick": 430 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 430 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5979d409f4c708174cceb9955e4c56800144ea242c143b29fc63023a1103a41f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 94, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 966, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -307, + "y": 333 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -876, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 9, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -4, + "velocity_y": 2, + "warnings": 0, + "x": 263, + "y": 20 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999869, + "result": null, + "round_number": 1, + "tick": 431 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 431 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fea0ec718da55297c060c8032aa8206917aff021892a781e0378f94b96234bfa", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 95, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 969, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -301, + "y": 330 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 8, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -4, + "velocity_y": 2, + "warnings": 0, + "x": 258, + "y": 23 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999868, + "result": null, + "round_number": 1, + "tick": 432 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 432 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "66ed85129a6bf8f365c811923e21f13171bffa3450882a2083ad7f2efae35bbb", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 96, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 972, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -295, + "y": 326 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -876, + "facing_y": 480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 7, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -4, + "velocity_y": 2, + "warnings": 0, + "x": 254, + "y": 25 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999867, + "result": null, + "round_number": 1, + "tick": 433 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 433 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e4963dff94f94a944fcb48cf01ea82cc87392506ba084f2453eeae796a90d0bf", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 97, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 975, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -289, + "y": 323 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 6, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -4, + "velocity_y": 2, + "warnings": 0, + "x": 250, + "y": 27 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999866, + "result": null, + "round_number": 1, + "tick": 434 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 434 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "248b859ea2fdd3f9de7d5a66f775a372d4d3ca68e0243b2b27982eaaaee9817e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 876, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 98, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 978, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -283, + "y": 320 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 5, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -4, + "velocity_y": 2, + "warnings": 0, + "x": 246, + "y": 29 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999865, + "result": null, + "round_number": 1, + "tick": 435 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 435 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7143a2db4325417a108d38d8c66dbb1dd67e665b93a0e3885899a6983cd2d814", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 99, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 981, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -277, + "y": 316 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 4, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -4, + "velocity_y": 2, + "warnings": 0, + "x": 242, + "y": 32 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999864, + "result": null, + "round_number": 1, + "tick": 436 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 436 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ca15cb59d4adf29aadf96e9378383620314e3e593c090ac7166998eb905ff497", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 878, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 100, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -271, + "y": 313 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 3, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -4, + "velocity_y": 2, + "warnings": 0, + "x": 237, + "y": 34 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999863, + "result": null, + "round_number": 1, + "tick": 437 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 437 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4bad5c0a308c5f1b14a5fc8be6698c4481c9c2f5121ce87b41ce7fb09a4e884a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -483, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 101, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 987, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -264, + "y": 310 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 2, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -4, + "velocity_y": 2, + "warnings": 0, + "x": 233, + "y": 36 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999862, + "result": null, + "round_number": 1, + "tick": 438 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 438 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d7ec1f3b73a0093aceb09116357afaa4888a50e0ce01dcff958ee7a5a1349a98", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 876, + "facing_y": -482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 102, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 990, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -258, + "y": 306 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -876, + "facing_y": 482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 690, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -4, + "velocity_y": 2, + "warnings": 0, + "x": 229, + "y": 39 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999861, + "result": null, + "round_number": 1, + "tick": 439 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 439 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "303defeffea49a884e406bd3f699af966281180f814bf8052d4c8e9999fc5593", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 103, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 993, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -252, + "y": 303 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -878, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 693, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 301, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -5, + "velocity_y": 3, + "warnings": 0, + "x": 224, + "y": 42 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999860, + "result": null, + "round_number": 1, + "tick": 440 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 440 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e5c85a4ff7132a9ab18a45c0073a7ff70553a74b896465e903ab63fdc1d3cd50", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 876, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 104, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -246, + "y": 300 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 693, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 301, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -5, + "velocity_y": 3, + "warnings": 0, + "x": 219, + "y": 45 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999859, + "result": null, + "round_number": 1, + "tick": 441 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 441 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "34521cd66203118633159bb082e046480336211b13eb466142f7fe14348f78e8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -479, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 105, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -240, + "y": 296 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 696, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 302, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 213, + "y": 48 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999858, + "result": null, + "round_number": 1, + "tick": 442 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 442 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "033cb4e164fa70718ac73e0bad2dfda09e43757ad06c240b898410afdfad8638", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 878, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 106, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -234, + "y": 293 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 696, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 302, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 207, + "y": 51 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999857, + "result": null, + "round_number": 1, + "tick": 443 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 443 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "15831709f0f46df000d8e89bf5abf1b4da7f16f9a60ed988bf81f56dc8b27c2e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 878, + "facing_y": -480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 107, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -228, + "y": 289 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 699, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 303, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 201, + "y": 54 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999856, + "result": null, + "round_number": 1, + "tick": 444 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 444 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2593b1d13f7ac230967a6e3844d844199a1558f22169978668cd6ca9f0f0d93f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 108, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -221, + "y": 286 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 699, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 303, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 195, + "y": 57 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999855, + "result": null, + "round_number": 1, + "tick": 445 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 445 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "31d85757337a91529aefc2c4d8e4f32da428ec2b5df762fa17b4708728a052c4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 876, + "facing_y": -482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 109, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -215, + "y": 283 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -876, + "facing_y": 483, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 304, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 189, + "y": 60 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999854, + "result": null, + "round_number": 1, + "tick": 446 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 446 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d540568219b218a51257c654487bdfb824dc93c21cf871cb352886d65d51d323", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 876, + "facing_y": -482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 110, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -209, + "y": 279 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 304, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 184, + "y": 64 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999853, + "result": null, + "round_number": 1, + "tick": 447 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 447 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "411189df6b7449d8d5b2b0a2e07bcd21daec8db5a210ecd7ad6c885b93e77b27", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 111, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -203, + "y": 276 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 305, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 178, + "y": 67 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999852, + "result": null, + "round_number": 1, + "tick": 448 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 448 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0256360028dcdf1980c18350319dbf4865bd978692b4acc4578e39a621f75a03", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 878, + "facing_y": -482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 112, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -197, + "y": 273 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -876, + "facing_y": 482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 305, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 172, + "y": 70 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999851, + "result": null, + "round_number": 1, + "tick": 449 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 449 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "98f8db3613e782c7e4cc2387825b2947d7d272bb0771b2ec2cd68486872a3a29", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 878, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 113, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -191, + "y": 269 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -876, + "facing_y": 482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 306, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 166, + "y": 73 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999850, + "result": null, + "round_number": 1, + "tick": 450 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 450 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "66cec8752014ed7adebc83ceb7a74ab0af21c95ef9c848bf8aff1da1313a47a0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 114, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -185, + "y": 266 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 306, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 160, + "y": 77 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999849, + "result": null, + "round_number": 1, + "tick": 451 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 451 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "697a61b7f5da9e2933b769dd9646a13f044af3549d26c84149146310c2a70030", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 878, + "facing_y": -479, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 115, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -179, + "y": 262 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -878, + "facing_y": 480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 307, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 154, + "y": 80 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999848, + "result": null, + "round_number": 1, + "tick": 452 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 452 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2c7f46cc6a0be75bbbfd0323bb0e74c1b0bdd4a3cb0ca43b40ecda0b10fffbc4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 878, + "facing_y": -482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 116, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -172, + "y": 259 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 307, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 148, + "y": 83 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999847, + "result": null, + "round_number": 1, + "tick": 453 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 453 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "90b4ab23c4ccf29e4ed7350a8588cc510b1cb4e8aacd17ae25878acd28f91d15", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -483, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 117, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -166, + "y": 256 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -876, + "facing_y": 483, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 308, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 142, + "y": 86 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999846, + "result": null, + "round_number": 1, + "tick": 454 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 454 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "da15f7bdf97974063c6a354a8de95be51238925686f139281f3bcf8a96d5b691", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 118, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -160, + "y": 252 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 308, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 137, + "y": 90 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999845, + "result": null, + "round_number": 1, + "tick": 455 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 455 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "76c55d954bd1f01e38710d3203a84e4567bfc26825af004108dac3cb2f76b497", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 879, + "facing_y": -480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 119, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -154, + "y": 249 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 309, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 131, + "y": 93 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999844, + "result": null, + "round_number": 1, + "tick": 456 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 456 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c18b9fa14177f1a6f54e1e91035e2e68bfc38876b9a5dddc6351fafd9c746a1a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 120, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -148, + "y": 246 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 309, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 125, + "y": 96 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999843, + "result": null, + "round_number": 1, + "tick": 457 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 457 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "81b494dd8423e0a8cbd7485f2af822d26abe6b779229ca2af374605a5fa8cfcb", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 878, + "facing_y": -480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 121, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -142, + "y": 242 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 310, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 119, + "y": 99 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999842, + "result": null, + "round_number": 1, + "tick": 458 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 458 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2738e9f9d994c1d6434f04bb149e7f3b4d6b0fdf950ccca18944a7d9b2cf4eab", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 879, + "facing_y": -482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 122, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -136, + "y": 239 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -876, + "facing_y": 482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 310, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 113, + "y": 102 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999841, + "result": null, + "round_number": 1, + "tick": 459 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 459 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "504b7f3c05a41845d716978b4951c0932ac3c97bf3be3a79507aa0cc92879d46", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 876, + "facing_y": -485, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 123, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -129, + "y": 236 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 483, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 311, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 107, + "y": 106 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999840, + "result": null, + "round_number": 1, + "tick": 460 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 460 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f2d3e7199b3b0d9227a1ae5ea16b06204928abc6676d69975007b644926942ba", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 124, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -123, + "y": 232 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 311, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 101, + "y": 109 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999839, + "result": null, + "round_number": 1, + "tick": 461 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 461 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ffa64d781d835f4ce0f367cc4a831d84d4112cad718631ba226ba911159c01ee", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 879, + "facing_y": -483, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 125, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -117, + "y": 229 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -876, + "facing_y": 482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 312, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 95, + "y": 112 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999838, + "result": null, + "round_number": 1, + "tick": 462 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 462 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4afff8afe3a2ea44d4d14da93fe6fbaf40ebc50885ce63ca529c2ecfe807ac24", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 880, + "facing_y": -482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 126, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -111, + "y": 225 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 312, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 89, + "y": 115 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999837, + "result": null, + "round_number": 1, + "tick": 463 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 463 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6ad22b7a831453ee26a961f0fff15909ce63dac882e6b0d5f23728ad89e2ffbe", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -484, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 127, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -105, + "y": 222 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -879, + "facing_y": 479, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 313, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 84, + "y": 119 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999836, + "result": null, + "round_number": 1, + "tick": 464 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 464 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a092d6c6c67427ccddc9ec9b17efef50d418c6bd2f253b80487475a6e20b9c20", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 879, + "facing_y": -480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 128, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -99, + "y": 219 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 313, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 78, + "y": 122 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999835, + "result": null, + "round_number": 1, + "tick": 465 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 465 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "212c45657aeee8e551fa9e9431287c29dd0f99e6d0bea63359364e7db2b84d12", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 881, + "facing_y": -479, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 129, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -93, + "y": 215 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -878, + "facing_y": 479, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 314, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 72, + "y": 125 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999834, + "result": null, + "round_number": 1, + "tick": 466 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 466 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d10ec8b5c174792d5084620a93503733304233f9f0af01d04bdd72a7f8481fde", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 878, + "facing_y": -480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 130, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -87, + "y": 212 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 314, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 66, + "y": 128 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999833, + "result": null, + "round_number": 1, + "tick": 467 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 467 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9c2bba98e40529a09dca51abdfdbb8e8f8c1edb093477bb1d400fbb535f35ee2", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 879, + "facing_y": -487, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 131, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -80, + "y": 209 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -880, + "facing_y": 484, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 315, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 60, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999832, + "result": null, + "round_number": 1, + "tick": 468 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 468 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "67ec434b3c69297ea188c607caccefdf689321404625ed19fb9bf8abe6fc7721", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 881, + "facing_y": -480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 132, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -74, + "y": 205 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -877, + "facing_y": 481, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 315, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -6, + "velocity_y": 3, + "warnings": 0, + "x": 54, + "y": 135 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999831, + "result": null, + "round_number": 1, + "tick": 469 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 469 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "74e857482c07aa32629cb6beb9924235ff6dcdf939e7031c89944c9b3bc68672", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": -482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 132, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": -68, + "y": 202 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -876, + "facing_y": 483, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 316, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -3, + "velocity_y": 2, + "warnings": 0, + "x": 51, + "y": 136 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999830, + "result": null, + "round_number": 1, + "tick": 470 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 470 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5fe9ce1ea119d625bb758d8a47a886977fb5f43b700ea5c11a66035cbdb7545c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 869, + "facing_y": -500, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 133, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": 0, + "warnings": 0, + "x": -62, + "y": 201 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -873, + "facing_y": 487, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 316, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -1, + "velocity_y": 1, + "warnings": 0, + "x": 50, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999829, + "result": null, + "round_number": 1, + "tick": 471 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 471 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "14f296c61d02a6317160507d7fdf7f2e93e0ca71ede05ea04fb7a2fae1a975ad", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 854, + "facing_y": -524, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 134, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": 2, + "warnings": 0, + "x": -56, + "y": 202 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -867, + "facing_y": 499, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 317, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": -1, + "velocity_y": 0, + "warnings": 0, + "x": 49, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999828, + "result": null, + "round_number": 1, + "tick": 472 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 472 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "99342729cf9173787058dbee7b2bb156d95124f8692edeab3a198fdf22113ae4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 833, + "facing_y": -566, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 135, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 5, + "velocity_y": 4, + "warnings": 0, + "x": -51, + "y": 206 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -860, + "facing_y": 524, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 317, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 49, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999827, + "result": null, + "round_number": 1, + "tick": 473 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 473 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7077251c40c40cd95fc7389e5ef89616e279484b5835edf71131f921e1122603", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 798, + "facing_y": -613, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 136, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 5, + "warnings": 0, + "x": -46, + "y": 211 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -827, + "facing_y": 562, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 318, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999826, + "result": null, + "round_number": 1, + "tick": 474 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 474 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dde22705216735d1d84416397f7ca363467835ab0d2c55dff85f1e5ec8476416", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 756, + "facing_y": -655, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 137, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 5, + "warnings": 0, + "x": -42, + "y": 216 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -802, + "facing_y": 598, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 318, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999825, + "result": null, + "round_number": 1, + "tick": 475 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 475 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "366840607c19aa47d9f15dd82de0f5bdd28900dc2a31d69b571bf8c4b0c41ac9", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 716, + "facing_y": -700, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 138, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 5, + "warnings": 0, + "x": -38, + "y": 222 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -775, + "facing_y": 632, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 319, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999824, + "result": null, + "round_number": 1, + "tick": 476 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 476 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0de5aa23da9b1bd8c0d3486bc7e676a0db0f10b4f279befb4afc6e74ed795c7d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 675, + "facing_y": -741, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 139, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 5, + "warnings": 0, + "x": -33, + "y": 227 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -747, + "facing_y": 665, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 319, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999823, + "result": null, + "round_number": 1, + "tick": 477 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 477 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "16fbf08d451ec0cfa410d6a49d6eee7a4445579d4a9f631734ccb68ec7ff7243", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 633, + "facing_y": -783, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 140, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 5, + "warnings": 0, + "x": -28, + "y": 232 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -717, + "facing_y": 697, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 320, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999822, + "result": null, + "round_number": 1, + "tick": 478 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 478 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0e878443a3dae156368a9cabb38d02bb4b431ac8cb17261b055c0c00fecffafb", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 586, + "facing_y": -818, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 141, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 5, + "warnings": 0, + "x": -23, + "y": 237 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -686, + "facing_y": 728, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 320, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999821, + "result": null, + "round_number": 1, + "tick": 479 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 479 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0531e69eb33a8f5f57f84785a068b123bc17674bd35da6da2b08998fd42f90e4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 540, + "facing_y": -844, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 142, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 5, + "velocity_y": 4, + "warnings": 0, + "x": -18, + "y": 241 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -653, + "facing_y": 757, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 321, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999820, + "result": null, + "round_number": 1, + "tick": 480 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 480 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "531127f7fd3e0c0998fca8291bb4430dd9506843c0b36d3b0a2a93c4591085c0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 495, + "facing_y": -876, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 143, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 5, + "velocity_y": 4, + "warnings": 0, + "x": -12, + "y": 244 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -620, + "facing_y": 785, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 321, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999819, + "result": null, + "round_number": 1, + "tick": 481 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 481 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ba2d1ac7010edbae25c85146bea21265fc21c9a75df1d38abe33431257661bda", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 442, + "facing_y": -901, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 144, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": 3, + "warnings": 0, + "x": -6, + "y": 248 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -585, + "facing_y": 811, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 322, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999818, + "result": null, + "round_number": 1, + "tick": 482 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 482 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a5cb4bcdd6c87645c68be36b1ccab8e765af29629d2201bf772c5ba48f4b055d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 385, + "facing_y": -926, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 145, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": 3, + "warnings": 0, + "x": 1, + "y": 251 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -549, + "facing_y": 836, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 322, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999817, + "result": null, + "round_number": 1, + "tick": 483 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 483 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f77337010bfcf1eb3b15a2b8179ffb9aa8da17e170e53ffb35ce67f89bc3e56e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 336, + "facing_y": -942, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 146, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": 2, + "warnings": 0, + "x": 7, + "y": 253 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -512, + "facing_y": 859, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 323, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999816, + "result": null, + "round_number": 1, + "tick": 484 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 484 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "12ad55bc40b28f5c5506cfd3d5e57e15e3ebd16f6a48b88c68daf193b4274687", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 280, + "facing_y": -966, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 147, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": 2, + "warnings": 0, + "x": 14, + "y": 255 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -474, + "facing_y": 881, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 323, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999815, + "result": null, + "round_number": 1, + "tick": 485 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 485 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "34e865330d815826e06d5d5ffd3be18eb2d72da1781f554b632466cb16c4bb5c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 229, + "facing_y": -975, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 148, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": 2, + "warnings": 0, + "x": 20, + "y": 257 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -435, + "facing_y": 900, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 324, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999814, + "result": null, + "round_number": 1, + "tick": 486 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 486 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3fc34ce9ccc8c311177d938817dd3c9b8baae8216d3e9dfe07a671020424f324", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 172, + "facing_y": -991, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 149, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": 1, + "warnings": 0, + "x": 27, + "y": 259 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -395, + "facing_y": 918, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 324, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999813, + "result": null, + "round_number": 1, + "tick": 487 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 487 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f45dded4702add34002629a0de1e962da56950388b32e721d1baff904aee872b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 114, + "facing_y": -1000, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 150, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": 1, + "warnings": 0, + "x": 34, + "y": 260 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -355, + "facing_y": 935, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 325, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999812, + "result": null, + "round_number": 1, + "tick": 488 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 488 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e8d1455b1fbb14a64e909605c54192d95d8002e938bdbba0c19a7c0c770f4d1e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 57, + "facing_y": -1000, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 151, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": 1, + "warnings": 0, + "x": 41, + "y": 260 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -314, + "facing_y": 949, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 325, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999811, + "result": null, + "round_number": 1, + "tick": 489 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 489 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "26137cdb33a85d57f986600027f221daa8f6a08acbf8da9a96e7e57935a7917c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 0, + "facing_y": -1000, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 152, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 7, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 260 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -272, + "facing_y": 962, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 326, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999810, + "result": null, + "round_number": 1, + "tick": 490 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 490 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b19cfb13e0b60d9e5ffc653a4115ee805b3b987b997ae2ff00c1c8511a19455d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -57, + "facing_y": -1000, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 153, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 7, + "velocity_y": 0, + "warnings": 0, + "x": 55, + "y": 260 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -230, + "facing_y": 973, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 326, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999809, + "result": null, + "round_number": 1, + "tick": 491 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 491 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "550c87f815f2b95baffefb1cfe370f67134440b4f8148074c4e9ae88b870070c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -114, + "facing_y": -1000, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 154, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -1, + "warnings": 0, + "x": 62, + "y": 260 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -187, + "facing_y": 982, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 327, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999808, + "result": null, + "round_number": 1, + "tick": 492 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 492 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "66cdd327ce776e87e57e5fe91a136dbc12e11fc745334674974aaa6b82975b45", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -172, + "facing_y": -991, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 155, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -1, + "warnings": 0, + "x": 69, + "y": 259 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -144, + "facing_y": 990, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 327, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999807, + "result": null, + "round_number": 1, + "tick": 493 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 493 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4fb926ad6b80eb134b14814c1ecc06d136e0d7bf396ad53db359bea337e6cfc8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -227, + "facing_y": -975, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 156, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -1, + "warnings": 0, + "x": 76, + "y": 258 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -101, + "facing_y": 995, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 328, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999806, + "result": null, + "round_number": 1, + "tick": 494 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 494 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e0f98f2ffaf226fa8b8125e865cdeb9c5fd78e6cbbacca2ff4214e0644803495", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -278, + "facing_y": -967, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 157, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -2, + "warnings": 0, + "x": 82, + "y": 256 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -57, + "facing_y": 998, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 328, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999805, + "result": null, + "round_number": 1, + "tick": 495 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 495 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "57c0e006a7a9277f5818fb7343ba9151d66b5cf400aa8c0e6cb904a2f60f2d31", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -336, + "facing_y": -942, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 158, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -2, + "warnings": 0, + "x": 89, + "y": 253 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -13, + "facing_y": 1000, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 329, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999804, + "result": null, + "round_number": 1, + "tick": 496 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 496 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c1e2aac052f38a85ab0aa4e95880d5e2ea25807f888cd7c331104dbbc802fb52", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -385, + "facing_y": -926, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 159, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": 95, + "y": 251 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 31, + "facing_y": 1000, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 329, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999803, + "result": null, + "round_number": 1, + "tick": 497 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 497 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dc2e3753b7ad40b831de8bb0327b635ab693136902be5883c6bc5b22fbd1aea6", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -442, + "facing_y": -901, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 160, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": 102, + "y": 248 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 75, + "facing_y": 997, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999802, + "result": null, + "round_number": 1, + "tick": 498 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 498 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fd3bef654bb6fa8050c40e5277ef72943d633b405e739d4a1cf1e78992bb13ab", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -495, + "facing_y": -876, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 161, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 6, + "velocity_y": -3, + "warnings": 0, + "x": 108, + "y": 244 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 1000, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 118, + "facing_y": 993, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 1000, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 1000, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999801, + "result": null, + "round_number": 1, + "tick": 499 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 499 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 0, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "d03b2be64817561ee44a44cf6f4882f7dec8dd7079981f898c3e9d5b10f5b32d", + "events": [ + { + "action_id": "two@500", + "actor_id": "two", + "amount": 0, + "blood": 0, + "detail": "right:straight:head", + "direction": 0, + "event_id": 23, + "kind": "punch_start", + "target_id": null, + "tick": 500 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -540, + "facing_y": -844, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 162, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 5, + "velocity_y": -4, + "warnings": 0, + "x": 114, + "y": 241 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 994, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 161, + "facing_y": 987, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 998, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 927, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999800, + "result": null, + "round_number": 1, + "tick": 500 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 500 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 1, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "07a143f83a0b8463f9bbbafb7a91fa5d3b3de00ec4526f60589f8f4e3b36c48e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -590, + "facing_y": -811, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 163, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 5, + "velocity_y": -4, + "warnings": 0, + "x": 120, + "y": 237 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 994, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 161, + "facing_y": 987, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 998, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 928, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999799, + "result": null, + "round_number": 1, + "tick": 501 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 501 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 2, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "079643f0852296aef79afc713fca27374d0ffae871bb3dfd6b9fe74725a14ff3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -631, + "facing_y": -778, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 164, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 5, + "velocity_y": -4, + "warnings": 0, + "x": 125, + "y": 233 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 994, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 161, + "facing_y": 987, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 998, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 929, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999798, + "result": null, + "round_number": 1, + "tick": 502 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 502 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 3, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "8476d11998458aaead015062a29b24d6c1612e241315129ad543ca55f5d8c366", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -677, + "facing_y": -743, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 165, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": -5, + "warnings": 0, + "x": 130, + "y": 228 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 994, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 161, + "facing_y": 987, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 998, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 930, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999797, + "result": null, + "round_number": 1, + "tick": 503 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 503 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 4, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "37b81b1d96894da9aab0343690de3e6cd8a8d463440f642059cf0bf0dca891b1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -719, + "facing_y": -702, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 166, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": -5, + "warnings": 0, + "x": 135, + "y": 223 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 994, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 161, + "facing_y": 987, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 998, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 931, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999796, + "result": null, + "round_number": 1, + "tick": 504 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 504 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 5, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "87fa6331cd51d81343bff78a5674be7edcf44204bed0fe9ce20b89433b0ee229", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -760, + "facing_y": -661, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 167, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": -5, + "warnings": 0, + "x": 140, + "y": 218 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 994, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 161, + "facing_y": 987, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 998, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999795, + "result": null, + "round_number": 1, + "tick": 505 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 505 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 6, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "c9f93776f8e6973585dc4f27eac5aed6684eec30aa30a9ed816c492b523758f8", + "events": [ + { + "action_id": "two@500", + "actor_id": "two", + "amount": 32, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 24, + "kind": "whiff", + "target_id": "one", + "tick": 506 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -795, + "facing_y": -614, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 168, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": -5, + "warnings": 0, + "x": 145, + "y": 213 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 161, + "facing_y": 987, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 901, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999794, + "result": null, + "round_number": 1, + "tick": 506 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 506 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 7, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "6aaad6dd620d9d31805e43f1a8d6388d9bbcf10a728926dd6621692c10234713", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -827, + "facing_y": -565, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 169, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": -5, + "warnings": 0, + "x": 149, + "y": 207 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 161, + "facing_y": 987, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 902, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999793, + "result": null, + "round_number": 1, + "tick": 507 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 507 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 8, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "2904a3807933a61baf129ced5d14bf2221265af06cbe66c53dac59432dac9ad2", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -860, + "facing_y": -516, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 170, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": -5, + "warnings": 0, + "x": 153, + "y": 201 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 161, + "facing_y": 987, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 903, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999792, + "result": null, + "round_number": 1, + "tick": 508 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 508 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 9, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "2a8cdc1c853f7fbf0d540c4f7f7050bd1d7998e7258afe708eabd12645c6e649", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -885, + "facing_y": -467, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 171, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 3, + "velocity_y": -6, + "warnings": 0, + "x": 156, + "y": 195 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 204, + "facing_y": 979, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 904, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999791, + "result": null, + "round_number": 1, + "tick": 509 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 509 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 10, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "f765f70742f37c80ad4c664294c73f9cecfe4fa31e7a74036ea7dd94ee7e7918", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -909, + "facing_y": -418, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 172, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 3, + "velocity_y": -6, + "warnings": 0, + "x": 159, + "y": 189 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 247, + "facing_y": 969, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 905, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999790, + "result": null, + "round_number": 1, + "tick": 510 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 510 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 11, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "2c811d436b1ba6367a6d395b3893e42ccbe13601789e2e6c7d3753e76c7aea8d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -934, + "facing_y": -368, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 173, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 3, + "velocity_y": -6, + "warnings": 0, + "x": 162, + "y": 183 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 289, + "facing_y": 957, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 906, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999789, + "result": null, + "round_number": 1, + "tick": 511 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 511 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 12, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "57a4d8ad24c7e2d8eb10468bad2389399d81e99639990dcf55df83274afb5c74", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -950, + "facing_y": -311, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 174, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 2, + "velocity_y": -6, + "warnings": 0, + "x": 164, + "y": 176 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 331, + "facing_y": 944, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 907, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999788, + "result": null, + "round_number": 1, + "tick": 512 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 512 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 13, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "5ea5b0472ce014e66c2038c7d02b0e31fe22b06c7c8bf9050a053b5ace3a6ee2", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -967, + "facing_y": -254, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 175, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 2, + "velocity_y": -6, + "warnings": 0, + "x": 166, + "y": 169 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 372, + "facing_y": 928, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 908, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999787, + "result": null, + "round_number": 1, + "tick": 513 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 513 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 14, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "40fbaa1ae3498c8967927cf100778ee61476bbd6adebbb3688f0f2ae7962921e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -983, + "facing_y": -198, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 176, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 1, + "velocity_y": -6, + "warnings": 0, + "x": 167, + "y": 162 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 412, + "facing_y": 911, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 909, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999786, + "result": null, + "round_number": 1, + "tick": 514 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 514 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 15, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "056a8aff9e2671ee37ef70790a50d7dceb9d239dadc7b8dfa5d052ab870664d5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": -147, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 177, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 1, + "velocity_y": -6, + "warnings": 0, + "x": 169, + "y": 156 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 451, + "facing_y": 892, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 910, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999785, + "result": null, + "round_number": 1, + "tick": 515 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 515 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 16, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "da9ebe3fe72091ab0fd77366fc663fbad5fc305c37e411a9c01b8f7c17190450", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": -90, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 178, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 1, + "velocity_y": -6, + "warnings": 0, + "x": 169, + "y": 149 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 490, + "facing_y": 872, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 911, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999784, + "result": null, + "round_number": 1, + "tick": 516 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 516 + }, + { + "attack_one": null, + "attack_two": { + "active": 2, + "age": 17, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "snapshot": { + "payload": { + "checksum": "6f0fe6e9d5895f7cda4b7581fc3f4183589de57cfb3b18741400b6e97d3b08bd", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": -32, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 179, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 1, + "velocity_y": -6, + "warnings": 0, + "x": 170, + "y": 142 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 527, + "facing_y": 850, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 912, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999783, + "result": null, + "round_number": 1, + "tick": 517 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 517 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4bc1c4232fdbe8f2c8b82cd6d6d24e6e50eae53d9975d1ce47a08110bd25cd61", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 24, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 180, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": -7, + "warnings": 0, + "x": 170, + "y": 135 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 564, + "facing_y": 826, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 331, + "queued_actions": 0, + "stamina": 916, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999782, + "result": null, + "round_number": 1, + "tick": 518 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 518 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8ed1f650ff122f5cba875da17737934d084178d6d6559b2faf4d9fd374cd56f1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 81, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 181, + "maximum_stamina": 996, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": -7, + "warnings": 0, + "x": 170, + "y": 128 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 599, + "facing_y": 800, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 331, + "queued_actions": 0, + "stamina": 920, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999781, + "result": null, + "round_number": 1, + "tick": 519 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 519 + }, + { + "attack_one": { + "active": 2, + "age": 0, + "class": "jab", + "combo_bonus": 0, + "hand": "left", + "power": "normal", + "recovery": 6, + "resolved": false, + "startup": 3, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d4c5d48e1dc23c9f8413493dd50a57ed4c446c3df5ef4ab82001fa3b2f630c8a", + "events": [ + { + "action_id": "one@520", + "actor_id": "one", + "amount": 0, + "blood": 0, + "detail": "left:jab:head", + "direction": 0, + "event_id": 25, + "kind": "punch_start", + "target_id": null, + "tick": 520 + } + ], + "fighters": [ + { + "action": "jab", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "left", + "action_id": "one@520", + "action_key": "jab:left:head:normal", + "action_power": "normal", + "action_recovery_ticks": 6, + "action_start_tick": 520, + "action_startup_ticks": 3, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 81, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 182, + "maximum_stamina": 995, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 955, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": -3, + "warnings": 0, + "x": 170, + "y": 124 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 634, + "facing_y": 774, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 332, + "queued_actions": 0, + "stamina": 924, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999780, + "result": null, + "round_number": 1, + "tick": 520 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 520 + }, + { + "attack_one": { + "active": 2, + "age": 1, + "class": "jab", + "combo_bonus": 0, + "hand": "left", + "power": "normal", + "recovery": 6, + "resolved": false, + "startup": 3, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "44c2be0647ab5680c9ca4c53a9f341568db4ae97e07f4156145f495dbf073e20", + "events": [], + "fighters": [ + { + "action": "jab", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "left", + "action_id": "one@520", + "action_key": "jab:left:head:normal", + "action_power": "normal", + "action_recovery_ticks": 6, + "action_start_tick": 520, + "action_startup_ticks": 3, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 81, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 995, + "player_id": "one", + "poise": 600, + "queued_actions": 1, + "stamina": 956, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": -2, + "warnings": 0, + "x": 170, + "y": 123 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 667, + "facing_y": 745, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 332, + "queued_actions": 0, + "stamina": 928, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999779, + "result": null, + "round_number": 1, + "tick": 521 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 521 + }, + { + "attack_one": { + "active": 2, + "age": 2, + "class": "jab", + "combo_bonus": 0, + "hand": "left", + "power": "normal", + "recovery": 6, + "resolved": false, + "startup": 3, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "577f88cb0a2146c700c6dfe7698f96c3ced4d860edc39cfabf7f7d90cd20b416", + "events": [], + "fighters": [ + { + "action": "jab", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "left", + "action_id": "one@520", + "action_key": "jab:left:head:normal", + "action_power": "normal", + "action_recovery_ticks": 6, + "action_start_tick": 520, + "action_startup_ticks": 3, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 81, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 995, + "player_id": "one", + "poise": 600, + "queued_actions": 1, + "stamina": 957, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": -1, + "warnings": 0, + "x": 170, + "y": 122 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 699, + "facing_y": 715, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 333, + "queued_actions": 0, + "stamina": 932, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 70, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 0, + "swelling": 20 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999778, + "result": null, + "round_number": 1, + "tick": 522 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 522 + }, + { + "attack_one": { + "active": 2, + "age": 3, + "class": "jab", + "combo_bonus": 0, + "hand": "left", + "power": "normal", + "recovery": 6, + "resolved": true, + "startup": 3, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "280c12df8389ef657baa2908e61089f828860100574958b178c13e2a6cde79a2", + "events": [ + { + "action_id": "one@520", + "actor_id": "one", + "amount": 33, + "blood": 8, + "detail": "jab:head", + "direction": -1, + "event_id": 26, + "kind": "hit", + "target_id": "two", + "tick": 523 + } + ], + "fighters": [ + { + "action": "jab", + "action_active_ticks": 2, + "action_contact_tick": 523, + "action_hand": "left", + "action_id": "one@520", + "action_key": "jab:left:head:normal", + "action_power": "normal", + "action_recovery_ticks": 6, + "action_start_tick": 520, + "action_startup_ticks": 3, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 81, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 995, + "player_id": "one", + "poise": 600, + "queued_actions": 1, + "stamina": 958, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 730, + "facing_y": 684, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 301, + "queued_actions": 0, + "stamina": 936, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 103, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 23, + "swelling": 25 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999777, + "result": null, + "round_number": 1, + "tick": 523 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 523 + }, + { + "attack_one": { + "active": 2, + "age": 4, + "class": "jab", + "combo_bonus": 0, + "hand": "left", + "power": "normal", + "recovery": 6, + "resolved": true, + "startup": 3, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b21b35268e6835ae91ff95d29e14b64e2f4481cac2082d9046ef0db45d0dc43d", + "events": [], + "fighters": [ + { + "action": "jab", + "action_active_ticks": 2, + "action_contact_tick": 523, + "action_hand": "left", + "action_id": "one@520", + "action_key": "jab:left:head:normal", + "action_power": "normal", + "action_recovery_ticks": 6, + "action_start_tick": 520, + "action_startup_ticks": 3, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 81, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 995, + "player_id": "one", + "poise": 600, + "queued_actions": 1, + "stamina": 959, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 759, + "facing_y": 651, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 302, + "queued_actions": 0, + "stamina": 940, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 103, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 23, + "swelling": 25 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999776, + "result": null, + "round_number": 1, + "tick": 524 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 524 + }, + { + "attack_one": { + "active": 2, + "age": 5, + "class": "jab", + "combo_bonus": 0, + "hand": "left", + "power": "normal", + "recovery": 6, + "resolved": true, + "startup": 3, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e79047fa46623486ccb04f66187a6e767c80c8d6f916396f5da7d952393a1093", + "events": [], + "fighters": [ + { + "action": "jab", + "action_active_ticks": 2, + "action_contact_tick": 523, + "action_hand": "left", + "action_id": "one@520", + "action_key": "jab:left:head:normal", + "action_power": "normal", + "action_recovery_ticks": 6, + "action_start_tick": 520, + "action_startup_ticks": 3, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 81, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 995, + "player_id": "one", + "poise": 600, + "queued_actions": 1, + "stamina": 960, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 787, + "facing_y": 617, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 302, + "queued_actions": 0, + "stamina": 944, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 103, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 23, + "swelling": 25 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999775, + "result": null, + "round_number": 1, + "tick": 525 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 525 + }, + { + "attack_one": { + "active": 2, + "age": 6, + "class": "jab", + "combo_bonus": 0, + "hand": "left", + "power": "normal", + "recovery": 6, + "resolved": true, + "startup": 3, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1ac91f634f62eab0fa7618ad66d39f733a34445fc439f5f4af7006e70fd02e54", + "events": [], + "fighters": [ + { + "action": "jab", + "action_active_ticks": 2, + "action_contact_tick": 523, + "action_hand": "left", + "action_id": "one@520", + "action_key": "jab:left:head:normal", + "action_power": "normal", + "action_recovery_ticks": 6, + "action_start_tick": 520, + "action_startup_ticks": 3, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -992, + "facing_y": 124, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 995, + "player_id": "one", + "poise": 600, + "queued_actions": 1, + "stamina": 961, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 813, + "facing_y": 582, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 303, + "queued_actions": 0, + "stamina": 948, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 103, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 23, + "swelling": 25 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999774, + "result": null, + "round_number": 1, + "tick": 526 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 526 + }, + { + "attack_one": { + "active": 2, + "age": 7, + "class": "jab", + "combo_bonus": 0, + "hand": "left", + "power": "normal", + "recovery": 6, + "resolved": true, + "startup": 3, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fbe6fb14649de60b96747f4dc8e976ebb331b88c1ad425b7f08ec3fbd138f56d", + "events": [], + "fighters": [ + { + "action": "jab", + "action_active_ticks": 2, + "action_contact_tick": 523, + "action_hand": "left", + "action_id": "one@520", + "action_key": "jab:left:head:normal", + "action_power": "normal", + "action_recovery_ticks": 6, + "action_start_tick": 520, + "action_startup_ticks": 3, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 995, + "player_id": "one", + "poise": 600, + "queued_actions": 1, + "stamina": 962, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 838, + "facing_y": 546, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 303, + "queued_actions": 0, + "stamina": 952, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 103, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 23, + "swelling": 25 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999773, + "result": null, + "round_number": 1, + "tick": 527 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 527 + }, + { + "attack_one": { + "active": 2, + "age": 0, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6a7ebd888707b66e7706357bd6ba0d307312c153aeac613c7723dc389fbe6924", + "events": [ + { + "action_id": "one@528", + "actor_id": "one", + "amount": 0, + "blood": 0, + "detail": "right:straight:head", + "direction": 0, + "event_id": 27, + "kind": "punch_start", + "target_id": null, + "tick": 528 + } + ], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 905, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 861, + "facing_y": 509, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 304, + "queued_actions": 0, + "stamina": 956, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 103, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 23, + "swelling": 25 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999772, + "result": null, + "round_number": 1, + "tick": 528 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 528 + }, + { + "attack_one": { + "active": 2, + "age": 1, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e3a87479a2e803fd8f61002b85113df8e6f5aa76a37dcd0d6c7b1b540b6e7f7c", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 906, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 882, + "facing_y": 471, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 304, + "queued_actions": 0, + "stamina": 960, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 103, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 23, + "swelling": 25 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999771, + "result": null, + "round_number": 1, + "tick": 529 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 529 + }, + { + "attack_one": { + "active": 2, + "age": 2, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b7fca15bc4b05f3ed09d517f09af34a6020ee64f2110cb377055a788fa33fd69", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 907, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 902, + "facing_y": 432, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 305, + "queued_actions": 0, + "stamina": 964, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 103, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 23, + "swelling": 25 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999770, + "result": null, + "round_number": 1, + "tick": 530 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 530 + }, + { + "attack_one": { + "active": 2, + "age": 3, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "15c5174f950a2ccf6fad2a5d9a38938d849d2dd5bbc351f4c749470922a110a1", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 908, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 920, + "facing_y": 392, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 305, + "queued_actions": 0, + "stamina": 968, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 103, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 23, + "swelling": 25 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999769, + "result": null, + "round_number": 1, + "tick": 531 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 531 + }, + { + "attack_one": { + "active": 2, + "age": 4, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "403803cebcf0f1ea382020a58521b4aa488a2847354eeec1e0034a3d872034d4", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 909, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 936, + "facing_y": 351, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 306, + "queued_actions": 0, + "stamina": 972, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 103, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 23, + "swelling": 25 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999768, + "result": null, + "round_number": 1, + "tick": 532 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 532 + }, + { + "attack_one": { + "active": 2, + "age": 5, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dfc6aacc4dfc3483296634970b8cae5b5ded470e864d0e9d95df68a4674a40ab", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 910, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 506, + "action_hand": "right", + "action_id": "two@500", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 500, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 951, + "facing_y": 310, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 306, + "queued_actions": 0, + "stamina": 976, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 103, + "left_cut": 0, + "left_eye": 44, + "right_cut": 0, + "right_eye": 23, + "swelling": 25 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999767, + "result": null, + "round_number": 1, + "tick": 533 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 533 + }, + { + "attack_one": { + "active": 2, + "age": 6, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3bfecaf9211088a64ee817cad18235a530c5a3cadbfaa0a53554673bd034291a", + "events": [ + { + "action_id": "one@528", + "actor_id": "one", + "amount": 63, + "blood": 15, + "detail": "straight:head", + "direction": -1, + "event_id": 28, + "kind": "hit", + "target_id": "two", + "tick": 534 + } + ], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 534, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 911, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 963, + "facing_y": 268, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 253, + "queued_actions": 0, + "stamina": 977, + "stance": "orthodox", + "stunned_ticks": 7, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999766, + "result": null, + "round_number": 1, + "tick": 534 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 534 + }, + { + "attack_one": { + "active": 2, + "age": 7, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b9bb16f087cdcb38ecc28a44b6ec137460b5fb890a81bf590fd8763efb76a3e4", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 534, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 912, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 974, + "facing_y": 226, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 253, + "queued_actions": 0, + "stamina": 978, + "stance": "orthodox", + "stunned_ticks": 6, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999765, + "result": null, + "round_number": 1, + "tick": 535 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 535 + }, + { + "attack_one": { + "active": 2, + "age": 8, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7290149d90bceec2d9e55f6657a20e5c3e50dd35e443c185bd6e6492ae4392dd", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 534, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 913, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 983, + "facing_y": 183, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 253, + "queued_actions": 0, + "stamina": 979, + "stance": "orthodox", + "stunned_ticks": 5, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999764, + "result": null, + "round_number": 1, + "tick": 536 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 536 + }, + { + "attack_one": { + "active": 2, + "age": 9, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b6864156138ef8a45d51358fb93959d6acbc1260198162436e0d902e370eb2cc", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 534, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 914, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 990, + "facing_y": 140, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 253, + "queued_actions": 0, + "stamina": 980, + "stance": "orthodox", + "stunned_ticks": 4, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999763, + "result": null, + "round_number": 1, + "tick": 537 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 537 + }, + { + "attack_one": { + "active": 2, + "age": 10, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "423bcaca0b5bce6e02aad33a169a0c5b59d774a005967cedae4b1c64cc3018b5", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 534, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 915, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 995, + "facing_y": 97, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 253, + "queued_actions": 0, + "stamina": 981, + "stance": "orthodox", + "stunned_ticks": 3, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999762, + "result": null, + "round_number": 1, + "tick": 538 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 538 + }, + { + "attack_one": { + "active": 2, + "age": 11, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7b6960cc6990bc03e9c4804e618898d13c6653fca471aafbc821fade46c37d46", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 534, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 916, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 999, + "facing_y": 54, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 253, + "queued_actions": 0, + "stamina": 982, + "stance": "orthodox", + "stunned_ticks": 2, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999761, + "result": null, + "round_number": 1, + "tick": 539 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 539 + }, + { + "attack_one": { + "active": 2, + "age": 12, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "46b31f5bf0efa4ed7527a16132706452740660894b42228b9520af995079bc63", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 534, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 917, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": 10, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 253, + "queued_actions": 0, + "stamina": 983, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999760, + "result": null, + "round_number": 1, + "tick": 540 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 540 + }, + { + "attack_one": { + "active": 2, + "age": 13, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "da3839d7305132b9c61ad7228b2ea39c0816c5fcfd1c5ba1d97d6856247679a1", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 534, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 918, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 999, + "facing_y": -34, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 253, + "queued_actions": 0, + "stamina": 987, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999759, + "result": null, + "round_number": 1, + "tick": 541 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 541 + }, + { + "attack_one": { + "active": 2, + "age": 14, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "55681c96643d66ce0112b2b03a0f9965bf7792d4733c60945caa8493613a5919", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 534, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 919, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 997, + "facing_y": -78, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 254, + "queued_actions": 0, + "stamina": 991, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999758, + "result": null, + "round_number": 1, + "tick": 542 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 542 + }, + { + "attack_one": { + "active": 2, + "age": 15, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "037d30fa154fd5907f93c56a023a6426fe81027a0c9d7b629044462c659d0900", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 534, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 920, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 993, + "facing_y": -121, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 254, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999757, + "result": null, + "round_number": 1, + "tick": 543 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 543 + }, + { + "attack_one": { + "active": 2, + "age": 16, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c40a77984182f4a62e46e0e3cc347092eea1ab7e1f313c14e8d3151a45c0a1db", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 534, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 921, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 255, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999756, + "result": null, + "round_number": 1, + "tick": 544 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 544 + }, + { + "attack_one": { + "active": 2, + "age": 17, + "class": "straight", + "combo_bonus": 10, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6028e6d1b44708fbe0d04730cad4fbccbc64f1af49740efdfa77661465d805d2", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 534, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 922, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 255, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999755, + "result": null, + "round_number": 1, + "tick": 545 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 545 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2febf8b3bc78e245c6cc9b342eed1cc95f4354e7248d85bded2fee5bcd168465", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 534, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 926, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 256, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999754, + "result": null, + "round_number": 1, + "tick": 546 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 546 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "79808045c1129827bd95209bb5dbec08c7a83eae33e79118a479f575a715162c", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 534, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 930, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 256, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999753, + "result": null, + "round_number": 1, + "tick": 547 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 547 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ae8b2dcef1d203252ee9a12d9503c260448dfd65423fed6a239be5448a6f294b", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 534, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 934, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 257, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999752, + "result": null, + "round_number": 1, + "tick": 548 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 548 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b2bcaf02e6cbe57c620dd48cf8aed59dab232c32b83cece0e216d8373933e08f", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 534, + "action_hand": "right", + "action_id": "one@528", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 528, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 983, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 183, + "maximum_stamina": 993, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 938, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 257, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999751, + "result": null, + "round_number": 1, + "tick": 549 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 549 + }, + { + "attack_one": { + "active": 3, + "age": 0, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": false, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b338ee16962ed534d84e4322dad5d56951e2af8a4580844738ce5783150122fb", + "events": [ + { + "action_id": "one@550", + "actor_id": "one", + "amount": 0, + "blood": 0, + "detail": "right:straight:head", + "direction": 0, + "event_id": 29, + "kind": "punch_start", + "target_id": null, + "tick": 550 + } + ], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 839, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 171, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999750, + "result": null, + "round_number": 1, + "tick": 550 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 550 + }, + { + "attack_one": { + "active": 3, + "age": 1, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": false, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "17af76df2959c7d427684bb685c60e0698b43acfa181a42ce0265af8a2ff5e18", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 840, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 171, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999749, + "result": null, + "round_number": 1, + "tick": 551 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 551 + }, + { + "attack_one": { + "active": 3, + "age": 2, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": false, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c2cd3a92ad3db7344e6c3c87b4934f7df3a897a0bb19b11257edddf1e2f4549d", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 841, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 172, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999748, + "result": null, + "round_number": 1, + "tick": 552 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 552 + }, + { + "attack_one": { + "active": 3, + "age": 3, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": false, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a88246abbadef54d7213b54fdd78668103f57cd31d1ecaf78497a53c446c7498", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 842, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 172, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999747, + "result": null, + "round_number": 1, + "tick": 553 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 553 + }, + { + "attack_one": { + "active": 3, + "age": 4, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": false, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "097a6de15df4a665d868d3fb10c0edcefa617bece4dbaca00a37a1b105240ef0", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 843, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 173, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999746, + "result": null, + "round_number": 1, + "tick": 554 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 554 + }, + { + "attack_one": { + "active": 3, + "age": 5, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": false, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "99464fbf94442fee73d569a2987ae20aff1355a64dd74a3728e50fcb00dea6eb", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 844, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 173, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999745, + "result": null, + "round_number": 1, + "tick": 555 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 555 + }, + { + "attack_one": { + "active": 3, + "age": 6, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": false, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8b2ffca67f1ec932616c7036b1795ca5585e046925094a77a4b7066e49be7462", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 845, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 174, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999744, + "result": null, + "round_number": 1, + "tick": 556 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 556 + }, + { + "attack_one": { + "active": 3, + "age": 7, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": false, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "19d64f35a87e328a3dc39169c7a4deaa6e9a879bd144b2c2934bfe980fb73e30", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 846, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 174, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 166, + "left_cut": 0, + "left_eye": 88, + "right_cut": 0, + "right_eye": 23, + "swelling": 45 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999743, + "result": null, + "round_number": 1, + "tick": 557 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 557 + }, + { + "attack_one": { + "active": 3, + "age": 8, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": true, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2c885c0ca7cd494ddbc3cbf9bdb50c087739421209e424a4afdb6fea42aec26b", + "events": [ + { + "action_id": "one@550", + "actor_id": "one", + "amount": 84, + "blood": 21, + "detail": "straight:head", + "direction": -1, + "event_id": 30, + "kind": "hit", + "target_id": "two", + "tick": 558 + }, + { + "action_id": null, + "actor_id": "one", + "amount": 84, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 31, + "kind": "stun", + "target_id": "two", + "tick": 558 + } + ], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": 558, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 847, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 29, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999742, + "result": null, + "round_number": 1, + "tick": 558 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 558 + }, + { + "attack_one": { + "active": 3, + "age": 9, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": true, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "81627b36b6481b8db2052afc580c995a3288cf6171a968ae7755d2dfb0af9778", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": 558, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 848, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 28, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999741, + "result": null, + "round_number": 1, + "tick": 559 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 559 + }, + { + "attack_one": { + "active": 3, + "age": 10, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": true, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "24f8a7be1fdaea22db0aa6d1ebb40885d238eaa7df20f1faa62cc6d7e29d3edd", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": 558, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 849, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 27, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999740, + "result": null, + "round_number": 1, + "tick": 560 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 560 + }, + { + "attack_one": { + "active": 3, + "age": 11, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": true, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b40804e1ac935ff2cbb60ced9d5cfa6986abff840491be4add1537c8e4033eb3", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": 558, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 850, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 26, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999739, + "result": null, + "round_number": 1, + "tick": 561 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 561 + }, + { + "attack_one": { + "active": 3, + "age": 12, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": true, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "69ad433f8534e143c09a34e4a506d18e3dff7cd60753149781b281cbcc947658", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": 558, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 851, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 25, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999738, + "result": null, + "round_number": 1, + "tick": 562 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 562 + }, + { + "attack_one": { + "active": 3, + "age": 13, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": true, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5efd742ada6819678f84cfba98bc9ce9a08fff837e91159a7c8e3f61831792af", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": 558, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 852, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 24, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999737, + "result": null, + "round_number": 1, + "tick": 563 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 563 + }, + { + "attack_one": { + "active": 3, + "age": 14, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": true, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9f622d59e6f6d2bee70516a12b0460937e5818107d89232d0eabf625e1ce2d8c", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": 558, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 853, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 23, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999736, + "result": null, + "round_number": 1, + "tick": 564 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 564 + }, + { + "attack_one": { + "active": 3, + "age": 15, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": true, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7560dd562dd38c27fcdbdbf8dd24a33de6c6ba64a9c213a525f4a1c82922ea5f", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": 558, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 854, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 22, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999735, + "result": null, + "round_number": 1, + "tick": 565 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 565 + }, + { + "attack_one": { + "active": 3, + "age": 16, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": true, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ad0a88a92c3c2dda362c4e4a5e806a53d8e7d3239723c9f1f85e93c1cd8062ef", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": 558, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 855, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 21, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999734, + "result": null, + "round_number": 1, + "tick": 566 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 566 + }, + { + "attack_one": { + "active": 3, + "age": 17, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": true, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1a77c7758df636feff4632c252f47b4cddbfee87f5ae7a9e20e864d591dd0f02", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": 558, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 856, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 20, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999733, + "result": null, + "round_number": 1, + "tick": 567 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 567 + }, + { + "attack_one": { + "active": 3, + "age": 18, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": true, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "42e1c0105c1e82309e63ef371a64bad4e1c67b671cbcd0497471dac43772c1b3", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": 558, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 857, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 19, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999732, + "result": null, + "round_number": 1, + "tick": 568 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 568 + }, + { + "attack_one": { + "active": 3, + "age": 19, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": true, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "96006c21d09332e3e7daec8a9aa6a37764817650f3dcd50efca2d02764a12357", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": 558, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 858, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 18, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999731, + "result": null, + "round_number": 1, + "tick": 569 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 569 + }, + { + "attack_one": { + "active": 3, + "age": 20, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": true, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2dea52584f886f24943f8a9913d77113e727092412eab6cbf965fa8d70b5a3af", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": 558, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 859, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 17, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999730, + "result": null, + "round_number": 1, + "tick": 570 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 570 + }, + { + "attack_one": { + "active": 3, + "age": 21, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": true, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "db78cdce9fb1c6c33f74536205861727af96173be66dee213cb1be4c0b054610", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": 558, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 860, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 16, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999729, + "result": null, + "round_number": 1, + "tick": 571 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 571 + }, + { + "attack_one": { + "active": 3, + "age": 22, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": true, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dea9d69664ca0a777f9c3e3997f5dc12e2222714464c82db662d69ac634886d0", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": 558, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 861, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 15, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999728, + "result": null, + "round_number": 1, + "tick": 572 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 572 + }, + { + "attack_one": { + "active": 3, + "age": 23, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "power", + "recovery": 13, + "resolved": true, + "startup": 8, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "16348989d8990111e3f6fb2c121e16a59d2053be590a9dbe260ca82c4ed88979", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": 558, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 862, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 14, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999727, + "result": null, + "round_number": 1, + "tick": 573 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 573 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c19529f8d09e6f32b5cd5ee3dc36d989ff6ae4597e1e2d0763e250c721d89fbe", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 3, + "action_contact_tick": 558, + "action_hand": "right", + "action_id": "one@550", + "action_key": "straight:right:head:power", + "action_power": "power", + "action_recovery_ticks": 13, + "action_start_tick": 550, + "action_startup_ticks": 8, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 975, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 184, + "maximum_stamina": 989, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 865, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 13, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999726, + "result": null, + "round_number": 1, + "tick": 574 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 574 + }, + { + "attack_one": { + "active": 2, + "age": 0, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9983ffbddcb0fb9facb9ea23f82a7e273aa6a779839729649d9e174321a69728", + "events": [ + { + "action_id": "one@575", + "actor_id": "one", + "amount": 0, + "blood": 0, + "detail": "right:straight:head", + "direction": 0, + "event_id": 32, + "kind": "punch_start", + "target_id": null, + "tick": 575 + } + ], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 801, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 12, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999725, + "result": null, + "round_number": 1, + "tick": 575 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 575 + }, + { + "attack_one": { + "active": 2, + "age": 1, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e0cd3d5a35a07977e3462845b2bdd5135f80a71eb332308b51735537ce453ef0", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 802, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 11, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999724, + "result": null, + "round_number": 1, + "tick": 576 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 576 + }, + { + "attack_one": { + "active": 2, + "age": 2, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6cc2548079f2edf86d11c39bf5076bc96145344313cdcae1caf2812c46ac8411", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 803, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 10, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999723, + "result": null, + "round_number": 1, + "tick": 577 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 577 + }, + { + "attack_one": { + "active": 2, + "age": 3, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "143685aa5fb1b7faddc097515014164fc08105a477fdc6d6da01b8f8a386e234", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 804, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 9, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999722, + "result": null, + "round_number": 1, + "tick": 578 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 578 + }, + { + "attack_one": { + "active": 2, + "age": 4, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c841ee043aac12d01d9a3545b326091c1cde96f34a31cd9dcd1272858338c87e", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 805, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 8, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999721, + "result": null, + "round_number": 1, + "tick": 579 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 579 + }, + { + "attack_one": { + "active": 2, + "age": 5, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": false, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "feb8f537ea13ad9e31e8a3ec0c9e7184cf5ab948440c3c9a474a9065121e1cba", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": null, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 806, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 97, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 7, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 250, + "left_cut": 0, + "left_eye": 146, + "right_cut": 0, + "right_eye": 23, + "swelling": 72 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999720, + "result": null, + "round_number": 1, + "tick": 580 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 580 + }, + { + "attack_one": { + "active": 2, + "age": 6, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "43283767347b385bfff8037a1f846b1d37d6662639d0ac06fdeaa582cdb2f327", + "events": [ + { + "action_id": "one@575", + "actor_id": "one", + "amount": 57, + "blood": 14, + "detail": "straight:head", + "direction": -1, + "event_id": 33, + "kind": "hit", + "target_id": "two", + "tick": 581 + } + ], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 807, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 44, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 6, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999719, + "result": null, + "round_number": 1, + "tick": 581 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 581 + }, + { + "attack_one": { + "active": 2, + "age": 7, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4fd8e50d2a71fdb7f51efa2ce369b4fc1282ab2c75c3d98b4464e9d20c3d0d56", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 808, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 44, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 5, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999718, + "result": null, + "round_number": 1, + "tick": 582 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 582 + }, + { + "attack_one": { + "active": 2, + "age": 8, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d0f394847535938e24cf93c7e490dbdb9cbae5f97291c10fbd18f92498518364", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 809, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 44, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 4, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999717, + "result": null, + "round_number": 1, + "tick": 583 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 583 + }, + { + "attack_one": { + "active": 2, + "age": 9, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d0b48816c2cb87b9c196fbcde6bb4f10ad4b8f073d74431c882ff36e49cc09d9", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 810, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 44, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 3, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999716, + "result": null, + "round_number": 1, + "tick": 584 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 584 + }, + { + "attack_one": { + "active": 2, + "age": 10, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1003d52efd16e0e7f9e8b149f1e74990bbb70f634c64a2c7e3c3d23742f7aab7", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 811, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 44, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 2, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999715, + "result": null, + "round_number": 1, + "tick": 585 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 585 + }, + { + "attack_one": { + "active": 2, + "age": 11, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4efc850371677d031f523dd5aabb35c42332f688cd3b837c6124555a27a6346d", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 812, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 44, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999714, + "result": null, + "round_number": 1, + "tick": 586 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 586 + }, + { + "attack_one": { + "active": 2, + "age": 12, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d20bdb1f08a342bfe6398caa9b50cbd816efcff4f7fdb21beb9e620b47b3fe4e", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 813, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 44, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999713, + "result": null, + "round_number": 1, + "tick": 587 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 587 + }, + { + "attack_one": { + "active": 2, + "age": 13, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "cd9b07123f0767ee94ed70a614d8ae9beea2f163c687daa8f6e3d418b0447af0", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 814, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 45, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999712, + "result": null, + "round_number": 1, + "tick": 588 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 588 + }, + { + "attack_one": { + "active": 2, + "age": 14, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0c7b93035e4554a4ab2de952d70a35a3c43c98991474888007e586a17e244d83", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 815, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 45, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999711, + "result": null, + "round_number": 1, + "tick": 589 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 589 + }, + { + "attack_one": { + "active": 2, + "age": 15, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "053615cd2b3e87665056a53fee0d5256d04b482e8fac1235dae6c9e5f41b4b19", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 816, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 46, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999710, + "result": null, + "round_number": 1, + "tick": 590 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 590 + }, + { + "attack_one": { + "active": 2, + "age": 16, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8bd6a809c8f9333c3c34fb6701d1b299310a99c1a7d520ce900a007e79247baa", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 817, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 46, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999709, + "result": null, + "round_number": 1, + "tick": 591 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 591 + }, + { + "attack_one": { + "active": 2, + "age": 17, + "class": "straight", + "combo_bonus": 0, + "hand": "right", + "power": "normal", + "recovery": 10, + "resolved": true, + "startup": 6, + "target": "head" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4d9be169805d06926cfb180e7d1e35d84c7db679783b63351b757ff8769090d1", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 818, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 47, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999708, + "result": null, + "round_number": 1, + "tick": 592 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 592 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3fbe1fc3f318fc0878d2c597718acbcc09e48ad737f3a2bc4ca6496f40b00c9d", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 821, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 47, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999707, + "result": null, + "round_number": 1, + "tick": 593 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 593 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "689603e71d94c5a207f69c44cede59b6450790e33e64ef42534a11b885b19dba", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 824, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 48, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 48, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999706, + "result": null, + "round_number": 1, + "tick": 594 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 594 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "79e3e2e6c2dadd483ec00eaa25ba812484b4b53d87ecbfdeaa5866a29c6e4b3f", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 138, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 827, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -141, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 48, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 3, + "velocity_y": 0, + "warnings": 0, + "x": 51, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999705, + "result": null, + "round_number": 1, + "tick": 595 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 595 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a533ff04f4cd05e5017419e9ff6ecedacd60856952294bf6d2822cc7f735a124", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 141, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 830, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -147, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 49, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 5, + "velocity_y": -1, + "warnings": 0, + "x": 56, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999704, + "result": null, + "round_number": 1, + "tick": 596 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 596 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "cefb735a017e9fb73f68dfa6888632146b6c321cdc8c68ea4e03ac2ea054305d", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 147, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 833, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 990, + "facing_y": -145, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 49, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 6, + "velocity_y": -1, + "warnings": 0, + "x": 61, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999703, + "result": null, + "round_number": 1, + "tick": 597 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 597 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bac2ba934a7481bd9985ac50cefd95e7f340a17e5636427228e9d42ee9a13862", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -990, + "facing_y": 145, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 836, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 990, + "facing_y": -145, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 50, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 6, + "velocity_y": -1, + "warnings": 0, + "x": 68, + "y": 136 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999702, + "result": null, + "round_number": 1, + "tick": 598 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 598 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bae222fd42d2a9cefde6181009887aeb8515649489bcbf5aade97439e90bfc9c", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -990, + "facing_y": 145, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 839, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -144, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 50, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 6, + "velocity_y": -1, + "warnings": 0, + "x": 74, + "y": 135 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999701, + "result": null, + "round_number": 1, + "tick": 599 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 599 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e0ba15f3606b0766b716059d806072f6108ba8096177ae335dcc0fd9e130f4b2", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 144, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 842, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -146, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 6, + "velocity_y": -1, + "warnings": 0, + "x": 81, + "y": 134 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999700, + "result": null, + "round_number": 1, + "tick": 600 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 600 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "19d8e54e428b77a0b9a1075a5e12d27106370624860cadd245f5654e3a7041fa", + "events": [ + { + "action_id": null, + "actor_id": "two", + "amount": 0, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 34, + "kind": "clinch_start", + "target_id": "one", + "tick": 601 + } + ], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 146, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 845, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 8, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -146, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 972, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 3, + "velocity_y": 0, + "warnings": 0, + "x": 84, + "y": 134 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999699, + "result": null, + "round_number": 1, + "tick": 601 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 601 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "62bc03f8f7499e8aff24be20616df6393ae197e7e9871c0d94e502d0251a4654", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 151, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 848, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 7, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -146, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 973, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 2, + "velocity_y": 0, + "warnings": 0, + "x": 85, + "y": 134 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999698, + "result": null, + "round_number": 1, + "tick": 602 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 602 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "231f4d4ff5198dc740d9c0baa2a2ac4e6621f26adc381ffc469c8c944bb1d342", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 152, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 851, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 6, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -146, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 974, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 1, + "velocity_y": 0, + "warnings": 0, + "x": 86, + "y": 133 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999697, + "result": null, + "round_number": 1, + "tick": 603 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 603 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "972a290311bb59b026a48b256f3d1e43bd5af14bed992402aa2dccc294316b86", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 142, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 854, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 5, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -146, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 975, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 87, + "y": 133 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999696, + "result": null, + "round_number": 1, + "tick": 604 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 604 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f7befe6c1d37bf6a5aa41dd20f75b5ea1ecc52c29edb6cf5891e95ab8e479652", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 144, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 857, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 4, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -146, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 976, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 87, + "y": 133 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999695, + "result": null, + "round_number": 1, + "tick": 605 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 605 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f46d22b3085f6190d5b0f0e513bb034d595798e5dba6ecc50de1eeb8805b175d", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 144, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 860, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 3, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -146, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 977, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 87, + "y": 133 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999694, + "result": null, + "round_number": 1, + "tick": 606 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 606 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9af5310a55d26a20b06e4e68f30feb87127f553e5263b7f89579992da1b7b146", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 144, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 863, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 2, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -146, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 978, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 87, + "y": 133 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999693, + "result": null, + "round_number": 1, + "tick": 607 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 607 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f00d740be35f4d22f365874fb0af683637d160c7bb65f00a63f2db5c02c1c9d0", + "events": [], + "fighters": [ + { + "action": "straight", + "action_active_ticks": 2, + "action_contact_tick": 581, + "action_hand": "right", + "action_id": "one@575", + "action_key": "straight:right:head:normal", + "action_power": "normal", + "action_recovery_ticks": 10, + "action_start_tick": 575, + "action_startup_ticks": 6, + "action_target": "head", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 144, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 866, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 1, + "clinch_ticks": 0, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -146, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 979, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 87, + "y": 133 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999692, + "result": null, + "round_number": 1, + "tick": 608 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 608 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "72b464234d57853a378e6430c2d267c9d5fed255b01bc80e43ec8efa236caac7", + "events": [ + { + "action_id": null, + "actor_id": "two", + "amount": 0, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 35, + "kind": "clinch", + "target_id": "one", + "tick": 609 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 45, + "conditioning": 970, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 144, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 847, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 170, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 45, + "conditioning": 991, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -146, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 960, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 87, + "y": 133 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999691, + "result": null, + "round_number": 1, + "tick": 609 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 609 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a3a1ea7357f2605e22e23c6941747f6cda3c104fc35f626edc49a91e97a51a0e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 44, + "conditioning": 969, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 144, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 849, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 164, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 44, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -144, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 962, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 93, + "y": 133 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999690, + "result": null, + "round_number": 1, + "tick": 610 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 610 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b5d6eb2b5cf99d9f0b5b7a43ac320a18f6a25fc429fc8901078ccbfc678f9021", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 43, + "conditioning": 969, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -986, + "facing_y": 166, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 851, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 43, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 986, + "facing_y": -166, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 964, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999689, + "result": null, + "round_number": 1, + "tick": 611 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 611 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9f2755e30f5fea8f81ae8ade45cf26a735bf8a66ec29e07b9f89474e404728bb", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 42, + "conditioning": 969, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 853, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 42, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 966, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999688, + "result": null, + "round_number": 1, + "tick": 612 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 612 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8ab193de17da1c3e593d62c829c0d45a95ffd987e073b9ddfd4721baf7d99fbe", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 41, + "conditioning": 969, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 855, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 41, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 968, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999687, + "result": null, + "round_number": 1, + "tick": 613 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 613 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "da04e32d650590ab5b5a52f1d72361e5bac7d0c964a37b947bc2ed16bf4d382c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 40, + "conditioning": 969, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 857, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 40, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 970, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999686, + "result": null, + "round_number": 1, + "tick": 614 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 614 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "18f76c674edadf1820d38f67d27fe306f7c9f749ad04001af78e02869d0113f7", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 39, + "conditioning": 969, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 859, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 39, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 972, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999685, + "result": null, + "round_number": 1, + "tick": 615 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 615 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "daa42285f54dc95e664fb0ccbbf229037a899b01c443e7f40a147bde73535f96", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 38, + "conditioning": 969, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 861, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 38, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 974, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999684, + "result": null, + "round_number": 1, + "tick": 616 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 616 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "66c0c94e1c3603f934be3e8671f959e201e4cc6cde39c67a6243637e3f1c38ed", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 37, + "conditioning": 969, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 863, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 37, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 976, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999683, + "result": null, + "round_number": 1, + "tick": 617 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 617 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d144523e2f23baab979b754a3d1a81ae1b7a5a5a0cd7bff661b5a9497621a546", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 36, + "conditioning": 969, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 865, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 36, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 978, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999682, + "result": null, + "round_number": 1, + "tick": 618 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 618 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0fc9664d844c50b27647e6cd49cd2a87be6cccaf13e7a41079c8b457cf893a6f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 35, + "conditioning": 969, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 987, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 867, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 35, + "conditioning": 990, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 980, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999681, + "result": null, + "round_number": 1, + "tick": 619 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 619 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4ca9162da6c0bb8658fa6d79e253b8ad7d94ae3b49c3d382806cb678515b9d9d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 34, + "conditioning": 968, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 869, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 34, + "conditioning": 989, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 982, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999680, + "result": null, + "round_number": 1, + "tick": 620 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 620 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ff596197e8bb669f03302b57bcc0f7a8566f57f085793bd8cadf84ee531d3a08", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 33, + "conditioning": 968, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 871, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 33, + "conditioning": 989, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999679, + "result": null, + "round_number": 1, + "tick": 621 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 621 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b0045bd1fc4ba2a29b67e894701b7eba2e151ea39508aeef98854b742679103f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 32, + "conditioning": 968, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 873, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 32, + "conditioning": 989, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 986, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999678, + "result": null, + "round_number": 1, + "tick": 622 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 622 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "472415f63a24a686c229f618dbbe9c1de33d90b81d70da51fecd9e21f6f20c6d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 31, + "conditioning": 968, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 875, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 31, + "conditioning": 989, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 988, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999677, + "result": null, + "round_number": 1, + "tick": 623 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 623 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4b6156985805ab464ff5b8de011f0f07c91de4fd7f75989f2ce54b37373b567b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 30, + "conditioning": 968, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 30, + "conditioning": 989, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 990, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999676, + "result": null, + "round_number": 1, + "tick": 624 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 624 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "03edfd886362e1aa98df139611367fa1e19def2a99dad8c58a6530613481b589", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 29, + "conditioning": 968, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 879, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 29, + "conditioning": 989, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 992, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999675, + "result": null, + "round_number": 1, + "tick": 625 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 625 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9c867a2922562690440e4c5114cec627df7906ba88992a744727030faa8541e6", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 28, + "conditioning": 968, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 881, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 28, + "conditioning": 989, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 994, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999674, + "result": null, + "round_number": 1, + "tick": 626 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 626 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5a6168100e0ccfd8886d4b68e679e45d822609c8f2744d7b87d83c932bb92403", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 27, + "conditioning": 968, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 883, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 27, + "conditioning": 989, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999673, + "result": null, + "round_number": 1, + "tick": 627 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 627 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "48eb134770cc3289d1ba59cd091f89645d14fd261af46a3bd0303a9d61d4624b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 26, + "conditioning": 968, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 885, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 26, + "conditioning": 989, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999672, + "result": null, + "round_number": 1, + "tick": 628 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 628 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dee2a494f6d530f08c21f52457a281b4b5f7ab20c7029acb7fb1bca5b07c0a31", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 25, + "conditioning": 968, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 887, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 25, + "conditioning": 989, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 996, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 996, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999671, + "result": null, + "round_number": 1, + "tick": 629 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 629 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d5d55d283ebf01fc21c2ae4f97c927f8aefee0c178320caf528ba32ab142057d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 24, + "conditioning": 967, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 889, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 24, + "conditioning": 988, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999670, + "result": null, + "round_number": 1, + "tick": 630 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 630 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0cea5c0f8d83b834067b718d4545ebb1986acaeeecbb49b41aec2b53f489ebf0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 23, + "conditioning": 967, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 891, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 23, + "conditioning": 988, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999669, + "result": null, + "round_number": 1, + "tick": 631 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 631 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7377d894fe96776a667b84626963109c8ece8e268b6d0df2af3a21a6c107a4e5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 22, + "conditioning": 967, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 893, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 22, + "conditioning": 988, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999668, + "result": null, + "round_number": 1, + "tick": 632 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 632 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "218e52cf821fc0434e82d989bdce79e72fe7761a6732a0fb644d3a884c11b36a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 21, + "conditioning": 967, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 895, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 21, + "conditioning": 988, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999667, + "result": null, + "round_number": 1, + "tick": 633 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 633 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b693b5e83590034305ce67c64917d83b5d39dc1950596eaf7db839c0e96a8cfc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 20, + "conditioning": 967, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 897, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 20, + "conditioning": 988, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999666, + "result": null, + "round_number": 1, + "tick": 634 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 634 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0e231d2726d1da87c9694be4a15180f913c897d8b4c88e84a51715487e6e26da", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 19, + "conditioning": 967, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 899, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 19, + "conditioning": 988, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999665, + "result": null, + "round_number": 1, + "tick": 635 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 635 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "60eb5440ab44af04b6f754132c8416a37ca89e9e4c9d38dc419bfc270f76d17b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 18, + "conditioning": 967, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 901, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 18, + "conditioning": 988, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999664, + "result": null, + "round_number": 1, + "tick": 636 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 636 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1a1b919cd7013aaa93a4171dfb80b9a07e7eb1addc8116411e6603bd3d1cbf92", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 17, + "conditioning": 967, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 903, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 17, + "conditioning": 988, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999663, + "result": null, + "round_number": 1, + "tick": 637 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 637 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dd683ec6bbfd2221bf32f8b3db2574a8074e8edaf7c8774e97c5afbc75d95096", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 16, + "conditioning": 967, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 905, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 16, + "conditioning": 988, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999662, + "result": null, + "round_number": 1, + "tick": 638 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 638 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f3c159f73e81217cc30e1a9aee7e9eb96f7e93f538b560e926eada9e78e965d3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 15, + "conditioning": 967, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 986, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 907, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 15, + "conditioning": 988, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999661, + "result": null, + "round_number": 1, + "tick": 639 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 639 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3758de1098668236c56f2ebbd505c8d5f1270ed0b2da19b4b0fb480c75a19a6f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 14, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 909, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 14, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999660, + "result": null, + "round_number": 1, + "tick": 640 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 640 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "715b4c501953b63ff257271bbf7a0337df7f6ee1dbc9c0f84354839d8e9c24b3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 13, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 911, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 13, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999659, + "result": null, + "round_number": 1, + "tick": 641 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 641 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ac4fdd095ef57a3bdadaa4c5a77cbb3949c74eac32c7d2e04b848464d65bd476", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 12, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 913, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 12, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999658, + "result": null, + "round_number": 1, + "tick": 642 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 642 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bbb721a8d5ae33c811b1afbb041844be8e03f6a5bb069381f22791275851ec63", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 11, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 915, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 11, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999657, + "result": null, + "round_number": 1, + "tick": 643 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 643 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6527fd5e0b4b85fa62ceaacc9559e421df31e851c174f566f1579ea7b0a56577", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 10, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 917, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 10, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999656, + "result": null, + "round_number": 1, + "tick": 644 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 644 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bf3f3385c331ea8882f47067a1bf23f259b929b1d9602be8c1c530571d06c9b9", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 9, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 919, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 9, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999655, + "result": null, + "round_number": 1, + "tick": 645 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 645 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d287e099ba9f1bc0febc5f8578dde2b196e111112f9184d18c7eebe62d8feed0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 8, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 921, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 8, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999654, + "result": null, + "round_number": 1, + "tick": 646 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 646 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c7585ccff44e41cc6d3901b51fb76e3252ebf9693839732b72e8e056347c6253", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 7, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 923, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 7, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999653, + "result": null, + "round_number": 1, + "tick": 647 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 647 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "194602f21fd4150e740e328dbf328fc6c13a1669e30e2dbcbd3a94bc4c91bbcc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 6, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 925, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 6, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999652, + "result": null, + "round_number": 1, + "tick": 648 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 648 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6f6a04c21c2e831ce9004f8e7e675b5bb5361761e5ca1a7b2d837e9fe0b2fc94", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 5, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 927, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 5, + "conditioning": 987, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 995, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 995, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999651, + "result": null, + "round_number": 1, + "tick": 649 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 649 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "32d0df85a37a70c1fb85867b892dbe249e45e6fba5fc0286d427cf86e1feea05", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 4, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 929, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 4, + "conditioning": 986, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 994, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 994, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999650, + "result": null, + "round_number": 1, + "tick": 650 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 650 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5e080e7a40024393cc1e821f03514bc623572260f4006c8150b708876f45bfe4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 3, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 931, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 3, + "conditioning": 986, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 994, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 994, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999649, + "result": null, + "round_number": 1, + "tick": 651 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 651 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "380f9c38864fa7d27f6eea66794ef0e1cbc123f6503a34a83b91297fb331bac9", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 2, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 933, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 2, + "conditioning": 986, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 994, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 994, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999648, + "result": null, + "round_number": 1, + "tick": 652 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 652 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7337e937346a5db5999e52e339c29a3b0f73cb34ca9493fe63ca73b1c9eaa4c3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 1, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 935, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 159, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 1, + "conditioning": 986, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 994, + "player_id": "two", + "poise": 51, + "queued_actions": 0, + "stamina": 994, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 98, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999647, + "result": null, + "round_number": 1, + "tick": 653 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 653 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ae514226986a2c93ee7d0dd905e451a77ce8b1111fb5c9f0891b3ef6207291e1", + "events": [ + { + "action_id": null, + "actor_id": "one", + "amount": 0, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 36, + "kind": "referee_break", + "target_id": "two", + "tick": 654 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 939, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 204, + "y": 115 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 986, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 994, + "player_id": "two", + "poise": 52, + "queued_actions": 0, + "stamina": 994, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 53, + "y": 139 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999646, + "result": null, + "round_number": 1, + "tick": 654 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 654 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e90015e01cdc572828dcb074316338bdcf1790e7b102cab4fdb785626be4e7cc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -993, + "facing_y": 157, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 942, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 204, + "y": 115 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 986, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 993, + "facing_y": -157, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 994, + "player_id": "two", + "poise": 52, + "queued_actions": 0, + "stamina": 994, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 53, + "y": 139 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999645, + "result": null, + "round_number": 1, + "tick": 655 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 655 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "306e1fe82deb5487b66d555d90196edae828a62c5640f42fe045fb2db9603e06", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -993, + "facing_y": 157, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 945, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 204, + "y": 115 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 986, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 993, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 994, + "player_id": "two", + "poise": 53, + "queued_actions": 0, + "stamina": 994, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 3, + "velocity_y": -1, + "warnings": 0, + "x": 56, + "y": 139 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999644, + "result": null, + "round_number": 1, + "tick": 656 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 656 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9d2ee11962dd35e39ca4dc17423852b87615a5507c4dc6053ccb853b095b8d3e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -993, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 948, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 204, + "y": 115 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 986, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 993, + "facing_y": -159, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 994, + "player_id": "two", + "poise": 53, + "queued_actions": 0, + "stamina": 994, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 5, + "velocity_y": -1, + "warnings": 0, + "x": 61, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999643, + "result": null, + "round_number": 1, + "tick": 657 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 657 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e7aa781df6e5638cff4e9df0873b9bd464185c5101f098243025156750489810", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -993, + "facing_y": 159, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 951, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 204, + "y": 115 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 986, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 992, + "facing_y": -159, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 994, + "player_id": "two", + "poise": 54, + "queued_actions": 0, + "stamina": 994, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 6, + "velocity_y": -1, + "warnings": 0, + "x": 67, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999642, + "result": null, + "round_number": 1, + "tick": 658 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 658 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ed435302302413aa92da66cd7460d36fe21b9e91af7e5bcb1a4eac101b77899f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -992, + "facing_y": 159, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 954, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 204, + "y": 115 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 986, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 992, + "facing_y": -159, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 994, + "player_id": "two", + "poise": 54, + "queued_actions": 0, + "stamina": 994, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 6, + "velocity_y": -1, + "warnings": 0, + "x": 73, + "y": 136 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999641, + "result": null, + "round_number": 1, + "tick": 659 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 659 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5577eda3231fd594ac9e470020fe0b289ef55bdf28e9136ce65a87df42416f47", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -992, + "facing_y": 159, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 957, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 204, + "y": 115 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 986, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 992, + "facing_y": -158, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 994, + "player_id": "two", + "poise": 55, + "queued_actions": 0, + "stamina": 994, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 6, + "velocity_y": -1, + "warnings": 0, + "x": 79, + "y": 135 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999640, + "result": null, + "round_number": 1, + "tick": 660 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 660 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3d02fb67125cb85eeeee244f8d5a2cdd1eab7672014a5fe6f2e5a5b5d07fb6a5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -992, + "facing_y": 158, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 960, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 204, + "y": 115 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 986, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -159, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 994, + "player_id": "two", + "poise": 55, + "queued_actions": 0, + "stamina": 994, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 6, + "velocity_y": -1, + "warnings": 0, + "x": 86, + "y": 134 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999639, + "result": null, + "round_number": 1, + "tick": 661 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 661 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "74e69e5d398ac1ebb163bc38380d1ac17d14018d8a893ecde72b5ad19b536935", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 159, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 963, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 204, + "y": 115 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 986, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -159, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 994, + "player_id": "two", + "poise": 56, + "queued_actions": 0, + "stamina": 994, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 6, + "velocity_y": -1, + "warnings": 0, + "x": 92, + "y": 133 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999638, + "result": null, + "round_number": 1, + "tick": 662 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 662 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8812d3b1d5b62d638ebbb8cb6b3aec08ca3bbeb557425af32647ea3e6860065a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 159, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 966, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 204, + "y": 115 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 986, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 990, + "facing_y": -160, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 994, + "player_id": "two", + "poise": 56, + "queued_actions": 0, + "stamina": 994, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 6, + "velocity_y": -1, + "warnings": 0, + "x": 99, + "y": 132 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999637, + "result": null, + "round_number": 1, + "tick": 663 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 663 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "091dc95fafda9c2fcfd1de9946b7c5ba143b560878321ba397cdbd4ee75b353c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -990, + "facing_y": 160, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 969, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 204, + "y": 115 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 986, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 994, + "player_id": "two", + "poise": 57, + "queued_actions": 0, + "stamina": 994, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 6, + "velocity_y": -1, + "warnings": 0, + "x": 106, + "y": 131 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999636, + "result": null, + "round_number": 1, + "tick": 664 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 664 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "99d410940b7e1e273cd2d77ac58e202d049472d395b7e61bf220b914a3cb06a1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 972, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 204, + "y": 115 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 986, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 994, + "player_id": "two", + "poise": 57, + "queued_actions": 0, + "stamina": 994, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 6, + "velocity_y": -1, + "warnings": 0, + "x": 112, + "y": 130 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999635, + "result": null, + "round_number": 1, + "tick": 665 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 665 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9163e577acd4651ba4d537a466c4a93d4b8743ec30843b719fd6a47e7d520bec", + "events": [ + { + "action_id": null, + "actor_id": "two", + "amount": 0, + "blood": 0, + "detail": "low_blow", + "direction": 0, + "event_id": 37, + "kind": "foul", + "target_id": "one", + "tick": 666 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 973, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 60, + "result": null, + "round_number": 1, + "tick": 666 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 666 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e1436ba49c538e9a06184988b789418d1f92664662f3a307500c29912362f05d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 977, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 59, + "result": null, + "round_number": 1, + "tick": 667 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 667 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "739577867f493cfff35444be6535e3d9a576bbce6a417fe603f5fc880a794d79", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 981, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 58, + "result": null, + "round_number": 1, + "tick": 668 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 668 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "75eb598906e2248cce85e8f46ccb2beb82222464509977316557f19d4c48bb09", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 57, + "result": null, + "round_number": 1, + "tick": 669 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 669 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d0ada636e8fdb786e9c08b80b64210b311faa0820b355e92bcd783737dab3b96", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 56, + "result": null, + "round_number": 1, + "tick": 670 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 670 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "90ec51ce19a2f679b6285e573b4b37656814c92365194cb60fdabad1975d3501", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 55, + "result": null, + "round_number": 1, + "tick": 671 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 671 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8e5d511cce4e2532c726185255a5196ddfea93a9b20588adfe0ab4242b9d3d50", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 54, + "result": null, + "round_number": 1, + "tick": 672 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 672 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d3d5d5ac1965ef7703856703bdb4eaa5412210eb3e483141e243018b86e3716d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 53, + "result": null, + "round_number": 1, + "tick": 673 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 673 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9a7b8c0d2f00667b84c76e0870cb0ea4580a23067947726f9a2263313f5302b4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 52, + "result": null, + "round_number": 1, + "tick": 674 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 674 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1c6627bb84eb9553f9d873f6a7c789cfd3de026ab476892617dd117e8b6a0944", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 51, + "result": null, + "round_number": 1, + "tick": 675 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 675 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1cc51001d33d4f03ce5b6048e112191d07de82d4a2039794dc469dd2fdab847f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 50, + "result": null, + "round_number": 1, + "tick": 676 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 676 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4c861f3d5fa680529b5cca983a7bd9259a770cc81d5344d4962274bd592433df", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 49, + "result": null, + "round_number": 1, + "tick": 677 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 677 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "90ba2d685a466867a62697740869cd4c83125f2262cafa9ac108d441d960fccb", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 48, + "result": null, + "round_number": 1, + "tick": 678 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 678 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9d5663a1e6b107129b8bc1eeecbfee4a748f5e9dc1560ad6832fccd2a579f317", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 47, + "result": null, + "round_number": 1, + "tick": 679 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 679 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "efd4bdb34ac15721842b4d29fb88e1e7f52bc8de7174aacff8cf55004bd30c29", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 46, + "result": null, + "round_number": 1, + "tick": 680 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 680 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f0656a207b4a24bec4bce154e736233b370ee88cb6a94135bf06329b8bfc21f6", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 45, + "result": null, + "round_number": 1, + "tick": 681 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 681 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dff8d59c58e4239a7dc2bf33bee1b1ade95d58f95eb234ab564f76f5f9a4270f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 44, + "result": null, + "round_number": 1, + "tick": 682 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 682 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6fcbfbdb21b5067cf231503d0a681922198b583f9ebd0cf89230e26900b8132f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 43, + "result": null, + "round_number": 1, + "tick": 683 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 683 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9ed2a59e860da09b6675767ba80df6c0923c31cd86d7a83378cb6082f49bb631", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 42, + "result": null, + "round_number": 1, + "tick": 684 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 684 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "26f827e78ed673ed427d1d0024bd305c14e3266e700a733f1b5677e292982d1f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 41, + "result": null, + "round_number": 1, + "tick": 685 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 685 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "92a5a4ec4744fdd1e876b63ef65f7a02535fdbd01de48c26a95df44a62254e17", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 40, + "result": null, + "round_number": 1, + "tick": 686 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 686 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "cd3cdde6dc15367c01e932996b2cf846ad41cc01cbc81ef293e31051d6f1bfe5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 39, + "result": null, + "round_number": 1, + "tick": 687 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 687 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f8bf906ef1d68e28da2a3ce674fb887c22ed72cd7f9a7e96719101bff47e391c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 38, + "result": null, + "round_number": 1, + "tick": 688 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 688 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e96cee4c87822d501c744ae079194d9f9ff16480e9217732781a9aa2d1661750", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 37, + "result": null, + "round_number": 1, + "tick": 689 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 689 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d18ad026126f6a75195c71ea25289843696416278118595f74ea33fdfa1f3d6d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 36, + "result": null, + "round_number": 1, + "tick": 690 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 690 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "34d857eb1c5cc01cfa8defe83798b477e87e871c377ea0a4ed39afd05c8b82f4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 35, + "result": null, + "round_number": 1, + "tick": 691 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 691 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "eb5b5b96546eab5a3a0036cfbe9fb8093e39fcf22f875b85798fa3e745da1781", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 34, + "result": null, + "round_number": 1, + "tick": 692 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 692 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6d00d1a0accb5bc4418e0ecaaae7cb9d85c9285347240bf2cbb2f5717e4892ea", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 33, + "result": null, + "round_number": 1, + "tick": 693 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 693 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7bb3b075686508031682490c67e982e80347f010576792847b3e54e48e16ce2a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 32, + "result": null, + "round_number": 1, + "tick": 694 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 694 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9705ba0edd635cc92cf095f9064f377cd0f4dc81991c37e6ef1f5b919b2e7d17", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 31, + "result": null, + "round_number": 1, + "tick": 695 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 695 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9e0bce5319737e210e2d5edcadd0a73f0d094fa41159125e261041fe4cdb77b5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 30, + "result": null, + "round_number": 1, + "tick": 696 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 696 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "85cdfdda7d372cb910b884e72e2e8ceb2a2e3741ba1390b9921a69342ea1859f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 29, + "result": null, + "round_number": 1, + "tick": 697 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 697 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e9d0d2ad3e7559af8cc3a967a53a5829966b365508983716d553f9ac99713ffa", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 28, + "result": null, + "round_number": 1, + "tick": 698 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 698 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "15fbd80fc4f86e5883f47d2162dd6d0b960452339162d1b46501afcb72dc1d3a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 27, + "result": null, + "round_number": 1, + "tick": 699 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 699 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9d9b3bdad089085a2132791007987a7ccc9586259fed46cee17b8946f4838b3a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 26, + "result": null, + "round_number": 1, + "tick": 700 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 700 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "84dd5d6a9a6c6ee796823a41fd3d126775db0ec0b005aecbffd753dec6dbbbb3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 25, + "result": null, + "round_number": 1, + "tick": 701 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 701 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dbbd958fe8be9873fd9c3515702a23920e78d1750f4d088aec81f6066e08f9e5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 24, + "result": null, + "round_number": 1, + "tick": 702 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 702 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "95006f33a0b55186e1e5ea10e6ec8d78da25f2c3a485d2352ddbf1b37f3cdb87", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 23, + "result": null, + "round_number": 1, + "tick": 703 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 703 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d6f952e10bc8d70403fbe73c579cb3490d6ec9f377e6449c4870dd042ba815df", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 22, + "result": null, + "round_number": 1, + "tick": 704 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 704 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0a7efbdc23508f4122172a56b5b457e841147d75b26ec47b65df0c7e8086daf7", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 21, + "result": null, + "round_number": 1, + "tick": 705 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 705 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bf74e456b7733ed3d6a1a1016b3c5a8f386534f013d26cccba21cf1c8b209403", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 20, + "result": null, + "round_number": 1, + "tick": 706 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 706 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2457348732e97bc66f7a632ca5a3cad64bf05fcac5b12f0d800c1732c350cb1c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 19, + "result": null, + "round_number": 1, + "tick": 707 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 707 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5d5a0e19a0a3918b7e28043353f85a0a7c1ae89b7914531a4a955926d547a9e4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 18, + "result": null, + "round_number": 1, + "tick": 708 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 708 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0b77cc616ca9b5d9645049f402aecb592e8d1956dfe5f01779748b4a3bbea784", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 17, + "result": null, + "round_number": 1, + "tick": 709 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 709 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6b9196cc6a7aecaf4cef01ae33b3c4a290bddc011328c90c0bf5f71bbeda9695", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 16, + "result": null, + "round_number": 1, + "tick": 710 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 710 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "12600ba803e0793198534d865f4a1fb5eac31674fa19e368ea638457056d57cc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 15, + "result": null, + "round_number": 1, + "tick": 711 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 711 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b4d51788101e4183d59716718c9bf8189a58c82975ca3ec6fb61dec7a3cf0fad", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 14, + "result": null, + "round_number": 1, + "tick": 712 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 712 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "43252e3982b0fcbdb56900d72bd9a46dbebc93a9ed6d98306f58877394a9c212", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 13, + "result": null, + "round_number": 1, + "tick": 713 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 713 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3932271ae93b77f1947b0ac6836715ea9b9374c40378f2efbf1f586465fcdd3f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 12, + "result": null, + "round_number": 1, + "tick": 714 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 714 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "de2c47c59adf679013a3f4cb86586cda74f6504774283109c4861ecf6e186784", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 11, + "result": null, + "round_number": 1, + "tick": 715 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 715 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6427fbee6ba0f0276eceb7cba38db306ecf208ba8a33c05f1ee83f4b6fa20725", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 10, + "result": null, + "round_number": 1, + "tick": 716 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 716 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a7330aef29fc895e99e276cef1667bfe551aa3b0037f63689338b49b30310891", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 9, + "result": null, + "round_number": 1, + "tick": 717 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 717 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f8bc785a423f01e5ff843684f702bfb4518c1c594f4097edee11e032a6e4865d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 8, + "result": null, + "round_number": 1, + "tick": 718 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 718 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "80eff065facf0a06ce59908471e46f18536e3b991e03487682f6e5c97d2d8e37", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 7, + "result": null, + "round_number": 1, + "tick": 719 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 719 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "257b84d3ffc68da2d4eef0d94afe282e1247ec4d1258cc2dd41f446ab83508fd", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 6, + "result": null, + "round_number": 1, + "tick": 720 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 720 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3841b060942f82f9ba89ea785d1493ab7c2e392928cccf4df5a70a27ffec517e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 5, + "result": null, + "round_number": 1, + "tick": 721 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 721 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2515cf6efb6a21e571219120aec985f7aa88fba01df945d40d3af406144607da", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 4, + "result": null, + "round_number": 1, + "tick": 722 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 722 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dc012e6fa9fdf5e7df1a93e9634a39913fb881ea7460a8f9bd45f0bc7ff84c2b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 3, + "result": null, + "round_number": 1, + "tick": 723 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 723 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1ea3b992f8db12acc70114d3e8bfd2bd528fcbf7682c8bed694e006e3267d559", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 2, + "result": null, + "round_number": 1, + "tick": 724 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 724 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dea79ee75a86848d7f6f3c2d433173f3dedef3e53842febc1e52ae9f729ef9af", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": true, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 30, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "foul_recovery", + "phase_ticks_remaining": 1, + "result": null, + "round_number": 1, + "tick": 725 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 725 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a4f335e0608a1e54c6ae96feb76dfc1d748194c4f31975bdb653c6623b187c6f", + "events": [ + { + "action_id": null, + "actor_id": null, + "amount": 0, + "blood": 0, + "detail": "foul_recovery_complete", + "direction": 0, + "event_id": 38, + "kind": "resume", + "target_id": null, + "tick": 726 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999634, + "result": null, + "round_number": 1, + "tick": 726 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 726 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "cd055eafdeb3bf582b8d8a9541124dabb1f291f0c5a2b81e50c2673dee02b241", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 159, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 185, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 248, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -159, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 58, + "queued_actions": 0, + "stamina": 880, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999633, + "result": null, + "round_number": 1, + "tick": 727 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 727 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bdbf092ae0f982d339b04d62522ff5f2f7aa6cf253497c13a472cffa0064bbe0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -988, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 186, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 0, + "warnings": 0, + "x": 246, + "y": 108 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -159, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 59, + "queued_actions": 0, + "stamina": 883, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999632, + "result": null, + "round_number": 1, + "tick": 728 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 728 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "017232ca430188d253b19c038cd5f3262a9bb57fd48fff1d91f7d58fbaa2a6a3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -988, + "facing_y": 160, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 187, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": 1, + "warnings": 0, + "x": 241, + "y": 109 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 988, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 59, + "queued_actions": 0, + "stamina": 886, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999631, + "result": null, + "round_number": 1, + "tick": 729 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 729 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fb9500735138ba82be9fe61bd6817b6006bd29e5c5be93ccdb0e7a81975e403e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -988, + "facing_y": 158, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 188, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -5, + "velocity_y": 1, + "warnings": 0, + "x": 236, + "y": 110 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 988, + "facing_y": -160, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 60, + "queued_actions": 0, + "stamina": 889, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999630, + "result": null, + "round_number": 1, + "tick": 730 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 730 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4ddf9498e9171490fe913b1aa4ec5f2a8ff59fd44838c19dc877a6d5e6485b62", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -987, + "facing_y": 164, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 189, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -5, + "velocity_y": 1, + "warnings": 0, + "x": 230, + "y": 110 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 988, + "facing_y": -158, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 60, + "queued_actions": 0, + "stamina": 892, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999629, + "result": null, + "round_number": 1, + "tick": 731 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 731 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d024cbe07611fa3756dd7fcab29a3bf9bbcff9ee2d7dabe25b13e99003238dcc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -987, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 190, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -5, + "velocity_y": 1, + "warnings": 0, + "x": 225, + "y": 111 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 987, + "facing_y": -164, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 61, + "queued_actions": 0, + "stamina": 895, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999628, + "result": null, + "round_number": 1, + "tick": 732 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 732 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "81d99361f8debca9eb2ccfa4a5ff3013120461fbcf25536e230d47e0bfe17da2", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -986, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 191, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -5, + "velocity_y": 1, + "warnings": 0, + "x": 219, + "y": 112 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 987, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 61, + "queued_actions": 0, + "stamina": 898, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999627, + "result": null, + "round_number": 1, + "tick": 733 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 733 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "173095545c76da2548207f8df3dd34241ee7a00edc4933890f503e017457da42", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -993, + "facing_y": 164, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 192, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -5, + "velocity_y": 1, + "warnings": 0, + "x": 213, + "y": 113 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 986, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 62, + "queued_actions": 0, + "stamina": 901, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999626, + "result": null, + "round_number": 1, + "tick": 734 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 734 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1ab3df8c73b4abeadc213df640e2bd448d1c377ff701e94f4239ab9242d05cc6", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -992, + "facing_y": 164, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 193, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -5, + "velocity_y": 1, + "warnings": 0, + "x": 207, + "y": 114 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 993, + "facing_y": -164, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 62, + "queued_actions": 0, + "stamina": 904, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999625, + "result": null, + "round_number": 1, + "tick": 735 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 735 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3bdc73ba31bae3e092d0b14e319b51cadd3d1cc7a864825a2dacfe61103a73aa", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -992, + "facing_y": 164, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 194, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -5, + "velocity_y": 1, + "warnings": 0, + "x": 201, + "y": 115 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 992, + "facing_y": -164, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 63, + "queued_actions": 0, + "stamina": 907, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999624, + "result": null, + "round_number": 1, + "tick": 736 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 736 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "37d0f7310cbdd6aaec8b29e97563eaab89b06d2c53b0566964678947439e4a5e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -992, + "facing_y": 164, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 195, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -5, + "velocity_y": 1, + "warnings": 0, + "x": 195, + "y": 116 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 992, + "facing_y": -164, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 63, + "queued_actions": 0, + "stamina": 910, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999623, + "result": null, + "round_number": 1, + "tick": 737 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 737 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c52d6be0ab84f60923eb99ccdbbb843197fde6560c8b2b1e34ccd880cd1a09e7", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 196, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -5, + "velocity_y": 1, + "warnings": 0, + "x": 189, + "y": 117 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 992, + "facing_y": -164, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 64, + "queued_actions": 0, + "stamina": 913, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999622, + "result": null, + "round_number": 1, + "tick": 738 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 738 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d2c0a6d8f03450d3b0eae017e556808435da954402ea756b1a8f4e4a2985eee5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -991, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 197, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -5, + "velocity_y": 1, + "warnings": 0, + "x": 183, + "y": 118 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 64, + "queued_actions": 0, + "stamina": 916, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999621, + "result": null, + "round_number": 1, + "tick": 739 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 739 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "de4e04594b18ac74bb277d9d0add3417daf4a07addb29c956f266bbb347c3607", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -990, + "facing_y": 163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 198, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -5, + "velocity_y": 1, + "warnings": 0, + "x": 177, + "y": 119 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 991, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 65, + "queued_actions": 0, + "stamina": 919, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999620, + "result": null, + "round_number": 1, + "tick": 740 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 740 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fc12d596307e0929e0ce9daf537a344b92929393ef5d3f323964f2b634444f36", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -990, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 199, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -5, + "velocity_y": 1, + "warnings": 0, + "x": 172, + "y": 120 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 990, + "facing_y": -163, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 65, + "queued_actions": 0, + "stamina": 922, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999619, + "result": null, + "round_number": 1, + "tick": 741 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 741 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "16bd4b479194e56d4e5e316af16bb2fa31e92baeb562c177ae15262dff44ea59", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 200, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -5, + "velocity_y": 1, + "warnings": 0, + "x": 166, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 990, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 66, + "queued_actions": 0, + "stamina": 925, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999618, + "result": null, + "round_number": 1, + "tick": 742 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 742 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "49e9f6024130e634a67c2093a4ad41fbfb26d448b3dd180bb2b828fed649531a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 201, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -5, + "velocity_y": 1, + "warnings": 0, + "x": 160, + "y": 122 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 66, + "queued_actions": 0, + "stamina": 928, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999617, + "result": null, + "round_number": 1, + "tick": 743 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 743 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e426a74d3197afa28feddc2998ef1dbe435c4fa5ab068395525262271237c37f", + "events": [ + { + "action_id": null, + "actor_id": "one", + "amount": 0, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 39, + "kind": "clinch_start", + "target_id": "two", + "tick": 744 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 8, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 961, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 0, + "warnings": 0, + "x": 157, + "y": 123 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": -161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 67, + "queued_actions": 0, + "stamina": 931, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999616, + "result": null, + "round_number": 1, + "tick": 744 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 744 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7a3e665d659db6bc4d4bf20330b139b82db12cd5e6b3193079dbfe86221be64e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 7, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 962, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -1, + "velocity_y": 0, + "warnings": 0, + "x": 155, + "y": 123 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 988, + "facing_y": -155, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 67, + "queued_actions": 0, + "stamina": 934, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999615, + "result": null, + "round_number": 1, + "tick": 745 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 745 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f363035277df97bf314dd7afccb6a5af9baf15d548aa7a8b1fa1206e7e2f2ff8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 6, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 963, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -1, + "velocity_y": 0, + "warnings": 0, + "x": 155, + "y": 123 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 988, + "facing_y": -159, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 68, + "queued_actions": 0, + "stamina": 937, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999614, + "result": null, + "round_number": 1, + "tick": 746 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 746 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2926e7e96f1fad02a7f70a0e06c3104547fa629f3520e529b5e89b38312c17df", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 5, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 964, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 154, + "y": 123 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 988, + "facing_y": -159, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 68, + "queued_actions": 0, + "stamina": 940, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999613, + "result": null, + "round_number": 1, + "tick": 747 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 747 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ae12298365cc920956187ae57151ce78dc0c50ee0cd8c5ef71d201c8f52f0474", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 4, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 965, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 154, + "y": 123 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 988, + "facing_y": -160, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 69, + "queued_actions": 0, + "stamina": 943, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999612, + "result": null, + "round_number": 1, + "tick": 748 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 748 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "213f073b5dc83e618b4e91dc5bcc6002a5bb0d0435c9c34ca3d2895e8d082640", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 3, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 966, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 154, + "y": 123 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 988, + "facing_y": -160, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 69, + "queued_actions": 0, + "stamina": 946, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999611, + "result": null, + "round_number": 1, + "tick": 749 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 749 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "cecb532da859eb198a0c9a3f957f56a336c96ef15e5677b3446d7a420916437e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 2, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 967, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 154, + "y": 123 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 988, + "facing_y": -160, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 70, + "queued_actions": 0, + "stamina": 949, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999610, + "result": null, + "round_number": 1, + "tick": 750 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 750 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d211983e0eecd7ada0114236130a6dacd03948efd24ee22a386acc18d0d02bda", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 1, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 968, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 154, + "y": 123 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 988, + "facing_y": -160, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 70, + "queued_actions": 0, + "stamina": 952, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999609, + "result": null, + "round_number": 1, + "tick": 751 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 751 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2ce1320e5f6cdafd6aac66e2a059d6d5c739dd8505cc4fe9e6e291b6dba4f478", + "events": [ + { + "action_id": null, + "actor_id": "one", + "amount": 0, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 40, + "kind": "clinch", + "target_id": "two", + "tick": 752 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 45, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": 161, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 949, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 154, + "y": 123 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 45, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 988, + "facing_y": -160, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 70, + "queued_actions": 0, + "stamina": 933, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 68, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999608, + "result": null, + "round_number": 1, + "tick": 752 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 752 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3257e5f9981fb9a098554cbdf07b1a00d313a79f172fec096d583070ff854f95", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 44, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -988, + "facing_y": 160, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 951, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 149, + "y": 123 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 44, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 988, + "facing_y": -160, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 70, + "queued_actions": 0, + "stamina": 935, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 73, + "y": 137 + } + ], + "phase": "fight", + "phase_ticks_remaining": 9, + "result": null, + "round_number": 1, + "tick": 753 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 753 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "175b8f08b46e9f71a3508ee9cf65f27fae92f992f384a2ca3517f803f6fc7b6c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 43, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -987, + "facing_y": 181, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 953, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 144, + "y": 124 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 43, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 987, + "facing_y": -181, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 70, + "queued_actions": 0, + "stamina": 937, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 78, + "y": 136 + } + ], + "phase": "fight", + "phase_ticks_remaining": 8, + "result": null, + "round_number": 1, + "tick": 754 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 754 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "cf152c29d01d2a588ec4943d196bc00865b884a51b24828bd52d18b4ee108e86", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 42, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -985, + "facing_y": 179, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 955, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 142, + "y": 124 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 42, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 985, + "facing_y": -179, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 70, + "queued_actions": 0, + "stamina": 939, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 80, + "y": 136 + } + ], + "phase": "fight", + "phase_ticks_remaining": 7, + "result": null, + "round_number": 1, + "tick": 755 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 755 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1c4003562cbb1bac248e03d11f584575b3b4a4e9acb54b08847f98603302859a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 41, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -984, + "facing_y": 190, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 957, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 142, + "y": 124 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 41, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 984, + "facing_y": -190, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 70, + "queued_actions": 0, + "stamina": 941, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 80, + "y": 136 + } + ], + "phase": "fight", + "phase_ticks_remaining": 6, + "result": null, + "round_number": 1, + "tick": 756 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 756 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1482c40d2959acc05b5641348d3b829a976beb0ce78922ff6937d7bb7913dd79", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 40, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -984, + "facing_y": 190, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 959, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 142, + "y": 124 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 40, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 984, + "facing_y": -190, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 70, + "queued_actions": 0, + "stamina": 943, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 80, + "y": 136 + } + ], + "phase": "fight", + "phase_ticks_remaining": 5, + "result": null, + "round_number": 1, + "tick": 757 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 757 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b10d2d172cfd7dd6b071612e20f391795b2313d828fb14a2ce088c0276ad4faa", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 39, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -984, + "facing_y": 190, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 961, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 142, + "y": 124 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 39, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 984, + "facing_y": -190, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 70, + "queued_actions": 0, + "stamina": 945, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 80, + "y": 136 + } + ], + "phase": "fight", + "phase_ticks_remaining": 4, + "result": null, + "round_number": 1, + "tick": 758 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 758 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0d763c70447eb26dd02f0402329c762125bd90bbb4c085141b66d7ed042e1af7", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 38, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -984, + "facing_y": 190, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 985, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 963, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 142, + "y": 124 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 38, + "conditioning": 966, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 984, + "facing_y": -190, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 70, + "queued_actions": 0, + "stamina": 947, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 80, + "y": 136 + } + ], + "phase": "fight", + "phase_ticks_remaining": 3, + "result": null, + "round_number": 1, + "tick": 759 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 759 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5eb54f9b82b09a65813b36ae2230ce70f734dbd7983f976459becd8c485cd68d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 37, + "conditioning": 964, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -984, + "facing_y": 190, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 965, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 142, + "y": 124 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 37, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 984, + "facing_y": -190, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 70, + "queued_actions": 0, + "stamina": 949, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 80, + "y": 136 + } + ], + "phase": "fight", + "phase_ticks_remaining": 2, + "result": null, + "round_number": 1, + "tick": 760 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 760 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "99b7870ffb3828547f2d96328cd17c5dba2999eccedb86a63fc8bd66272c8e46", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 36, + "conditioning": 964, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -984, + "facing_y": 190, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 967, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 142, + "y": 124 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 36, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 984, + "facing_y": -190, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 70, + "queued_actions": 0, + "stamina": 951, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 80, + "y": 136 + } + ], + "phase": "fight", + "phase_ticks_remaining": 1, + "result": null, + "round_number": 1, + "tick": 761 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 761 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "97fc5f66b1843617bc07dc847696cea95d99a3f2779b7f17f51aa4ea0d666b89", + "events": [ + { + "action_id": null, + "actor_id": null, + "amount": 0, + "blood": 0, + "detail": "round_end", + "direction": 0, + "event_id": 41, + "kind": "bell", + "target_id": null, + "tick": 762 + }, + { + "action_id": null, + "actor_id": "one", + "amount": 0, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 42, + "kind": "referee_break", + "target_id": "two", + "tick": 762 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -984, + "facing_y": 190, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 969, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": 186, + "y": 116 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 984, + "facing_y": -190, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 70, + "queued_actions": 0, + "stamina": 953, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 36, + "y": 144 + } + ], + "phase": "rest", + "phase_ticks_remaining": 240, + "result": null, + "round_number": 1, + "tick": 762 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 762 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8931200f7e7b5547167a31ffd157f344dafa679efe6229c0cbad9c55dca19cbc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 202, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 972, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 183, + "y": 113 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": null, + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 830, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 72, + "queued_actions": 0, + "stamina": 956, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 185, + "right_cut": 0, + "right_eye": 23, + "swelling": 90 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 40, + "y": 146 + } + ], + "phase": "rest", + "phase_ticks_remaining": 239, + "result": null, + "round_number": 1, + "tick": 763 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 763 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "89dbb6237d9bb2d3d69bb8711f187957492570eeb578ff6422db4a98c8125a07", + "events": [ + { + "action_id": null, + "actor_id": "one", + "amount": 0, + "blood": 0, + "detail": "cut", + "direction": 0, + "event_id": 43, + "kind": "corner", + "target_id": null, + "tick": 764 + }, + { + "action_id": null, + "actor_id": "two", + "amount": 0, + "blood": 0, + "detail": "swelling", + "direction": 0, + "event_id": 44, + "kind": "corner", + "target_id": null, + "tick": 764 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 975, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 179, + "y": 110 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 560, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 74, + "queued_actions": 0, + "stamina": 959, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 44, + "y": 149 + } + ], + "phase": "rest", + "phase_ticks_remaining": 238, + "result": null, + "round_number": 1, + "tick": 764 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 764 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fcff0b9fb985bf79c0bf995c271e50af8e5db8d6cd538dbebc356d9321ad5c2b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 978, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 175, + "y": 107 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 559, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 76, + "queued_actions": 0, + "stamina": 962, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 48, + "y": 152 + } + ], + "phase": "rest", + "phase_ticks_remaining": 237, + "result": null, + "round_number": 1, + "tick": 765 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 765 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "eaeda4854c12d1d1c3cf64e2e9230ff0a31be61c07b34025c80045253a11eaf1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 981, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 171, + "y": 103 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 832, + "facing_y": 559, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 78, + "queued_actions": 0, + "stamina": 965, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 52, + "y": 155 + } + ], + "phase": "rest", + "phase_ticks_remaining": 236, + "result": null, + "round_number": 1, + "tick": 766 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 766 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e9fd320a0748c46ed5acb1bb4f2c32848495db7cc3173180d46d6dfb7706404e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 168, + "y": 100 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 832, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 80, + "queued_actions": 0, + "stamina": 968, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 56, + "y": 157 + } + ], + "phase": "rest", + "phase_ticks_remaining": 235, + "result": null, + "round_number": 1, + "tick": 767 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 767 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a265e45f2ce0fa00b2f9d8764a264127dc0c2faccdd8b6627b333b97fd2ec59d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 164, + "y": 97 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 830, + "facing_y": 559, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 82, + "queued_actions": 0, + "stamina": 971, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 60, + "y": 160 + } + ], + "phase": "rest", + "phase_ticks_remaining": 234, + "result": null, + "round_number": 1, + "tick": 768 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 768 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e108efd4d0c9003e96a8a5a3373ae663775cf3b9e21389153c0ebde4d8d68d2a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 160, + "y": 93 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 84, + "queued_actions": 0, + "stamina": 974, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 65, + "y": 163 + } + ], + "phase": "rest", + "phase_ticks_remaining": 233, + "result": null, + "round_number": 1, + "tick": 769 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 769 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f0749c56b83641efef7b21009d070707d2ce96cd2ff0064b1c52e880ff09e296", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 156, + "y": 90 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 559, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 86, + "queued_actions": 0, + "stamina": 977, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 69, + "y": 166 + } + ], + "phase": "rest", + "phase_ticks_remaining": 232, + "result": null, + "round_number": 1, + "tick": 770 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 770 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bc774cca7795e43ea8c467290421d54a686a13f790a2abadc2f295ac267f3c90", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 153, + "y": 87 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 88, + "queued_actions": 0, + "stamina": 980, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 73, + "y": 169 + } + ], + "phase": "rest", + "phase_ticks_remaining": 231, + "result": null, + "round_number": 1, + "tick": 771 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 771 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b197e804f75b6ef7b09e6e2904e1112c15c49c7d701f3e5bb60162e5f558e813", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 149, + "y": 84 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 90, + "queued_actions": 0, + "stamina": 983, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 77, + "y": 171 + } + ], + "phase": "rest", + "phase_ticks_remaining": 230, + "result": null, + "round_number": 1, + "tick": 772 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 772 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8b207349e3fd65b65e8731dfcef19f789f228659683b7edb30f1ad98f1f75196", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 145, + "y": 80 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 829, + "facing_y": 559, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 92, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 81, + "y": 174 + } + ], + "phase": "rest", + "phase_ticks_remaining": 229, + "result": null, + "round_number": 1, + "tick": 773 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 773 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "eef6585cc5fea82cc28d979b07a0d3ab708b95778e8209171bd7f7f0d9dead49", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 141, + "y": 77 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 830, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 94, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 85, + "y": 177 + } + ], + "phase": "rest", + "phase_ticks_remaining": 228, + "result": null, + "round_number": 1, + "tick": 774 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 774 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f0cc6dd26251fba7fa4e75b495f5cf92fd601801cb426ada19d31c8d33176974", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 138, + "y": 74 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 830, + "facing_y": 557, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 96, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 89, + "y": 180 + } + ], + "phase": "rest", + "phase_ticks_remaining": 227, + "result": null, + "round_number": 1, + "tick": 775 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 775 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4351727b5fb4dbb6d34808977848b4c24048e2985d1b74fea3a02b70d158900a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 134, + "y": 70 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 557, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 98, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 94, + "y": 183 + } + ], + "phase": "rest", + "phase_ticks_remaining": 226, + "result": null, + "round_number": 1, + "tick": 776 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 776 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9e61476e190bfbb8933278095a011e5f7b468a510325337d6a1ec456ae46c4cb", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 130, + "y": 67 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 100, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 98, + "y": 185 + } + ], + "phase": "rest", + "phase_ticks_remaining": 225, + "result": null, + "round_number": 1, + "tick": 777 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 777 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "28114d02de6ba39b2b47dc3018af1289a4686824da7709e311ea651e5fadf779", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 126, + "y": 64 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 829, + "facing_y": 559, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 102, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 102, + "y": 188 + } + ], + "phase": "rest", + "phase_ticks_remaining": 224, + "result": null, + "round_number": 1, + "tick": 778 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 778 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2ceca3b7ab300df02680ace0d51fe2e28ca8fc7bea9747db56b222ceb0b7cd08", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 122, + "y": 61 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 829, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 104, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 106, + "y": 191 + } + ], + "phase": "rest", + "phase_ticks_remaining": 223, + "result": null, + "round_number": 1, + "tick": 779 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 779 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "332c3b2445110c856fffcd720d5fdd40c4ad33d7cecbc0785cdcc480bc858bc8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -752, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 119, + "y": 57 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 830, + "facing_y": 557, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 106, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 110, + "y": 194 + } + ], + "phase": "rest", + "phase_ticks_remaining": 222, + "result": null, + "round_number": 1, + "tick": 780 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 780 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c05e54ccb74df903587feed202f152799775e6315a0af93cdbf1e0f05e0492b9", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 115, + "y": 54 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 830, + "facing_y": 557, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 108, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 114, + "y": 197 + } + ], + "phase": "rest", + "phase_ticks_remaining": 221, + "result": null, + "round_number": 1, + "tick": 781 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 781 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d1b28fdfede17d6745676cbd67f68515141d1983864bff5de64b624e0cb85766", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 111, + "y": 51 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 556, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 110, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 119, + "y": 199 + } + ], + "phase": "rest", + "phase_ticks_remaining": 220, + "result": null, + "round_number": 1, + "tick": 782 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 782 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "cb455012e2fa5b47cfb9df986eeeaff69f943fa62a6e3dbe6a544b0f14c28859", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 107, + "y": 47 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 560, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 112, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 123, + "y": 202 + } + ], + "phase": "rest", + "phase_ticks_remaining": 219, + "result": null, + "round_number": 1, + "tick": 783 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 783 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f3070ceb9b371d95d3600e292e5521ae1056c5ef40d7431bedb3182b583537a6", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 104, + "y": 44 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 560, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 114, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 127, + "y": 205 + } + ], + "phase": "rest", + "phase_ticks_remaining": 218, + "result": null, + "round_number": 1, + "tick": 784 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 784 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "578d6e96afc54698a308e9c46938deb5d03793c61772d2c384c153f6a8b7ab4b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 100, + "y": 41 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 832, + "facing_y": 559, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 116, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 131, + "y": 208 + } + ], + "phase": "rest", + "phase_ticks_remaining": 217, + "result": null, + "round_number": 1, + "tick": 785 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 785 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3c2681d879b797dfcd5ee567fc4665b46052225df34141101078b9b6c9eb2a54", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 96, + "y": 38 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 832, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 118, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 135, + "y": 210 + } + ], + "phase": "rest", + "phase_ticks_remaining": 216, + "result": null, + "round_number": 1, + "tick": 786 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 786 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "214cd35cefe25eb022d1c66e325182467bcc1b462a6d9c549f31272f621db3c5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 92, + "y": 34 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 830, + "facing_y": 559, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 120, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 139, + "y": 213 + } + ], + "phase": "rest", + "phase_ticks_remaining": 215, + "result": null, + "round_number": 1, + "tick": 787 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 787 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a7c6f9ac12b79106407c677a69368ebd9d9d45623ab4a6177bfacd57546eb151", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 89, + "y": 31 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 830, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 122, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 143, + "y": 216 + } + ], + "phase": "rest", + "phase_ticks_remaining": 214, + "result": null, + "round_number": 1, + "tick": 788 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 788 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "94ea7272504c5b509de6725d001c3f1b46df36b2e61b6c9f91fdc51dd5e21a3b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 85, + "y": 28 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 124, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 148, + "y": 219 + } + ], + "phase": "rest", + "phase_ticks_remaining": 213, + "result": null, + "round_number": 1, + "tick": 789 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 789 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d1786c83005ff25dd313711d31983efef687a0ef65f66eca27bb0c092439f767", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 81, + "y": 24 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 559, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 126, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 152, + "y": 222 + } + ], + "phase": "rest", + "phase_ticks_remaining": 212, + "result": null, + "round_number": 1, + "tick": 790 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 790 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bf4ae660be1f2564f8fd3561115932d8af0b378d65fd50bc80132984f3bc1483", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 77, + "y": 21 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 832, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 128, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 156, + "y": 224 + } + ], + "phase": "rest", + "phase_ticks_remaining": 211, + "result": null, + "round_number": 1, + "tick": 791 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 791 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8f9602917c2b26961223c605438a8e625b9b3f2b8808b8c341339b51b416c7e1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 73, + "y": 18 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 829, + "facing_y": 559, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 130, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 160, + "y": 227 + } + ], + "phase": "rest", + "phase_ticks_remaining": 210, + "result": null, + "round_number": 1, + "tick": 792 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 792 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5d16841738fcf7cb023f7812d8c079f466b0fe51e4cabc4184f10e7aa75e78da", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 70, + "y": 15 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 829, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 132, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 164, + "y": 230 + } + ], + "phase": "rest", + "phase_ticks_remaining": 209, + "result": null, + "round_number": 1, + "tick": 793 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 793 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b59fa59fc58e582f883e92879568cce961e162681d23fff4cdcdb869abc3aece", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 66, + "y": 11 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 830, + "facing_y": 557, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 134, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 168, + "y": 233 + } + ], + "phase": "rest", + "phase_ticks_remaining": 208, + "result": null, + "round_number": 1, + "tick": 794 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 794 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e694d947241144889f0646eea0ed86d03fa6c3713c70f8ccbbcc6e75c0261c38", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 62, + "y": 8 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 556, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 136, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 173, + "y": 236 + } + ], + "phase": "rest", + "phase_ticks_remaining": 207, + "result": null, + "round_number": 1, + "tick": 795 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 795 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1c0f56792469e5fc1ea5515c026594252f6aebd1a9c305639c4f4917a9d00e77", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 58, + "y": 5 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 138, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 177, + "y": 238 + } + ], + "phase": "rest", + "phase_ticks_remaining": 206, + "result": null, + "round_number": 1, + "tick": 796 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 796 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2729ed728329feb14a3baf8d477c911f6a2c2f8bbb477767a3d9317ae9147607", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 55, + "y": 1 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 561, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 140, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 181, + "y": 241 + } + ], + "phase": "rest", + "phase_ticks_remaining": 205, + "result": null, + "round_number": 1, + "tick": 797 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 797 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7bf98805df5fc3fd7edace15befc9589ef0da2547f38cc4f44a00df774ff6652", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 51, + "y": -2 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 832, + "facing_y": 561, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 142, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 185, + "y": 244 + } + ], + "phase": "rest", + "phase_ticks_remaining": 204, + "result": null, + "round_number": 1, + "tick": 798 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 798 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5cf3b81d04915053e00a001d67a2a0bfb05c7fe785f071f72b98167dbe0ef60b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 47, + "y": -5 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 833, + "facing_y": 560, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 144, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 189, + "y": 247 + } + ], + "phase": "rest", + "phase_ticks_remaining": 203, + "result": null, + "round_number": 1, + "tick": 799 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 799 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "50aec01b94cd53ded76b2f0114911dffba8e0f252f723a942e9b19c84cbdc6e4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 43, + "y": -8 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 834, + "facing_y": 559, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 146, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 193, + "y": 250 + } + ], + "phase": "rest", + "phase_ticks_remaining": 202, + "result": null, + "round_number": 1, + "tick": 800 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 800 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e50261a187d7a9f372233450262104b369c9999fadb06528b6b31c142870fad9", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -752, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 40, + "y": -12 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 834, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 148, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 198, + "y": 252 + } + ], + "phase": "rest", + "phase_ticks_remaining": 201, + "result": null, + "round_number": 1, + "tick": 801 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 801 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0a9811e56eb5a7288e088d22eb15d431c4448f0e82f122014f688dad9a291640", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 36, + "y": -15 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 830, + "facing_y": 562, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 150, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 202, + "y": 255 + } + ], + "phase": "rest", + "phase_ticks_remaining": 200, + "result": null, + "round_number": 1, + "tick": 802 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 802 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4c64014beaff7b8f80af2e6bdbef885373b02e26cdfb1bc82749f23dd5c0474b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 32, + "y": -18 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 561, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 152, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 206, + "y": 258 + } + ], + "phase": "rest", + "phase_ticks_remaining": 199, + "result": null, + "round_number": 1, + "tick": 803 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 803 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ac4332afaa0ea5ffcd01075e06f5731cb860171a4e99fc76f7e1a6e575aef1cc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 28, + "y": -22 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 832, + "facing_y": 560, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 154, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 210, + "y": 261 + } + ], + "phase": "rest", + "phase_ticks_remaining": 198, + "result": null, + "round_number": 1, + "tick": 804 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 804 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d98d2b4977302b1cc51dc85bd219ca62f9c2c1eae9a73d7854f9c59145f0c2dd", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 25, + "y": -25 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 833, + "facing_y": 559, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 156, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 214, + "y": 264 + } + ], + "phase": "rest", + "phase_ticks_remaining": 197, + "result": null, + "round_number": 1, + "tick": 805 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 805 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d34560cd0a6c06b5eb4ce20634aabb21f348a6708c44c352a467dff14b07d5ea", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 21, + "y": -28 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 834, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 158, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 218, + "y": 266 + } + ], + "phase": "rest", + "phase_ticks_remaining": 196, + "result": null, + "round_number": 1, + "tick": 806 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 806 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "634dffac5ac16b7cf5b49c76e8946527de3d5add113c809e608623991c2bfd95", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 17, + "y": -31 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 830, + "facing_y": 559, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 160, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 222, + "y": 269 + } + ], + "phase": "rest", + "phase_ticks_remaining": 195, + "result": null, + "round_number": 1, + "tick": 807 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 807 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d2481627a7f25cfe09e9470d65dd7b8d24a37e15225866b041a622738ec0a11d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 13, + "y": -35 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 162, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 227, + "y": 272 + } + ], + "phase": "rest", + "phase_ticks_remaining": 194, + "result": null, + "round_number": 1, + "tick": 808 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 808 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ff312b538fa47e4ff4aed0f15f42f9d70fad6e3ba2b58418bc4d84b3303e6ea7", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 9, + "y": -38 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 560, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 164, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 231, + "y": 275 + } + ], + "phase": "rest", + "phase_ticks_remaining": 193, + "result": null, + "round_number": 1, + "tick": 809 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 809 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d1bc8790881515736616d4447ffefec50819ce170a04215f8e6ff71e2074f8d8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 6, + "y": -41 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 832, + "facing_y": 559, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 166, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 235, + "y": 278 + } + ], + "phase": "rest", + "phase_ticks_remaining": 192, + "result": null, + "round_number": 1, + "tick": 810 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 810 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d767fb6b0fd8a54000d5213a7cdaaf7417bf1df8989de5adbbe455dde0672020", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": 2, + "y": -45 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 833, + "facing_y": 557, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 168, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 239, + "y": 280 + } + ], + "phase": "rest", + "phase_ticks_remaining": 191, + "result": null, + "round_number": 1, + "tick": 811 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 811 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b4439a865c37566154d26551948c500f68db85d2c49b2016ff2a3bc14a2d1993", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -755, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -2, + "y": -48 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 834, + "facing_y": 562, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 170, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 243, + "y": 283 + } + ], + "phase": "rest", + "phase_ticks_remaining": 190, + "result": null, + "round_number": 1, + "tick": 812 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 812 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4f33165fc493955e06d3acc8bba52a141fa888c5d9063e08c82e063c9e0fd78f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -6, + "y": -51 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 835, + "facing_y": 561, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 172, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 247, + "y": 286 + } + ], + "phase": "rest", + "phase_ticks_remaining": 189, + "result": null, + "round_number": 1, + "tick": 813 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 813 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "abe781fb1f468eaabae73e444e5e79c664f56273775ffae7c3f280c51f51e6b1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -9, + "y": -55 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 830, + "facing_y": 556, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 174, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 252, + "y": 289 + } + ], + "phase": "rest", + "phase_ticks_remaining": 188, + "result": null, + "round_number": 1, + "tick": 814 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 814 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f60a62b94def24d75d09c350017dac6f63167d600d6b3faad16801e321400fcc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -656, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -13, + "y": -58 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 830, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 176, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 256, + "y": 292 + } + ], + "phase": "rest", + "phase_ticks_remaining": 187, + "result": null, + "round_number": 1, + "tick": 815 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 815 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fe957f8be7f00277c54f403406d4066517d2e573e4d980d4a37b47f9642df282", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -17, + "y": -61 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 832, + "facing_y": 557, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 178, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 260, + "y": 294 + } + ], + "phase": "rest", + "phase_ticks_remaining": 186, + "result": null, + "round_number": 1, + "tick": 816 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 816 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f5c5270e3016d8a91fce69814f48be406398b8c8ef1f2aa0e1e185b42b8f1103", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -21, + "y": -64 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 833, + "facing_y": 563, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 180, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 264, + "y": 297 + } + ], + "phase": "rest", + "phase_ticks_remaining": 185, + "result": null, + "round_number": 1, + "tick": 817 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 817 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f949ad8a35e7b7f63e94162b2ab31be2b0ff30af4f179207121bb5467758dd98", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -752, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -24, + "y": -68 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 834, + "facing_y": 561, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 182, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 268, + "y": 300 + } + ], + "phase": "rest", + "phase_ticks_remaining": 184, + "result": null, + "round_number": 1, + "tick": 818 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 818 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6d5cceea5c888de4325ca2152e696b604f13d69ca3706bbd56aa1c4d083ace87", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -28, + "y": -71 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 836, + "facing_y": 560, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 184, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 272, + "y": 303 + } + ], + "phase": "rest", + "phase_ticks_remaining": 183, + "result": null, + "round_number": 1, + "tick": 819 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 819 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b77c6d7075ca4cf5d24289487f28a11182329e843536cb9f5bd2bbdc6e245709", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -32, + "y": -74 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 837, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 186, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 277, + "y": 306 + } + ], + "phase": "rest", + "phase_ticks_remaining": 182, + "result": null, + "round_number": 1, + "tick": 820 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 820 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "94894911fef0b959d0d4360edf704016df52f9256f7095bdba4da49956ae84f2", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -36, + "y": -78 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 838, + "facing_y": 561, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 188, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 281, + "y": 308 + } + ], + "phase": "rest", + "phase_ticks_remaining": 181, + "result": null, + "round_number": 1, + "tick": 821 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 821 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "de97b43fdd15c58f43ef33072a3e6b38254750548e05a5bdd3418456361e9438", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -40, + "y": -81 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": 564, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 190, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 285, + "y": 311 + } + ], + "phase": "rest", + "phase_ticks_remaining": 180, + "result": null, + "round_number": 1, + "tick": 822 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 822 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "16a1499c84f80b3669c8d266bb51795682b60dcaeb02e1c9950306bd2c149410", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -43, + "y": -84 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 833, + "facing_y": 562, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 192, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 289, + "y": 314 + } + ], + "phase": "rest", + "phase_ticks_remaining": 179, + "result": null, + "round_number": 1, + "tick": 823 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 823 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "49f2fafbd28ce13a02f3a7095068d50db62f3d1b06981f1f5426ffa35acbe8fe", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -47, + "y": -87 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 835, + "facing_y": 560, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 194, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 293, + "y": 317 + } + ], + "phase": "rest", + "phase_ticks_remaining": 178, + "result": null, + "round_number": 1, + "tick": 824 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 824 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "971d965ee8836fcd49da1fe88ad04e2db979640be57e67a3527dece37a240c99", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -51, + "y": -91 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 837, + "facing_y": 558, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 196, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 297, + "y": 320 + } + ], + "phase": "rest", + "phase_ticks_remaining": 177, + "result": null, + "round_number": 1, + "tick": 825 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 825 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "083b475bbd725810fc13740ff53532efad8d63b416bc5d1fc116976d56c61cd0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -55, + "y": -94 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 839, + "facing_y": 555, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 198, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 302, + "y": 322 + } + ], + "phase": "rest", + "phase_ticks_remaining": 176, + "result": null, + "round_number": 1, + "tick": 826 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 826 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "03c69d8d1adf728f2334342b757f6055c6704373bb5af33fde7d1e99dd04e376", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -58, + "y": -97 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 828, + "facing_y": 565, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 200, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 306, + "y": 325 + } + ], + "phase": "rest", + "phase_ticks_remaining": 175, + "result": null, + "round_number": 1, + "tick": 827 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 827 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e4afa61546f7d3011271d79a8bc0e75b670659b6b2671e6153139f967e02c0c2", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -62, + "y": -101 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 830, + "facing_y": 563, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 202, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 310, + "y": 328 + } + ], + "phase": "rest", + "phase_ticks_remaining": 174, + "result": null, + "round_number": 1, + "tick": 828 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 828 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3df41929b65c5d7b5a80c2d2c0129513b683408bb618738ecba3a55a50098208", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -755, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -66, + "y": -104 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 833, + "facing_y": 560, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 204, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 314, + "y": 331 + } + ], + "phase": "rest", + "phase_ticks_remaining": 173, + "result": null, + "round_number": 1, + "tick": 829 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 829 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bea1621028bceb7a3450af911e334f51b8b4e2539475a02d315ed4e14329295a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -755, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -70, + "y": -107 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 836, + "facing_y": 557, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 206, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 318, + "y": 334 + } + ], + "phase": "rest", + "phase_ticks_remaining": 172, + "result": null, + "round_number": 1, + "tick": 830 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 830 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "11170ba78e8ec986079147a7e8c8dbf7066d3ef51253039172ace9767b1c2f05", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -73, + "y": -110 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 839, + "facing_y": 553, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 208, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 323, + "y": 336 + } + ], + "phase": "rest", + "phase_ticks_remaining": 171, + "result": null, + "round_number": 1, + "tick": 831 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 831 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1aafb0cd7dab2ca931ccdec470b26722a178534c182db172b49e77c1852d9192", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -77, + "y": -114 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 823, + "facing_y": 568, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 210, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 327, + "y": 339 + } + ], + "phase": "rest", + "phase_ticks_remaining": 170, + "result": null, + "round_number": 1, + "tick": 832 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 832 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "05eff6054eb34a5f9aec2e47f03e090e0943a72b0ad5dd811d7b60f1bb70f6fe", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -81, + "y": -117 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 826, + "facing_y": 565, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 212, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 331, + "y": 342 + } + ], + "phase": "rest", + "phase_ticks_remaining": 169, + "result": null, + "round_number": 1, + "tick": 833 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 833 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2cb40799c38d11caf5a2ef6482dfb831495b5367fcff3485cc1ebc154a9724f1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -85, + "y": -120 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 829, + "facing_y": 560, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 214, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 335, + "y": 345 + } + ], + "phase": "rest", + "phase_ticks_remaining": 168, + "result": null, + "round_number": 1, + "tick": 834 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 834 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3c53367f63ed22ae6732b8508e0b2687b38424a09197dc51fa81d1e3230d23c1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -752, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -89, + "y": -124 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 833, + "facing_y": 555, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 216, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 339, + "y": 348 + } + ], + "phase": "rest", + "phase_ticks_remaining": 167, + "result": null, + "round_number": 1, + "tick": 835 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 835 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5bce7cf2d558ddea532ec5675a64fa1e59f54cf8c630661c3c2d16c5134a043d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -92, + "y": -127 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 838, + "facing_y": 548, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 218, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 343, + "y": 350 + } + ], + "phase": "rest", + "phase_ticks_remaining": 166, + "result": null, + "round_number": 1, + "tick": 836 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 836 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5419b42d43424809af35323bea61a33cd22cba4e53c925426b52f53add08ba64", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -96, + "y": -130 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 846, + "facing_y": 576, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 220, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 347, + "y": 353 + } + ], + "phase": "rest", + "phase_ticks_remaining": 165, + "result": null, + "round_number": 1, + "tick": 837 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 837 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8b2558c5aab943b3e851f7ca1b88f430f4063d9d36f51105ccf74f41eecc36fc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -100, + "y": -133 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 857, + "facing_y": 571, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 222, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 352, + "y": 356 + } + ], + "phase": "rest", + "phase_ticks_remaining": 164, + "result": null, + "round_number": 1, + "tick": 838 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 838 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3ea50385b747ea77f023bc452f2b47c62145e517b90f3a57a745c44439f6d8d4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -752, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -104, + "y": -137 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 866, + "facing_y": 600, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 224, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 3, + "warnings": 1, + "x": 356, + "y": 359 + } + ], + "phase": "rest", + "phase_ticks_remaining": 163, + "result": null, + "round_number": 1, + "tick": 839 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 839 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "94209e117bdd6c787fd70111a1906767707e19c28f1cb6c5f6d6bcdc6bc45831", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -107, + "y": -140 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 900, + "facing_y": 600, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 226, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 5, + "velocity_y": 3, + "warnings": 1, + "x": 361, + "y": 362 + } + ], + "phase": "rest", + "phase_ticks_remaining": 162, + "result": null, + "round_number": 1, + "tick": 840 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 840 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b771803cc89936ea9e769762dad7972e596847bde93d48ed04e4a23e91d29f8b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -111, + "y": -143 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 855, + "facing_y": 518, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 228, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 161, + "result": null, + "round_number": 1, + "tick": 841 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 841 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "195d4678198d54e9f04a179d35066ea1369b3426c4edfbeaffde106bb1e213c5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -115, + "y": -147 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 877, + "facing_y": 480, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 230, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 160, + "result": null, + "round_number": 1, + "tick": 842 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 842 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "53a5372f2cdfc8e7f7c914ca347cfe5838680232215fad49229031f036b8db02", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -755, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -119, + "y": -150 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 897, + "facing_y": 441, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 232, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 159, + "result": null, + "round_number": 1, + "tick": 843 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 843 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "76414eeac1eeb13beef43b0fff623576d0f6b31edc456662bf956900c8642187", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -122, + "y": -153 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 916, + "facing_y": 402, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 234, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 158, + "result": null, + "round_number": 1, + "tick": 844 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 844 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8acae6b2fd657f0a74c0752dcb8303acb4b804ebf9594790de3fb615b5724503", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -126, + "y": -157 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 932, + "facing_y": 362, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 236, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 157, + "result": null, + "round_number": 1, + "tick": 845 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 845 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "24fe4d242d5ca91a8429b5666b6172ba770c1a215f78fdeb14430982f4a5271d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -756, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -130, + "y": -160 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 947, + "facing_y": 321, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 238, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 156, + "result": null, + "round_number": 1, + "tick": 846 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 846 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c764b03d3814bfca0c81272b3cfb1d3aebcfd83c673a9aea279bd6414ac85d30", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -755, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -134, + "y": -163 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 960, + "facing_y": 279, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 240, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 155, + "result": null, + "round_number": 1, + "tick": 847 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 847 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d69ef13d8ecadbce55691aeb4df615a9d69ee1dd6de6b5b4e74a8a1aef57ff8a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -660, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -138, + "y": -166 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 972, + "facing_y": 237, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 242, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 154, + "result": null, + "round_number": 1, + "tick": 848 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 848 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "09d1f3a43031ffff96c1d9012d65b362ddb738a6e497c086aa29f2e075adcd5a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -661, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -141, + "y": -170 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 981, + "facing_y": 194, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 244, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 153, + "result": null, + "round_number": 1, + "tick": 849 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 849 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e0ea72e98a063728d57e51e856aac2cfc0ad0f89267d0164e2ed3a062be47ee3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -756, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -145, + "y": -173 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 989, + "facing_y": 151, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 246, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 152, + "result": null, + "round_number": 1, + "tick": 850 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 850 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7a7c01e4b6b6fc87e1bdf0160c42a81a30bbb7a14ed9c5256c75020ebee035ca", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -149, + "y": -176 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 994, + "facing_y": 108, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 248, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 151, + "result": null, + "round_number": 1, + "tick": 851 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 851 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a394f477117aa2d307dc44296901489b6b4d7791c6d4004f599e93786f999b4e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -752, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -153, + "y": -180 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 998, + "facing_y": 65, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 250, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 150, + "result": null, + "round_number": 1, + "tick": 852 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 852 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "33ca9c7d09e2a2700cc291b7575fade4dc241d08be6d42335bd20a15ec559555", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -156, + "y": -183 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": 21, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 252, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 149, + "result": null, + "round_number": 1, + "tick": 853 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 853 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ef54951dfdf4fe35f5c279ee115172ef491804d61512ea9f5702f814c8da24d4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -160, + "y": -186 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -23, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 254, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 148, + "result": null, + "round_number": 1, + "tick": 854 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 854 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0ff572fcbd49d38311f55a6ba37c223031f18dfee3b95e40891eaac3e77cb43f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -164, + "y": -189 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 998, + "facing_y": -67, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 256, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 147, + "result": null, + "round_number": 1, + "tick": 855 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 855 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ae99a77a2c3a27662a510fb9459d2234d8283fe98cfec717947f77f79ee7559e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -752, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -168, + "y": -193 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 994, + "facing_y": -110, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 258, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 146, + "result": null, + "round_number": 1, + "tick": 856 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 856 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "893946b2229fcecf5adf8d2d3a1c6cccac5de263bcebf17ddda46c984180b8b1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -171, + "y": -196 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 988, + "facing_y": -153, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 260, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 145, + "result": null, + "round_number": 1, + "tick": 857 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 857 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3fd88b5e3c37f0128d641e096fbf94c1c7177fb01cbc2aa26fa4ee35630eef34", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -175, + "y": -199 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 981, + "facing_y": -196, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 262, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 144, + "result": null, + "round_number": 1, + "tick": 858 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 858 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "479fa808c4b3a996be2fb475b6280867a7e9317fbbdfaa6f6c7c50fc8cd867e0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -179, + "y": -203 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 971, + "facing_y": -239, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 264, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 143, + "result": null, + "round_number": 1, + "tick": 859 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 859 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2bf56f14f9465c3cdfc722ce48054fed7f46a914b0b2dc5823933b03ff0442e8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -756, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -183, + "y": -206 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 960, + "facing_y": -281, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 266, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 142, + "result": null, + "round_number": 1, + "tick": 860 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 860 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "877fc89b82bd061fd285672fdf223f85a3e4cfd0d072ff0c56afb17b5105942b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -755, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -187, + "y": -209 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 947, + "facing_y": -323, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 268, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 141, + "result": null, + "round_number": 1, + "tick": 861 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 861 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f8d18d6f8f333fcb97b0b06f379d34a8ae72f69b463577cae17dbb8b55479074", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -661, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -190, + "y": -212 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 931, + "facing_y": -364, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 270, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 140, + "result": null, + "round_number": 1, + "tick": 862 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 862 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9b77489e20062bef8e98ea90f6d3cb2067d8a8bd44dd71c03334ef321fec235f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -194, + "y": -216 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 915, + "facing_y": -404, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 272, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 139, + "result": null, + "round_number": 1, + "tick": 863 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 863 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "61bf8711c62454b37d624bf70a06b80399214e7dcd6f1adaf5771382b8315bd3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -756, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -198, + "y": -219 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 896, + "facing_y": -443, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 274, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 138, + "result": null, + "round_number": 1, + "tick": 864 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 864 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9686a293d62c075bb0b7b33d5378cf568ba2e951c16669d2d0efc3d8bf0dbd98", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -755, + "facing_y": -660, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -202, + "y": -222 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 876, + "facing_y": -482, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 276, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 137, + "result": null, + "round_number": 1, + "tick": 865 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 865 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "04ef9e169496e2b34762db4e330fff61cb274778b1a897db09d8a719f49b29c8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -662, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -205, + "y": -226 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 854, + "facing_y": -520, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 278, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 136, + "result": null, + "round_number": 1, + "tick": 866 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 866 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d89c7a5fb915b1967440070e8c158e6cc7bc78a9ff7da97ce2d52ef1a91b5efe", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -758, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -209, + "y": -229 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 831, + "facing_y": -557, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 280, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 135, + "result": null, + "round_number": 1, + "tick": 867 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 867 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d92630b738680d4d13f93af49190e3bcbb692cd348c0b13c58523395dfaa0226", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -757, + "facing_y": -660, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -213, + "y": -232 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 806, + "facing_y": -592, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 282, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 134, + "result": null, + "round_number": 1, + "tick": 868 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 868 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "38aa24f80442a102e0e8a243f3dc851b4be6bdcdc997137ba2aaef24cec732ea", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -756, + "facing_y": -661, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -217, + "y": -236 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 779, + "facing_y": -627, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 284, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 133, + "result": null, + "round_number": 1, + "tick": 869 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 869 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ec5690cc28cbdb912c64918466551c84224f8b72cd765463c52a976c74d60eb5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -755, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -221, + "y": -239 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 751, + "facing_y": -660, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 286, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 132, + "result": null, + "round_number": 1, + "tick": 870 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 870 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "cdf5993e8eff031ba49aec909e32c7741ff4753e8e8eb621c6321c549fadaea1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -224, + "y": -242 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 722, + "facing_y": -692, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 288, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 131, + "result": null, + "round_number": 1, + "tick": 871 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 871 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c8fec68787f13d17ab0ca21e688df9c98b8935bedcaca501a4b38d976a016769", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -228, + "y": -245 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 691, + "facing_y": -723, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 290, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 130, + "result": null, + "round_number": 1, + "tick": 872 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 872 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ac38417b257dce7b711df4fd27551e89812e2c732f82937494c14ca4c66090fc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -752, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -232, + "y": -249 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 659, + "facing_y": -752, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 292, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 129, + "result": null, + "round_number": 1, + "tick": 873 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 873 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "17ba281aec09520d750d69aa02ae75886731aec9da79afcff58a20673d134e75", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -755, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -236, + "y": -252 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 626, + "facing_y": -780, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 294, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 128, + "result": null, + "round_number": 1, + "tick": 874 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 874 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bd4ed3dfaa2e2f0e0a60d77c693c4188d834a1c63bb32bac9c605eaf67165de0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -660, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -239, + "y": -255 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 591, + "facing_y": -806, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 296, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 127, + "result": null, + "round_number": 1, + "tick": 875 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 875 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dd095a2d10909f1ab89d7ffcee66f3ca895689af0bbe306a60b15d17769c1eef", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -243, + "y": -259 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 556, + "facing_y": -831, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 298, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 126, + "result": null, + "round_number": 1, + "tick": 876 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 876 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "456069dcb998fa90c45fe772d885eeee10d3478ae5128fa12011f09cbae670ce", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -757, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -247, + "y": -262 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 519, + "facing_y": -855, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 300, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 125, + "result": null, + "round_number": 1, + "tick": 877 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 877 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e108000c40bcdd73adf5b423ea5371e0bc112dfabebc32eecf0cee84d30c31c0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -756, + "facing_y": -660, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -251, + "y": -265 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 481, + "facing_y": -877, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 302, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 124, + "result": null, + "round_number": 1, + "tick": 878 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 878 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8a765c1533fa2652e054ff85fa70c6eb86b6519c02414d7274e9d8ab64a9e092", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -662, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -254, + "y": -269 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 442, + "facing_y": -897, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 304, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 123, + "result": null, + "round_number": 1, + "tick": 879 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 879 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "07be02978bb676422e0d3aa42805374d22d641b8e6ceb94d3a88b135391b5ba8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -760, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -258, + "y": -272 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 402, + "facing_y": -915, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 306, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 122, + "result": null, + "round_number": 1, + "tick": 880 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 880 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e892c9a24edafa19d5d3ba29531f1162fc031bff4122cab9c067e0e0973d2ccf", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -758, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -262, + "y": -275 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 362, + "facing_y": -932, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 308, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 121, + "result": null, + "round_number": 1, + "tick": 881 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 881 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6cb370ef5652435d604187dd074e8af8a5a41598c53ef42389dcfc545fbacd0f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -757, + "facing_y": -661, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -266, + "y": -278 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 321, + "facing_y": -947, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 310, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 120, + "result": null, + "round_number": 1, + "tick": 882 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 882 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "412b0fd440e0ad6c2084cc45fcaf01d200589270881e8fc8a65cc9b85e401f39", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -755, + "facing_y": -664, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -270, + "y": -282 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 279, + "facing_y": -960, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 312, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 119, + "result": null, + "round_number": 1, + "tick": 883 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 883 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "94d58dfb979fbb89239e4dab320ab2c516e9abc3f01f8499804eaa1440f26ae0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -273, + "y": -285 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 237, + "facing_y": -972, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 314, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 118, + "result": null, + "round_number": 1, + "tick": 884 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 884 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "190b60f63966cb5c694c2473181ccc7b0639864769bab7864a28e7621dafd8a8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -760, + "facing_y": -661, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -277, + "y": -288 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 194, + "facing_y": -981, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 316, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 117, + "result": null, + "round_number": 1, + "tick": 885 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 885 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ec976ec45c7da86b91fc2f6d0e71ac741a1a705b2f04a9e231d5c29ed91da18b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -758, + "facing_y": -663, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -281, + "y": -292 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 151, + "facing_y": -989, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 318, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 116, + "result": null, + "round_number": 1, + "tick": 886 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 886 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "37f21ed6f3e40ca112d8c110aba471ecaf1d6d3480e52123b62e44f4d7a58530", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -756, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -285, + "y": -295 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 108, + "facing_y": -994, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 320, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 115, + "result": null, + "round_number": 1, + "tick": 887 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 887 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f36e38e4557f7a99f2b8b1a3264f96c052dcdf3889b30d753730921417f6c06c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -754, + "facing_y": -660, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -289, + "y": -298 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 65, + "facing_y": -998, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 322, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 114, + "result": null, + "round_number": 1, + "tick": 888 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 888 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fed9788c092ebe428a29aad7a0dfe34236d862e3f97cd46622b6975d1af7aaf5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -752, + "facing_y": -663, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -292, + "y": -302 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 21, + "facing_y": -1000, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 324, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 113, + "result": null, + "round_number": 1, + "tick": 889 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 889 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "66f51632bfc1f59989241a9ed14f6cf29c5ec4ece132ae96cc9d76f7f7f11ee5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -760, + "facing_y": -656, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -296, + "y": -305 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -23, + "facing_y": -1000, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 326, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 112, + "result": null, + "round_number": 1, + "tick": 890 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 890 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "31d1a5b3fd1fa93e96e4142d09465d905eb6d58522203c5b769c63673d95b2d5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -758, + "facing_y": -659, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -300, + "y": -308 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -67, + "facing_y": -998, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 328, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 111, + "result": null, + "round_number": 1, + "tick": 891 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 891 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b8fcbf4aa69842c2d0178bc28f2691478e2bd5b00ebc0406974d97716cec05fd", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -755, + "facing_y": -662, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -304, + "y": -311 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -110, + "facing_y": -994, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 330, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 110, + "result": null, + "round_number": 1, + "tick": 892 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 892 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "267ddab7a486d86c80e8e2457fb491dccbd1312e814ed8f7438e77ef82d1d0da", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -753, + "facing_y": -666, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -307, + "y": -315 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -153, + "facing_y": -988, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 332, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 109, + "result": null, + "round_number": 1, + "tick": 893 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 893 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0530ee889cfb1378de58a722be9cf0ef348d8b050e34a8bfad9d344e40595442", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -763, + "facing_y": -657, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -311, + "y": -318 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -196, + "facing_y": -981, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 334, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 108, + "result": null, + "round_number": 1, + "tick": 894 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 894 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f0e767f54fe1cd1e1e77f1720d4c9456b160f33410663de6d60ba919ac48d4d3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -760, + "facing_y": -661, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -315, + "y": -321 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -239, + "facing_y": -971, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 336, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 107, + "result": null, + "round_number": 1, + "tick": 895 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 895 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ffbf312801f51e2f244afbd69b34557b34328c5d59bfa9932270e867d39433ae", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -757, + "facing_y": -666, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -319, + "y": -325 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -281, + "facing_y": -960, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 338, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 106, + "result": null, + "round_number": 1, + "tick": 896 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 896 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bbb6d335940231fcf2d903746f9ce8e771b6a2236a016a5e6771ce6954793de0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -766, + "facing_y": -666, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -323, + "y": -328 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -323, + "facing_y": -947, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 340, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 105, + "result": null, + "round_number": 1, + "tick": 897 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 897 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e340cbf011585b6f504f6bd42a04e8ee959aba4aef5387a145f595b3e7ac8978", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -763, + "facing_y": -672, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -326, + "y": -331 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -364, + "facing_y": -931, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 342, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 104, + "result": null, + "round_number": 1, + "tick": 898 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 898 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2113039d60f879e496789cccb23729c2b7a182a80af02cea9368a9522bafb655", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -764, + "facing_y": -666, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -330, + "y": -335 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -404, + "facing_y": -915, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 344, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 103, + "result": null, + "round_number": 1, + "tick": 899 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 899 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "577a020ba566315bf1c7152fee6b25128fc1349e0f97ce9b30c641b6f15da516", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -760, + "facing_y": -652, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -334, + "y": -338 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -443, + "facing_y": -896, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 346, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 102, + "result": null, + "round_number": 1, + "tick": 900 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 900 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "cb9585718f9735951793f4de21b21902a5628f66403dfe4c2c763987cd0f3814", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -756, + "facing_y": -658, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -338, + "y": -341 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -482, + "facing_y": -876, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 348, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 101, + "result": null, + "round_number": 1, + "tick": 901 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 901 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ea2a70aae6c3ade4286c854a69142de016a5d2edd1bdfe63fbf0d9ff21fabbce", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -750, + "facing_y": -666, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -342, + "y": -345 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -520, + "facing_y": -854, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 350, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 100, + "result": null, + "round_number": 1, + "tick": 902 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 902 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7d0f3a5d731d47ef9c124115cc2981f6033b464b52283e09e18aba47adac2933", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -766, + "facing_y": -666, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -345, + "y": -348 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -557, + "facing_y": -831, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 352, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 99, + "result": null, + "round_number": 1, + "tick": 903 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 903 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5c84dea6e76703142ab8cd7947e6b8d2b378f3814e662a240c057c1cd1ccfbc1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -769, + "facing_y": -653, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -349, + "y": -351 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -592, + "facing_y": -806, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 354, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 98, + "result": null, + "round_number": 1, + "tick": 904 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 904 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ccf7d60e13eb8fb9a9b7acff17b27a466c62566ef0495bc0599d42ae7c3c8160", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -761, + "facing_y": -666, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -353, + "y": -355 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -627, + "facing_y": -779, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 356, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 97, + "result": null, + "round_number": 1, + "tick": 905 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 905 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "24dc7468f7a0d3ca138083ad3ac19e765ef84623414fc351196be0564e22a5de", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -800, + "facing_y": -666, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -3, + "warnings": 0, + "x": -357, + "y": -358 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -660, + "facing_y": -751, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 358, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 96, + "result": null, + "round_number": 1, + "tick": 906 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 906 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c45123c93c80f22b46684531f5f56581e68006a7db0eb84b2c75323be3feaa9f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -800, + "facing_y": -700, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 0, + "x": -361, + "y": -361 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -692, + "facing_y": -722, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 360, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 95, + "result": null, + "round_number": 1, + "tick": 907 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 907 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "71ee290eb4e3baa63735d858a8e05fa09380f2ec739a28fd9e47deec6fb7682d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -781, + "facing_y": -625, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 362, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 94, + "result": null, + "round_number": 1, + "tick": 908 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 908 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "27ac25d2b1e090f50641c0ac123f1d38c8c666e5f15415c11d898863059737ca", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -807, + "facing_y": -590, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 364, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 93, + "result": null, + "round_number": 1, + "tick": 909 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 909 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e510ac8d907386c9a58860ba4b71ed874d2cbb20cdd1280749abc77f272fcf7d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -832, + "facing_y": -554, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 366, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 92, + "result": null, + "round_number": 1, + "tick": 910 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 910 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e250a62f60ebd74b064a91e3928ac11274a71c22697e42e681a4070f4db7dadd", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -856, + "facing_y": -517, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 368, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 91, + "result": null, + "round_number": 1, + "tick": 911 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 911 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bd87ad1b25e7c30ad74798061914922b5a4c8ada55fc4c71f826603d8713768d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -878, + "facing_y": -479, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 370, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 90, + "result": null, + "round_number": 1, + "tick": 912 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 912 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7f63a384205404b3f646147574913d4b7e85862ebea1256efc29240304ba8821", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -898, + "facing_y": -440, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 372, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 89, + "result": null, + "round_number": 1, + "tick": 913 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 913 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a6872be76800a3aa2c2c51ebfa1c362f0b52e98ef4aa281cf2cd769a4f990a59", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -916, + "facing_y": -400, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 374, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 88, + "result": null, + "round_number": 1, + "tick": 914 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 914 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c402be80c15d7f416ac2f866211904d8e4ace241d04d61ba1b8577a24719ba1e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -933, + "facing_y": -360, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 376, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 87, + "result": null, + "round_number": 1, + "tick": 915 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 915 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6c55f9c0f9136b1e7307e892d4e9edd09d6f3d11b26b87f4fbf8fd0ac8005c6e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -948, + "facing_y": -319, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 378, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 86, + "result": null, + "round_number": 1, + "tick": 916 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 916 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b31b247c6790f8014fcd00da7df178a0aee85d020447966efd20fa8f58376283", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -961, + "facing_y": -277, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 380, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 85, + "result": null, + "round_number": 1, + "tick": 917 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 917 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fc6a9691d6c848aecf7656cd16437a96ca44cfaa779c911b3e72bb10a96deb89", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -972, + "facing_y": -235, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 382, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 84, + "result": null, + "round_number": 1, + "tick": 918 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 918 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "842169ec03f06a8ed15cf392ee2a0f0d55d5bf9b921df9079324a4332972107d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -981, + "facing_y": -192, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 384, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 83, + "result": null, + "round_number": 1, + "tick": 919 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 919 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6ecc4647d72f06f992567bf608306657055bc040e2769b5042a7f8836efb618b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -989, + "facing_y": -149, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 386, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 82, + "result": null, + "round_number": 1, + "tick": 920 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 920 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a41d1f8c1783cabba8f93d27dcb40f0843e558bd4deace7d9f6c11323550dd23", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -994, + "facing_y": -106, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 388, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 81, + "result": null, + "round_number": 1, + "tick": 921 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 921 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "198bd2092aaa90b8ea462b4c5b39824ab5d9781ec0b1da42f53a0e64c46e4942", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -998, + "facing_y": -63, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 390, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 80, + "result": null, + "round_number": 1, + "tick": 922 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 922 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "99d46912b54626f81fc640be58a08b6e3cf25b88e0f3c3c34f2df48be5015ebe", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": -19, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 392, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 79, + "result": null, + "round_number": 1, + "tick": 923 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 923 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0b2a369605ed0a77af54531567753dad820bda55068771d0a5e28a56cba21943", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -1000, + "facing_y": 25, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 394, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 78, + "result": null, + "round_number": 1, + "tick": 924 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 924 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "11d0b7a9f37d9ed797509cefc90e00c075e7c64d6f7de136361d045e0bc63bd7", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -998, + "facing_y": 69, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 396, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 77, + "result": null, + "round_number": 1, + "tick": 925 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 925 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "32ef1c094c1c3592aeb11b9c1ba72e761da27a7818e2f078de4de67fc3e89539", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -994, + "facing_y": 112, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 398, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 76, + "result": null, + "round_number": 1, + "tick": 926 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 926 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f0874f6fece61dd74cc73ea1e60277b1d4853bed216290a6dc59e4915149c1ef", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -988, + "facing_y": 155, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 400, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 75, + "result": null, + "round_number": 1, + "tick": 927 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 927 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6bc8dc92a3403d23dbdcf95ed021ca04bc9a42746cf733de081f0c8040000961", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -980, + "facing_y": 198, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 402, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 74, + "result": null, + "round_number": 1, + "tick": 928 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 928 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4cfaa9bd60716d43e847a33fafb8b69c8f45900ae3936cb4564f8250ae665bab", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -971, + "facing_y": 241, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 404, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 73, + "result": null, + "round_number": 1, + "tick": 929 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 929 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5f0c64c462a1fe1d7e8204e600835ba4ef55a5112308e6574a060424470b1888", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -959, + "facing_y": 283, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 406, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 72, + "result": null, + "round_number": 1, + "tick": 930 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 930 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "68dbfe09d21c08e9258c925a8b152cf5ee6c2a20c9d061fbcf2a81154bdfa928", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -946, + "facing_y": 325, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 408, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 71, + "result": null, + "round_number": 1, + "tick": 931 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 931 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dc0dbe058f42331874992104ff481e7a656740dfe26a1ef1d7bd810697b659d3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -931, + "facing_y": 366, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 410, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 70, + "result": null, + "round_number": 1, + "tick": 932 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 932 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1661e9bb575502ec880d5c27853784f19f7b33570b87e352f11d54153dd5ca2b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -914, + "facing_y": 406, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 412, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 69, + "result": null, + "round_number": 1, + "tick": 933 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 933 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "713f4945f63aa21e5a34bd8c43d855a4ca59577f078f1ba641765edca3ff8ce4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -895, + "facing_y": 445, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 414, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 68, + "result": null, + "round_number": 1, + "tick": 934 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 934 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9dfbf1dc2d101b3b1c97a7847df73fc31c50ba39e4dfd12f2f3f3d96c1d97c01", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -875, + "facing_y": 484, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 416, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 67, + "result": null, + "round_number": 1, + "tick": 935 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 935 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3c06a6d88acd58070bd9578f45906c0e027e17f4e3392170b8b3d58901cbda79", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -853, + "facing_y": 522, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 418, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 66, + "result": null, + "round_number": 1, + "tick": 936 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 936 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e4f71bfd182143fb6613b36613a7e8cbcf5231499e802b6ef6f3587460f6d005", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -829, + "facing_y": 559, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 420, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 65, + "result": null, + "round_number": 1, + "tick": 937 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 937 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2abe4cdb6d45b8247bcc641cf2294936ce512742419e8754a37b9733f246f3b5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -804, + "facing_y": 595, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 422, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 64, + "result": null, + "round_number": 1, + "tick": 938 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 938 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ec468c5854d935ddd6b8ac85a6bb17b661140f7f2e61f37607ff43d03a1f4e83", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -777, + "facing_y": 629, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 424, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 63, + "result": null, + "round_number": 1, + "tick": 939 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 939 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "35a576ec2702841408a383c1e1cc5f8a4b65349425e87bb06d32bc11dd90b257", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -749, + "facing_y": 663, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 426, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 62, + "result": null, + "round_number": 1, + "tick": 940 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 940 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7aa30f7af2b87188bf2ea2cabfcb2d9e558e133bd508e55dad9e8d2e72411ef4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -719, + "facing_y": 695, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 428, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 61, + "result": null, + "round_number": 1, + "tick": 941 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 941 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1b9c00da438c9e1fa238f8e1c20046bef5109faf5743cbff74545f9c86e3b785", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -688, + "facing_y": 726, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 430, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 60, + "result": null, + "round_number": 1, + "tick": 942 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 942 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "28fa1248ad142eb48e3d206bfa7d86f2c575ed5865458da00c6e9a58ebc9b5f0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -656, + "facing_y": 755, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 432, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 59, + "result": null, + "round_number": 1, + "tick": 943 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 943 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2dbb0663fbdc2c5c0bfa4b78bd9273643aa15398ff440da02164d0f25086ccce", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -622, + "facing_y": 783, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 434, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 58, + "result": null, + "round_number": 1, + "tick": 944 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 944 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6df1e4eae32a4c3649e6fcd3c5e1f472d53919ad8a231c0d6169cbcfa82afbaf", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -587, + "facing_y": 809, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 436, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 57, + "result": null, + "round_number": 1, + "tick": 945 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 945 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "45f1c86575e3344847dd77a7191e542758ef8f3d5f540f7cc2dd8d1998770dc0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -551, + "facing_y": 834, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 438, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 56, + "result": null, + "round_number": 1, + "tick": 946 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 946 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f52735d2bcda09aa54549d151c719db019d54ac9021e9d4539a8f3440486151f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -514, + "facing_y": 858, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 440, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 55, + "result": null, + "round_number": 1, + "tick": 947 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 947 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "aee6cfcc89812274d2b65c04b6458648d8f823edf11bd6af3c24abf9b70d6811", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -476, + "facing_y": 879, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 442, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 54, + "result": null, + "round_number": 1, + "tick": 948 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 948 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "50bc6b30c25f9a38ee2600c3a67753ee80ed5e3c5a1218c29998ac6e30c5a014", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -437, + "facing_y": 899, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 444, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 53, + "result": null, + "round_number": 1, + "tick": 949 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 949 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "04a80cab5daeb96e95045879bfe2120320722144a6615d9a1c1d956807a1f183", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -398, + "facing_y": 918, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 446, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 52, + "result": null, + "round_number": 1, + "tick": 950 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 950 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "82378cf2b215ee2193dab5f978f009171caa040434919c633b23104119786b7e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -357, + "facing_y": 934, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 448, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 51, + "result": null, + "round_number": 1, + "tick": 951 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 951 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "788799aaded0fe208051a9167d5ffcf11c2c9ec3fb922e5d721c7ac9e59afda0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -316, + "facing_y": 949, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 450, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 50, + "result": null, + "round_number": 1, + "tick": 952 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 952 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ca26ebc8567ff63c70ca215da2d7e62789f6834c7d23b2456f3d1ea6762359b1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -274, + "facing_y": 962, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 452, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 49, + "result": null, + "round_number": 1, + "tick": 953 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 953 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "14ed44f6ea883e246d105c1f32b4db27ffacdd1e27ed179dc29c21584db54950", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -232, + "facing_y": 973, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 454, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 48, + "result": null, + "round_number": 1, + "tick": 954 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 954 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "89b0534594441cba0977e72d8e79d927fecd1df8bd339e3cc6d427ba1e8f3ed3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -189, + "facing_y": 982, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 456, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 47, + "result": null, + "round_number": 1, + "tick": 955 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 955 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fae2ec10304844792e84d0790d6a85578de12fe2ce249fca7a798f345344a07b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -146, + "facing_y": 989, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 458, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 46, + "result": null, + "round_number": 1, + "tick": 956 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 956 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8b70693ef0272f40b11716bd2aaad46abacfd5c1bd9b4c2e05efc71754d7ec73", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -103, + "facing_y": 995, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 460, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 45, + "result": null, + "round_number": 1, + "tick": 957 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 957 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2ed5451725b575cb6c47a085ef40a8f71cca3e511090ca7ee9bc6c523cc561d5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -59, + "facing_y": 998, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 462, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 44, + "result": null, + "round_number": 1, + "tick": 958 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 958 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f508ca906e21051713cd358f9e1d8ed309f7e3b93fb24ecff516dc0c9b51edf4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -15, + "facing_y": 1000, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 464, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 43, + "result": null, + "round_number": 1, + "tick": 959 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 959 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e0cf0bccdc4a7913ec7c75463d583771d3d5f55cde9d6aa1c1b885711bca1196", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 29, + "facing_y": 1000, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 466, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 42, + "result": null, + "round_number": 1, + "tick": 960 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 960 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c5805772a9d226bb4a2daf2f16b35052a687475e1ee70ba182ff93bca53d0d7c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 73, + "facing_y": 997, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 468, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 41, + "result": null, + "round_number": 1, + "tick": 961 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 961 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c54b65d9d74c88a4ec0124f6bb1a29e37fa4c4abd71f9996ff8e802bd2047358", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 116, + "facing_y": 993, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 470, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 40, + "result": null, + "round_number": 1, + "tick": 962 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 962 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bd11ae1a50820a1dd86a1aab3e680c033d35d9b619a39d45d1fbde0ff93174db", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 159, + "facing_y": 987, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 472, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 39, + "result": null, + "round_number": 1, + "tick": 963 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 963 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9ceca97eb5db970d74adbedc5e7ecddda7634550036d5ac1c6825f2e0de58b7c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 202, + "facing_y": 979, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 474, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 38, + "result": null, + "round_number": 1, + "tick": 964 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 964 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a9efcb0014d0b5bc2afe72ad050e45ccdd6519530e7f26f19a2b9ec38bbeab1c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 245, + "facing_y": 970, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 476, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 37, + "result": null, + "round_number": 1, + "tick": 965 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 965 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "13f69b573f8df74574e1f362327bc7b6b5713333ce3c911702b45e2991230c8d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 287, + "facing_y": 958, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 478, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 36, + "result": null, + "round_number": 1, + "tick": 966 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 966 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f8c35361a3325c28f9146f4341d74c461fdb8e3feea7c47879d6962bebeed381", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 328, + "facing_y": 945, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 480, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 35, + "result": null, + "round_number": 1, + "tick": 967 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 967 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "46740a990dfeb273addbfe5d6addb5625508e20ac21bf19b1f47712350025ea1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 369, + "facing_y": 930, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 482, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 34, + "result": null, + "round_number": 1, + "tick": 968 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 968 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "64f1ad1dc9547a502455494277decf63c81afb7cdfc64f80217fd96277e0b553", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 409, + "facing_y": 913, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 484, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 33, + "result": null, + "round_number": 1, + "tick": 969 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 969 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "97f2143e4f6445ace4520f30a20e7537c1278c5ffcddbcac1fb7a69d4ce97a09", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 448, + "facing_y": 894, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 486, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 32, + "result": null, + "round_number": 1, + "tick": 970 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 970 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7c29e6dc3480dcbd3ac5c0467499e7fd4436ec9f6c9337350449072b7a64147b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 487, + "facing_y": 874, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 488, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 31, + "result": null, + "round_number": 1, + "tick": 971 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 971 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7d826fbb8591138fdd7cda34d89dc823b6d33660a452f6588a69e5a780602224", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 524, + "facing_y": 851, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 490, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 30, + "result": null, + "round_number": 1, + "tick": 972 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 972 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "26334f7481864a50232c23d10447f60409e8e4b7a7a951e59c9b49f1e55565eb", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 561, + "facing_y": 828, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 492, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 29, + "result": null, + "round_number": 1, + "tick": 973 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 973 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1eada7986ba65dd3f6555aad9518e8c8b2eedd59556af0e7eed6d9ede1b246cc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 596, + "facing_y": 803, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 494, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 28, + "result": null, + "round_number": 1, + "tick": 974 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 974 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c541328811ff46aff3fc8c91ecc79182ac8622893b7c8ca836e7366ed7ba0194", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 630, + "facing_y": 776, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 496, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 27, + "result": null, + "round_number": 1, + "tick": 975 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 975 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "af2527622da5bc445d1b03cab085f21942f63562430610ad99ff526df3efd848", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 664, + "facing_y": 748, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 498, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 26, + "result": null, + "round_number": 1, + "tick": 976 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 976 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e6eb70d8355019f01549a3ba119c61dd0f0f1f1f993a78c380355ec955045b85", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 696, + "facing_y": 718, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 500, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 25, + "result": null, + "round_number": 1, + "tick": 977 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 977 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "62bc342e4859f0244abe58fc87f59d9431c90fcf2c1126d712ad0d43209f5cf9", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 502, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 24, + "result": null, + "round_number": 1, + "tick": 978 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 978 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6f1a0c166e6eb8d2b29a3bb8c716ab2c87e4fa953070bd5ea6e7deed28e05789", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 504, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 23, + "result": null, + "round_number": 1, + "tick": 979 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 979 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "878a4d99319438ffc35415dbd797fa794cff0f5e4b0b6be8c5a6f293fa0b2942", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 506, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 22, + "result": null, + "round_number": 1, + "tick": 980 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 980 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "59709aef29b0a8b04c864ca8b00a0d9a08b20aa72306420d7d11e931ea8f7a71", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 21, + "result": null, + "round_number": 1, + "tick": 981 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 981 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "311db910dd89a274e1b29116202eeed8b8442926e5147fcc89b4d8ed785cecb5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 20, + "result": null, + "round_number": 1, + "tick": 982 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 982 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5d9c1c7a78207190d959f625156d8ac36ddcc5b94dd250e99f8d065fbef8ecb9", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 19, + "result": null, + "round_number": 1, + "tick": 983 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 983 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9c0b8c560f7e641372b0b03ab22ca7205a58212365258b91a4cfd2bde558b53d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 18, + "result": null, + "round_number": 1, + "tick": 984 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 984 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4161b14e45edc885a7748efc5a6e571b5c7f1eaac1c65722ba0a98dd6daa63ba", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 17, + "result": null, + "round_number": 1, + "tick": 985 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 985 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e2261699b7c1b3aaddaa9cbbe9fa9f3d6f1131a9d2b975da9e7e633955417b9d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 16, + "result": null, + "round_number": 1, + "tick": 986 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 986 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b8b0aeabe8f29354680d83e52ac8c9276fbd6aa98ed20cfd3d503c335e6efd82", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 15, + "result": null, + "round_number": 1, + "tick": 987 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 987 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8176488cd2bc63d6aca1da184c495e91b1080d6a5bc20283803ba6662e046b16", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 14, + "result": null, + "round_number": 1, + "tick": 988 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 988 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6f56557c73237ccf5a0ae9aea5977db73fee75eecc09c0c1dd4ae8eb69d861d1", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 13, + "result": null, + "round_number": 1, + "tick": 989 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 989 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "10d597be334b2ec22a5b5979d689dd72bfd053b54f39eda7685491716922d4df", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 12, + "result": null, + "round_number": 1, + "tick": 990 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 990 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "af3afe7fe848ed958b46cd7ca7075d66f27d20e1291652e5c4bde077a850e0da", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 11, + "result": null, + "round_number": 1, + "tick": 991 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 991 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "aea56f2e471feb614d23fb6931d22b0667ca11fdd6693aa81f3a2c6f377d6287", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 10, + "result": null, + "round_number": 1, + "tick": 992 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 992 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "32eae404ae6ef3e1dbc008f560ae709d61ac7da9554bbc07ea0bd4d679a50eed", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 9, + "result": null, + "round_number": 1, + "tick": 993 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 993 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1be6a0f7d53133fc34917c3789d5c0296626af5ba2a9f5708f03d6bd1a59c59e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 8, + "result": null, + "round_number": 1, + "tick": 994 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 994 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "691c9832958f038233082fc5c1ff7b72553d24ebd49ca0e40ae370a0ca360202", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 7, + "result": null, + "round_number": 1, + "tick": 995 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 995 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e87d8323d4fba279f0745b217d36e7e886af1dfaca7c0a3ff879af071547dd17", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 6, + "result": null, + "round_number": 1, + "tick": 996 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 996 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b18ed76eb48f3660841df9d49a10fb45dd98dae78ba448a3ba10c95e0b1f4d70", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 5, + "result": null, + "round_number": 1, + "tick": 997 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 997 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5ed5452cf6a671bada892e9ba2b893ff48ae93bb5ee2d0c23ba2c49ad86b0d03", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 4, + "result": null, + "round_number": 1, + "tick": 998 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 998 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "de55bcec4714934ed14eaf2ec804f2e5158f3d5561b2db8ebc56038e46ff0669", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 3, + "result": null, + "round_number": 1, + "tick": 999 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 999 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "49ca3704e6e2d7025805464e4310b3eaf63e757f9dbf5bc218f51eb8afddbde6", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 2, + "result": null, + "round_number": 1, + "tick": 1000 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1000 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c7117b7daf99a53689fb0128550237c8efb9cfc291cf581249a98d11358b79d6", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "rest", + "phase_ticks_remaining": 1, + "result": null, + "round_number": 1, + "tick": 1001 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1001 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "65c987a522125353840024bcbba1b37182d98759299f2fe22db6c0044fdd913d", + "events": [ + { + "action_id": null, + "actor_id": null, + "amount": 0, + "blood": 0, + "detail": "round_start", + "direction": 0, + "event_id": 45, + "kind": "bell", + "target_id": null, + "tick": 1002 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "fight", + "phase_ticks_remaining": 1000000, + "result": null, + "round_number": 2, + "tick": 1002 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1002 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ecfc9f410082f400c689dc4f09fae6ef14f13418cfdd29a7d2fecda0c8ce6717", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 203, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -365, + "y": -365 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 365, + "y": 365 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999999, + "result": null, + "round_number": 2, + "tick": 1003 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1003 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "90706b86e747af3e0e77fb255603e51c19e9afe6e5cef0cadf0af22133849ed5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 204, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 2, + "velocity_y": 2, + "warnings": 0, + "x": -363, + "y": -363 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -2, + "velocity_y": -2, + "warnings": 1, + "x": 363, + "y": 363 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999998, + "result": null, + "round_number": 2, + "tick": 1004 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1004 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0fbd68d415fc6255ebb4a78a58ff6b01f81fb0cc255a2da60d3ec5fbbe1e4285", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 205, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 3, + "velocity_y": 3, + "warnings": 0, + "x": -360, + "y": -360 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": -3, + "warnings": 1, + "x": 360, + "y": 360 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999997, + "result": null, + "round_number": 2, + "tick": 1005 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1005 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1928aa0fd2a8cc901c2ff8165b83943ef9f04dc2773cbc115e53cd24afb7d8ef", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 206, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -356, + "y": -356 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 357, + "y": 357 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999996, + "result": null, + "round_number": 2, + "tick": 1006 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1006 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1ecdc0cdd740216c6154edbd183ecb2234e5a1ca99f9b6cd69df88fda4aa647f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 207, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -353, + "y": -353 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 353, + "y": 353 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999995, + "result": null, + "round_number": 2, + "tick": 1007 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1007 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5a4a04ee23d4e84488d098e16ea8c54c5155d20603f0393605a10ad5cfb91739", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 208, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -348, + "y": -348 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 349, + "y": 349 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999994, + "result": null, + "round_number": 2, + "tick": 1008 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1008 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fa7f6d5bf2a5fa50a32cb61a3d8d7060657273932ecaf6316b43c6dccb58574b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 209, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -344, + "y": -344 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 345, + "y": 345 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999993, + "result": null, + "round_number": 2, + "tick": 1009 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1009 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "052ef0d5bcbc162a056c63268f438a3dc20ed22b8e2ce35278db97976ee26a5d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 210, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -340, + "y": -340 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 341, + "y": 341 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999992, + "result": null, + "round_number": 2, + "tick": 1010 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1010 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9d3f691b6036f3603f172b123eda6fe10c05379b403a7fda50b9b73be7a651e7", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 211, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -336, + "y": -336 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 337, + "y": 337 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999991, + "result": null, + "round_number": 2, + "tick": 1011 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1011 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a9bfe06cd0db5a684e2a1a4f037b3e894562921006579a267b88da6a76b130e5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 212, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -332, + "y": -332 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 333, + "y": 333 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999990, + "result": null, + "round_number": 2, + "tick": 1012 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1012 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6e730b6c339b952c8bb055fc85e2d5b91ce5ce3772fc9cfd5f0573878bde084e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 213, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -327, + "y": -327 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 329, + "y": 329 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999989, + "result": null, + "round_number": 2, + "tick": 1013 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1013 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "83bba958ff0dea5559a9676835828a7d4fafd5cb69400cc0ccc7f0cbb4ad40e8", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 214, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -323, + "y": -323 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 325, + "y": 325 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999988, + "result": null, + "round_number": 2, + "tick": 1014 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1014 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fa0be9255358b01486a5fb9024292ae661afca798d8306b2e326c355d7a7b680", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 215, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -319, + "y": -319 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 321, + "y": 321 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999987, + "result": null, + "round_number": 2, + "tick": 1015 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1015 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a67b06ead2b860c187d6a08482f459a3dd119a60dd619b3e50bd0a2d9f3971fe", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 216, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -315, + "y": -315 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 317, + "y": 317 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999986, + "result": null, + "round_number": 2, + "tick": 1016 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1016 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f7044a950e6fa4635f2950d2927cb198fd242e643cbe57853d95b74396870c5e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 217, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -310, + "y": -310 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 313, + "y": 313 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999985, + "result": null, + "round_number": 2, + "tick": 1017 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1017 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c49ea003b269852f289cb2d85bdbf6504786e4199e79289b603db87bc30f087c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 218, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -306, + "y": -306 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 308, + "y": 308 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999984, + "result": null, + "round_number": 2, + "tick": 1018 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1018 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0af04fc5816fa875123a8c07052e491ca47c14f627b608dcc8f1b24d4efe6d85", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 219, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -302, + "y": -302 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 304, + "y": 304 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999983, + "result": null, + "round_number": 2, + "tick": 1019 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1019 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d8559de5b1dfc31c299f4c338ce2083944266a9cb15eecde9ce9dbe8e1fca46b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 220, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -298, + "y": -298 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 300, + "y": 300 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999982, + "result": null, + "round_number": 2, + "tick": 1020 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1020 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "34b8a4a805baa81b32114e3b7c33b53fe85fcb3ba92308405474454a0783cff9", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 221, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -293, + "y": -293 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 296, + "y": 296 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999981, + "result": null, + "round_number": 2, + "tick": 1021 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1021 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dc7a7e73f5e5a2a2cafd6063102b645cc0276950d8ae5365a4453c509e7b23d0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 222, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -289, + "y": -289 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 292, + "y": 292 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999980, + "result": null, + "round_number": 2, + "tick": 1022 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1022 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e8f78a0e71247a9d75d0014c62e3db420db3618291f4b19256231a6579fa6982", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 223, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -285, + "y": -285 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 288, + "y": 288 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999979, + "result": null, + "round_number": 2, + "tick": 1023 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1023 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8182c3d06b02eea3d09d21290b87333b8b59637a9d371829ac7490392b2bf5b4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 224, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -281, + "y": -281 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 284, + "y": 284 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999978, + "result": null, + "round_number": 2, + "tick": 1024 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1024 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "44477d333498f3e2a2948657634936a6f8692a275a804208ff9946be8bf4b6ed", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 225, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -276, + "y": -276 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 280, + "y": 280 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999977, + "result": null, + "round_number": 2, + "tick": 1025 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1025 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "da1b2458efd1e32d3c7515c6b95d78c8f12f619699a7a2f5334bd1db7510da1a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 226, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -272, + "y": -272 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 276, + "y": 276 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999976, + "result": null, + "round_number": 2, + "tick": 1026 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1026 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9a504626a0425b172c146ec1a5624004ffab0ac7acd4af8ff2189c0f3380df65", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 227, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -268, + "y": -268 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 272, + "y": 272 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999975, + "result": null, + "round_number": 2, + "tick": 1027 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1027 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "569fef86630c2d1fb32157e99559e04fdbc02ccc154919dd8228c00fb9cd635f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 228, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -264, + "y": -264 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 268, + "y": 268 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999974, + "result": null, + "round_number": 2, + "tick": 1028 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1028 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "260a5747c837c85f3ddf18a79d252b5118691b47d1ab61351a1a1c5bb0baa6f6", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 229, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -259, + "y": -259 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 264, + "y": 264 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999973, + "result": null, + "round_number": 2, + "tick": 1029 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1029 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e89a442ab91ad75ed1fce752fa3657dd03b02bdf645b16f2f1f4ad8bad4526e5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 230, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -255, + "y": -255 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 260, + "y": 260 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999972, + "result": null, + "round_number": 2, + "tick": 1030 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1030 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ce6b59aa35624ab99501e534719c8f2129bce20f045b4c4d31907f7cd17ff2c2", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 231, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -251, + "y": -251 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 256, + "y": 256 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999971, + "result": null, + "round_number": 2, + "tick": 1031 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1031 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d3ea6e01ea1e1e681e9c8731e0a7daa4b37f18102839b41eb997711e9b561078", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 232, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -247, + "y": -247 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 252, + "y": 252 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999970, + "result": null, + "round_number": 2, + "tick": 1032 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1032 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7fcce0ca0009aadd7bf8ec54fbaf106566cb8b9912ee30d22cdf020de2fefa30", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 233, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -243, + "y": -243 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 247, + "y": 247 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999969, + "result": null, + "round_number": 2, + "tick": 1033 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1033 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "22ac69fef5a8c3266056b3e1416b0da8886d6205fb9aed9fa9fe4bde7f660992", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 234, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -238, + "y": -238 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 243, + "y": 243 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999968, + "result": null, + "round_number": 2, + "tick": 1034 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1034 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b916f525634d7a94101c426fe4c449d750ff2bd8d637d6fd82190fe0e270801e", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 235, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -234, + "y": -234 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 239, + "y": 239 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999967, + "result": null, + "round_number": 2, + "tick": 1035 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1035 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "73301e5a8a78da19b803328b8f4bfc4a5bb473b007a1dad08fdf0108dcb453ec", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 236, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -230, + "y": -230 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 235, + "y": 235 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999966, + "result": null, + "round_number": 2, + "tick": 1036 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1036 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0497b27cd4519d1cdac513fd0ebc277ada9621562c72272d303e601745e196de", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 237, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -226, + "y": -226 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 231, + "y": 231 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999965, + "result": null, + "round_number": 2, + "tick": 1037 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1037 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "840a6c21b94f82d9e2e84f2c903be315078eedbe293f82fad8abb4462c0720c3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 238, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -221, + "y": -221 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 227, + "y": 227 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999964, + "result": null, + "round_number": 2, + "tick": 1038 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1038 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "162c7ea3ae2cbd7a0751ef2d80dc5817148fe4f18006cca6552975f262a167dc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 239, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -217, + "y": -217 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 223, + "y": 223 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999963, + "result": null, + "round_number": 2, + "tick": 1039 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1039 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "66152ef9efb68ceaa40eb0130cbdab2ef7ecfe7f6a1a10fc8488eb6c2ccdc5a6", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 240, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -213, + "y": -213 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 219, + "y": 219 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999962, + "result": null, + "round_number": 2, + "tick": 1040 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1040 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ab488d198c48facde06b92d0f3cadbcb56839379469e38323ff5fb3d9306936d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 241, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -209, + "y": -209 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 215, + "y": 215 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999961, + "result": null, + "round_number": 2, + "tick": 1041 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1041 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ed0d796b48fce68c084e2fb7f440d93e4cf0a0a6e12d6fcc791e850390d21abd", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 242, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -204, + "y": -204 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 211, + "y": 211 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999960, + "result": null, + "round_number": 2, + "tick": 1042 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1042 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b758dec67089bc28d1b58a99b8740b7a474a28d4255ae134c6fa511d3b98ffdf", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 243, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -200, + "y": -200 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 207, + "y": 207 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999959, + "result": null, + "round_number": 2, + "tick": 1043 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1043 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "686853b28319db4ad25b0b14a6c98712e494ae80f440b7c8637fc0fc17628965", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 244, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -196, + "y": -196 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 203, + "y": 203 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999958, + "result": null, + "round_number": 2, + "tick": 1044 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1044 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "54a1e557295c3f6148a53a5c552bccc9d06462a2cd8c0caf828eb36b09827415", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 245, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -192, + "y": -192 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 199, + "y": 199 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999957, + "result": null, + "round_number": 2, + "tick": 1045 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1045 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "412b076e5e422fe1d021585ea198ee60ebcaa1ac841dc63738c83861655336a7", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 246, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -187, + "y": -187 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 195, + "y": 195 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999956, + "result": null, + "round_number": 2, + "tick": 1046 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1046 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6a9a266a68e5120f97cf713277593909edda450cd376b5835003958cbf1af20b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 247, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -183, + "y": -183 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 190, + "y": 190 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999955, + "result": null, + "round_number": 2, + "tick": 1047 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1047 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c47632781bf975cb0027f2b343f1debee61034da7a7261acf0fedc681f68f107", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 248, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -179, + "y": -179 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 186, + "y": 186 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999954, + "result": null, + "round_number": 2, + "tick": 1048 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1048 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ad96488024bfa00d3a75c4334e5609d8fa86bdffb2cdfa74b537ed9740327ee3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 249, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -175, + "y": -175 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 182, + "y": 182 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999953, + "result": null, + "round_number": 2, + "tick": 1049 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1049 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2d945211e2e332944e2c51ecbabb170a42b6f09f279baad2016c96f08db1e098", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 250, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -170, + "y": -170 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 178, + "y": 178 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999952, + "result": null, + "round_number": 2, + "tick": 1050 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1050 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "672e5d4cc5544ec9cc4e6e59666e1eb25f075a48d96fa53c1ab1c8dfcfcdcbd7", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 251, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -166, + "y": -166 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 174, + "y": 174 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999951, + "result": null, + "round_number": 2, + "tick": 1051 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1051 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e83e4a50240e89801dbfb89a538994446d9370a21585f21175c463d569618b42", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 252, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -162, + "y": -162 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 170, + "y": 170 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999950, + "result": null, + "round_number": 2, + "tick": 1052 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1052 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "880a2d05608dda2de8d35195da0c4c1dcca3a0d4dc18a38689c02f8ec3ea33b9", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 253, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -158, + "y": -158 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 166, + "y": 166 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999949, + "result": null, + "round_number": 2, + "tick": 1053 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1053 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5f5bf9430238b5288f0d52b6077cef897aefd7514789bd4be12b8506abd0f3e3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 254, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -153, + "y": -153 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 162, + "y": 162 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999948, + "result": null, + "round_number": 2, + "tick": 1054 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1054 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a6a300a089f2504c35fdf8599242937a8498c8cb2aad5579c6907fd027ee05f3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 255, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -149, + "y": -149 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 158, + "y": 158 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999947, + "result": null, + "round_number": 2, + "tick": 1055 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1055 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fdc7933cfe52462b1355f55f53ce062eb4f6a6f6cd00a1e8344a207ce09cbd4f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 256, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -145, + "y": -145 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 154, + "y": 154 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999946, + "result": null, + "round_number": 2, + "tick": 1056 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1056 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "40f0e18373acefa69c345a0d33eddbf4b6e16a979070dc54d22ea79674858b4c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 257, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -141, + "y": -141 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 150, + "y": 150 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999945, + "result": null, + "round_number": 2, + "tick": 1057 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1057 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "a4c35bb52d6980cb6f5e483bae4662f6c0eedd59986c18e8ca8e0e0421ead5aa", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 258, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -136, + "y": -136 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 146, + "y": 146 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999944, + "result": null, + "round_number": 2, + "tick": 1058 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1058 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "45a8a80de5671b3ba650d030523b3b8cd96a452afa17f990ae4b47b3f7f399b3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 259, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -132, + "y": -132 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 142, + "y": 142 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999943, + "result": null, + "round_number": 2, + "tick": 1059 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1059 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "581101eedabff7005e54c66177c2971d88a50a108007a1ee871fa5572d36c082", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 260, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -128, + "y": -128 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 138, + "y": 138 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999942, + "result": null, + "round_number": 2, + "tick": 1060 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1060 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "edbf44276e27ec5dcd6d4b6c6566951bae720e1813a8cbf2f7c2f0112f4252d7", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 261, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -124, + "y": -124 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 133, + "y": 133 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999941, + "result": null, + "round_number": 2, + "tick": 1061 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1061 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8d86b15c155ab48b75e86bcd9724249ec403e6a4cf46f384ea79ecc21b03bff4", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 262, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -120, + "y": -120 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 129, + "y": 129 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999940, + "result": null, + "round_number": 2, + "tick": 1062 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1062 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "45d1a2b1265833d6c2f6f898d42e728c859229a80b115451091663289aa41299", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 263, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -115, + "y": -115 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 125, + "y": 125 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999939, + "result": null, + "round_number": 2, + "tick": 1063 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1063 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9480ca836de3f3fb354c06c5dee155392d636d1245a905991f2b048dfc4e64fe", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 264, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -111, + "y": -111 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 121, + "y": 121 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999938, + "result": null, + "round_number": 2, + "tick": 1064 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1064 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "29e842da7ff19699cbb47d03a23baf2b0cbc6d5d8e752cfa38af6790f9ab6f8a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 265, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -107, + "y": -107 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 117, + "y": 117 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999937, + "result": null, + "round_number": 2, + "tick": 1065 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1065 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4093be5b37862f5a2805f96533664633d1b0d16c11b00a750ccd69d920764555", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 266, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -103, + "y": -103 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 113, + "y": 113 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999936, + "result": null, + "round_number": 2, + "tick": 1066 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1066 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b03dcb70fab6f5bfb0282e240bf2746140da5512c0f0baf6c2ea727a166475fc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 267, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -98, + "y": -98 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 109, + "y": 109 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999935, + "result": null, + "round_number": 2, + "tick": 1067 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1067 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "71d83a1c8b805be5d501f8576611098e245f39138f9c4dcd945d3fd2b298f95c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 268, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -94, + "y": -94 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 105, + "y": 105 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999934, + "result": null, + "round_number": 2, + "tick": 1068 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1068 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "61e57f5e89dc9d009a895cb61a7f1c8e383191f18fb5e26d5cf087349187872c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 269, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -90, + "y": -90 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 101, + "y": 101 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999933, + "result": null, + "round_number": 2, + "tick": 1069 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1069 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "fbad7bdd4f07b521f52d509cd5c5fa8bbaf7e426be745da4230ef6012756dc4c", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 270, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -86, + "y": -86 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 97, + "y": 97 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999932, + "result": null, + "round_number": 2, + "tick": 1070 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1070 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "34c9cfd1949e00e1c671188850077a898c00bcf79e23bf0655461104c75cac97", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 709, + "facing_y": 709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 271, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -81, + "y": -81 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 93, + "y": 93 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999931, + "result": null, + "round_number": 2, + "tick": 1071 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1071 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f2a40b4f735f6af6053449ee2f64d0e7cfa62c8a915658e9920c4645b8cabd38", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 272, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -77, + "y": -77 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 89, + "y": 89 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999930, + "result": null, + "round_number": 2, + "tick": 1072 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1072 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "80d5ebf02fb82e8d9db1a6b84ce9ba64cb7acd8fad1fb384753ae497552db9b9", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 709, + "facing_y": 709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 273, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -73, + "y": -73 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 85, + "y": 85 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999929, + "result": null, + "round_number": 2, + "tick": 1073 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1073 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "5635ebc500c1184b5c07b93fea7e952d93c1e30fcac55a1eacde299d5ef0f7c9", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 274, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -69, + "y": -69 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 81, + "y": 81 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999928, + "result": null, + "round_number": 2, + "tick": 1074 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1074 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3b3213250170cd350d6b38834d8a1f8f78c8bb5c90e52287786acac64572d8e3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 275, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -64, + "y": -64 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 76, + "y": 76 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999927, + "result": null, + "round_number": 2, + "tick": 1075 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1075 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "dff1449eda6c00559e5c978d431ab48856d23f8f53e712785688720223e8db0b", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 710, + "facing_y": 710, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 276, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -60, + "y": -60 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -710, + "facing_y": -710, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 72, + "y": 72 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999926, + "result": null, + "round_number": 2, + "tick": 1076 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1076 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8d025c2179109c8ce8d423efb57b717bec0994cc53ea2f18b76375aae2b75d23", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 709, + "facing_y": 709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 277, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -56, + "y": -56 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 68, + "y": 68 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999925, + "result": null, + "round_number": 2, + "tick": 1077 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1077 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8ac93c121a4879fe203cc3f8ad26c58305b4247e891939cb354f6e0cada162bf", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 278, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -52, + "y": -52 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 64, + "y": 64 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999924, + "result": null, + "round_number": 2, + "tick": 1078 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1078 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "de3ad81c36450930bad7e9ca3d2d43085402c1f1ab43223fc78e6dd517659235", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 279, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -47, + "y": -47 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 60, + "y": 60 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999923, + "result": null, + "round_number": 2, + "tick": 1079 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1079 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1bf746ef81c5d81ad2673b5665538de1678a8449c69d3f81228bfa739d2119be", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 708, + "facing_y": 708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 280, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -43, + "y": -43 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 56, + "y": 56 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999922, + "result": null, + "round_number": 2, + "tick": 1080 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1080 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d20c791e2960556cfdc7831ebdd561383ecab95d316ee794cd20ef30685a84be", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 707, + "facing_y": 707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 281, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -39, + "y": -39 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -707, + "facing_y": -707, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 52, + "y": 52 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999921, + "result": null, + "round_number": 2, + "tick": 1081 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1081 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "53f0186aed1f816bfb644e0e2411e77518b9309b29ed0fef93d44fbddcc926cf", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 710, + "facing_y": 710, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 282, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -35, + "y": -35 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -710, + "facing_y": -710, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 48, + "y": 48 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999920, + "result": null, + "round_number": 2, + "tick": 1082 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1082 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3731905ecacb071dfd5bed182d0b4907280a68e4a578323e238caf30c5233491", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 964, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 709, + "facing_y": 709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 283, + "maximum_stamina": 984, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 984, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 4, + "velocity_y": 4, + "warnings": 0, + "x": -30, + "y": -30 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 985, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 985, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -4, + "velocity_y": -4, + "warnings": 1, + "x": 44, + "y": 44 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999919, + "result": null, + "round_number": 2, + "tick": 1083 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1083 + }, + { + "attack_one": { + "active": 4, + "age": 0, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": false, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "cc5e2eeace4b08675ed6e4177bde5248f1b59b1b68fb7fac3b11be818556c276", + "events": [ + { + "action_id": "one@1084", + "actor_id": "one", + "amount": 0, + "blood": 0, + "detail": "left:hook:body", + "direction": 0, + "event_id": 46, + "kind": "punch_start", + "target_id": null, + "tick": 1084 + } + ], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": null, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 711, + "facing_y": 711, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 858, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 2, + "velocity_y": 2, + "warnings": 0, + "x": -28, + "y": -28 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -711, + "facing_y": -711, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 752, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 302, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 700, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -2, + "velocity_y": -2, + "warnings": 1, + "x": 42, + "y": 42 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999918, + "result": null, + "round_number": 2, + "tick": 1084 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1084 + }, + { + "attack_one": { + "active": 4, + "age": 1, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": false, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "460076fd3788357ca952dcebd86f71f7ced084abdb022ca52bb76808fa38b480", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": null, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 711, + "facing_y": 711, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 859, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 1, + "velocity_y": 1, + "warnings": 0, + "x": -27, + "y": -27 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -714, + "facing_y": -714, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 752, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 304, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 700, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -1, + "velocity_y": -1, + "warnings": 1, + "x": 41, + "y": 41 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999917, + "result": null, + "round_number": 2, + "tick": 1085 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1085 + }, + { + "attack_one": { + "active": 4, + "age": 2, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": false, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "37c190fa78f62adfdf2bd6e1a2ae9213c3d83fd4745961ca9055241906ec3bdf", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": null, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 711, + "facing_y": 711, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 860, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 1, + "velocity_y": 1, + "warnings": 0, + "x": -27, + "y": -27 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -708, + "facing_y": -708, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 752, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 306, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 700, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -1, + "velocity_y": -1, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999916, + "result": null, + "round_number": 2, + "tick": 1086 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1086 + }, + { + "attack_one": { + "active": 4, + "age": 3, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": false, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0505915e46d285df038ce4ae45d3373eb3bcb094bca9550db582796ddeb67b60", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": null, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 711, + "facing_y": 711, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 861, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -712, + "facing_y": -712, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 752, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 308, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 700, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999915, + "result": null, + "round_number": 2, + "tick": 1087 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1087 + }, + { + "attack_one": { + "active": 4, + "age": 4, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": false, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b37d2fa78d9bf0608eede68347fe913de7d71640e31ac6732786cee9a2b1a808", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": null, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 711, + "facing_y": 711, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 862, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 752, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 310, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 700, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999914, + "result": null, + "round_number": 2, + "tick": 1088 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1088 + }, + { + "attack_one": { + "active": 4, + "age": 5, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": false, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "8a4c7864f55f70a237ea6d60fdd9a49f9f510b6d1ee3b3b34d1275b9e306e051", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": null, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 711, + "facing_y": 711, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 863, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 752, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 312, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 700, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999913, + "result": null, + "round_number": 2, + "tick": 1089 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1089 + }, + { + "attack_one": { + "active": 4, + "age": 6, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": false, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6059859ca5cf213bea40b4de9d0020b6afe352b9d3a1fe0108258610b4b7bf6a", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": null, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 711, + "facing_y": 711, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 864, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 752, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 314, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 700, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999912, + "result": null, + "round_number": 2, + "tick": 1090 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1090 + }, + { + "attack_one": { + "active": 4, + "age": 7, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": false, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "173240bb33683a3558646d464b55d329ac805b1a62988088f7bcbe3498ed52ca", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": null, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 711, + "facing_y": 711, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 865, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 752, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 316, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 700, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999911, + "result": null, + "round_number": 2, + "tick": 1091 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1091 + }, + { + "attack_one": { + "active": 4, + "age": 8, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": false, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f359587a7a35628c483120b5ec68d95295ec20b13954358a396b13ee99059e5d", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": null, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 711, + "facing_y": 711, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 866, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 752, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 318, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 700, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999910, + "result": null, + "round_number": 2, + "tick": 1092 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1092 + }, + { + "attack_one": { + "active": 4, + "age": 9, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": false, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2663a59601e8f93056e7923b0b558f46b3a8c2d6e8098de8de2bb3b5694d60c1", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": null, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 711, + "facing_y": 711, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 867, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 965, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 752, + "player_id": "two", + "poise": 508, + "queued_actions": 0, + "stamina": 320, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 700, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999909, + "result": null, + "round_number": 2, + "tick": 1093 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1093 + }, + { + "attack_one": { + "active": 4, + "age": 10, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": true, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "108d3c9a0190a2fe6324e54a863e74e1ddabedfa8dc2713f067db9b666e771c7", + "events": [ + { + "action_id": "one@1084", + "actor_id": "one", + "amount": 84, + "blood": 21, + "detail": "hook:body", + "direction": 1, + "event_id": 47, + "kind": "hit", + "target_id": "two", + "tick": 1094 + }, + { + "action_id": "one@1084", + "actor_id": "one", + "amount": 10, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 48, + "kind": "body_collapse", + "target_id": "two", + "tick": 1094 + } + ], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 711, + "facing_y": 711, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 868, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 441, + "queued_actions": 0, + "stamina": 246, + "stance": "orthodox", + "stunned_ticks": 10, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999908, + "result": null, + "round_number": 2, + "tick": 1094 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1094 + }, + { + "attack_one": { + "active": 4, + "age": 11, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": true, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0680cd0c3e0f98378989691a735fb71f5dccd9bfb23afce18bf09580cebc2ace", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 711, + "facing_y": 711, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 869, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 441, + "queued_actions": 0, + "stamina": 247, + "stance": "orthodox", + "stunned_ticks": 9, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999907, + "result": null, + "round_number": 2, + "tick": 1095 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1095 + }, + { + "attack_one": { + "active": 4, + "age": 12, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": true, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d3b9aa7f32a763aa7165c0292b8cdd1a55197c0b3648296b253ede6c4adc27b0", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 711, + "facing_y": 711, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 870, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 441, + "queued_actions": 0, + "stamina": 248, + "stance": "orthodox", + "stunned_ticks": 8, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999906, + "result": null, + "round_number": 2, + "tick": 1096 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1096 + }, + { + "attack_one": { + "active": 4, + "age": 13, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": true, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1167f8f45562ee7aa680b29ee7d4311b13819c47cfce2d0a31dcc6cd2850b92d", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 711, + "facing_y": 711, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 871, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 441, + "queued_actions": 0, + "stamina": 249, + "stance": "orthodox", + "stunned_ticks": 7, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999905, + "result": null, + "round_number": 2, + "tick": 1097 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1097 + }, + { + "attack_one": { + "active": 4, + "age": 14, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": true, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6abf06f917480c235cf3cb00768a2d1334aa3a21332ade04686130fd26eb7237", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 711, + "facing_y": 711, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 872, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 441, + "queued_actions": 0, + "stamina": 250, + "stance": "orthodox", + "stunned_ticks": 6, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999904, + "result": null, + "round_number": 2, + "tick": 1098 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1098 + }, + { + "attack_one": { + "active": 4, + "age": 15, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": true, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bf42af1ddc9fa547f06f03573c270754aec1fb875670d9154e92de24d567123f", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 709, + "facing_y": 709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 873, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 441, + "queued_actions": 0, + "stamina": 251, + "stance": "orthodox", + "stunned_ticks": 5, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999903, + "result": null, + "round_number": 2, + "tick": 1099 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1099 + }, + { + "attack_one": { + "active": 4, + "age": 16, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": true, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "99a281317cee61cf55235153874f937dbd455abac41f2502b75dc8b5af435bc0", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 709, + "facing_y": 709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 874, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 441, + "queued_actions": 0, + "stamina": 252, + "stance": "orthodox", + "stunned_ticks": 4, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999902, + "result": null, + "round_number": 2, + "tick": 1100 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1100 + }, + { + "attack_one": { + "active": 4, + "age": 17, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": true, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9f0b9bb0d12f57c461957345aece7f3fa4739960a5abab525ba9d429e5fbda57", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 709, + "facing_y": 709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 875, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 441, + "queued_actions": 0, + "stamina": 253, + "stance": "orthodox", + "stunned_ticks": 3, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999901, + "result": null, + "round_number": 2, + "tick": 1101 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1101 + }, + { + "attack_one": { + "active": 4, + "age": 18, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": true, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bc5f45469b0afaaefc8a40fdce59b2d7f76ccf00bd3fa876c9260427d9581381", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 709, + "facing_y": 709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 876, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 441, + "queued_actions": 0, + "stamina": 254, + "stance": "orthodox", + "stunned_ticks": 2, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999900, + "result": null, + "round_number": 2, + "tick": 1102 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1102 + }, + { + "attack_one": { + "active": 4, + "age": 19, + "class": "hook", + "combo_bonus": 0, + "hand": "left", + "power": "power", + "recovery": 15, + "resolved": true, + "startup": 10, + "target": "body" + }, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "c7b9513c85b8a9ce43a133c98eee1c135391e74723b37700f8fc81ac2a959589", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 709, + "facing_y": 709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 8, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 1, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 441, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "fight", + "phase_ticks_remaining": 999899, + "result": null, + "round_number": 2, + "tick": 1103 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1103 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "95970bb92a41d9ae0972b9e6a37b6cacc64557fa881e9a8256109f4ae77d35bc", + "events": [ + { + "action_id": "one@1084", + "actor_id": "one", + "amount": 2, + "blood": 0, + "detail": "body", + "direction": 0, + "event_id": 49, + "kind": "knockdown", + "target_id": "two", + "tick": 1104 + } + ], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 709, + "facing_y": 709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 0, + "x": -26, + "y": -26 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 300, + "result": null, + "round_number": 2, + "tick": 1104 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1104 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "14ebc3a03c2aad2e4a3ab1dc5d210071728e5149dcde140cc02f84e2e65fd07a", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 739, + "facing_y": 673, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -28, + "y": -22 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 299, + "result": null, + "round_number": 2, + "tick": 1105 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1105 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e9047af585b7a79fa1a4fc42e510b36e7ec892a10869a37b8bd873b251967717", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 774, + "facing_y": 634, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -32, + "y": -19 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 298, + "result": null, + "round_number": 2, + "tick": 1106 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1106 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "917abefa75d47272742dcbcb7e95083250a44200719b00555e3e2443acc9be60", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 806, + "facing_y": 591, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -35, + "y": -15 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 297, + "result": null, + "round_number": 2, + "tick": 1107 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1107 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "62a33feba859113ad70d31f3776a7cdcd8ebc782135805de7b546ad85e160539", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 838, + "facing_y": 548, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -38, + "y": -11 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 296, + "result": null, + "round_number": 2, + "tick": 1108 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1108 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "2aa8001d251254c10d08f7a1be5451ed717bef6e8f32c0d1626dde8e45d64cef", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 872, + "facing_y": 500, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -42, + "y": -7 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 295, + "result": null, + "round_number": 2, + "tick": 1109 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1109 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "6eb8270e6a11c7721ef4a14869693dc8ea7d06d717b9a7809e372c66dc47aa2b", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 894, + "facing_y": 463, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -45, + "y": -4 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 294, + "result": null, + "round_number": 2, + "tick": 1110 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1110 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b3ed07a1a73cb81534d5596793cbdfcb8031bbfc8593398b0d65e75663e20af1", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 916, + "facing_y": 416, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -48, + "y": 0 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 293, + "result": null, + "round_number": 2, + "tick": 1111 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1111 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0dd41850ecb6d35648652ed297ace3bf993cb8fdddbbb1e9c15079621090ed4a", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 938, + "facing_y": 371, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -51, + "y": 4 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 292, + "result": null, + "round_number": 2, + "tick": 1112 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1112 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ac5da59f7219b05452c164c2a67000b77dd1a7afc9a655933e2ccaf4d6e2f840", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 950, + "facing_y": 320, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -55, + "y": 8 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 291, + "result": null, + "round_number": 2, + "tick": 1113 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1113 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0219eb05eca32d7e56d7b1485e6cd4d6d3dbd4af15220ed79b347016a8312e48", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 970, + "facing_y": 277, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -58, + "y": 12 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 290, + "result": null, + "round_number": 2, + "tick": 1114 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1114 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "08c996222ae8282699bed9606349e14143c70e5b0f87bfd4aa9b5774b0d00508", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 971, + "facing_y": 240, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -61, + "y": 15 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 289, + "result": null, + "round_number": 2, + "tick": 1115 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1115 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "822a814790575baffb7e83a0decd67dbc1561fb736567ab8f895a67237185da4", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 981, + "facing_y": 196, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -65, + "y": 19 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 288, + "result": null, + "round_number": 2, + "tick": 1116 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1116 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "f8366ff320d5fa34214d2ce8c56a5fc0ac10cc8d5ffd61a8abcc31fa7f96568a", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 990, + "facing_y": 155, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -68, + "y": 23 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 287, + "result": null, + "round_number": 2, + "tick": 1117 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1117 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "4ffeebc7ca88830e91f95f800dc961dd2cfb1e19c37e523243176c7aeaed1d1d", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": 117, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -71, + "y": 27 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 286, + "result": null, + "round_number": 2, + "tick": 1118 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1118 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0778fa8830accd295533186b763d84724ba452e0e6d94389821b7572733f953a", + "events": [], + "fighters": [ + { + "action": "hook", + "action_active_ticks": 4, + "action_contact_tick": 1094, + "action_hand": "left", + "action_id": "one@1084", + "action_key": "hook:left:body:power", + "action_power": "power", + "action_recovery_ticks": 15, + "action_start_tick": 1084, + "action_startup_ticks": 10, + "action_target": "body", + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": 87, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -74, + "y": 30 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 285, + "result": null, + "round_number": 2, + "tick": 1119 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1119 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d9948b7bcd502f57cc66e2d275d5391f91d68387e860323e8bf7126def6ccedf", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": 50, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -78, + "y": 34 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 284, + "result": null, + "round_number": 2, + "tick": 1120 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1120 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3c3e3e4d70d4d62927693dac10794837b6c71a885ca25233ba8a4e3a92fe1d40", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": 16, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -81, + "y": 38 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 283, + "result": null, + "round_number": 2, + "tick": 1121 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1121 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "47ea6778b32c9c8bcde63cbb5596331efc7348bf2d6d32808a8a264f147d3a73", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -16, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -84, + "y": 42 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 282, + "result": null, + "round_number": 2, + "tick": 1122 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1122 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "1ecfb1234cadcfe6189ee59481a430e191e5dc2f82c1427d83b35b13155ba953", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -46, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -88, + "y": 46 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 281, + "result": null, + "round_number": 2, + "tick": 1123 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1123 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7f25e81755e48ec345bb30ad3d980f2f56cb11e0d1f67073e710ac46c4ccc4a5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -68, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -91, + "y": 49 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 280, + "result": null, + "round_number": 2, + "tick": 1124 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1124 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "b86c1a42a0c39ef0e4e03b56f38437d88dfeb2bdeb62eea615901d6ab4abe0cc", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 1000, + "facing_y": -97, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -94, + "y": 53 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 279, + "result": null, + "round_number": 2, + "tick": 1125 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1125 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3f4828d0d7631621f3d62a4f63954c646c1e6f8d089c3d11692666ec49c8c06d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 992, + "facing_y": -123, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -97, + "y": 57 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 278, + "result": null, + "round_number": 2, + "tick": 1126 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1126 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ae16e99e2657b99262fd4962419696a179ae1d4d1fd0ba5914b84bc0b6349490", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 992, + "facing_y": -147, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -101, + "y": 61 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 277, + "result": null, + "round_number": 2, + "tick": 1127 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1127 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "062e6af17185a0bc99b7a24d37f0fd88ead11a997d72305f4a062c33b98a009a", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 993, + "facing_y": -165, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -104, + "y": 64 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 276, + "result": null, + "round_number": 2, + "tick": 1128 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1128 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "ffae473ff5d2acb0c00bf11a73381ac1a034e798d5cda38a703943d8b4f49d8f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 986, + "facing_y": -187, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -107, + "y": 68 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 275, + "result": null, + "round_number": 2, + "tick": 1129 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1129 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "42fc4c7d1a9c71eb84db03792b4fd42c27be35706d2441d68f4933c55bf8ef54", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 980, + "facing_y": -209, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -110, + "y": 72 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 274, + "result": null, + "round_number": 2, + "tick": 1130 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1130 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "074bfe681de9e6cfa7feff2041231435f167f64e953dd22e3808a884fb6a63f7", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 974, + "facing_y": -227, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -114, + "y": 76 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 273, + "result": null, + "round_number": 2, + "tick": 1131 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1131 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3afa53a53343ac8fa5a1d485e99aad8f54dd5624441bddba27f8ff524da1dbe3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 969, + "facing_y": -246, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -117, + "y": 80 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 272, + "result": null, + "round_number": 2, + "tick": 1132 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1132 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "38de922bc12220257bd05bfab4eb510e20a81a6d2ebb1220ab03e56ac405c6e6", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 969, + "facing_y": -260, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -120, + "y": 83 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 0, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 271, + "result": null, + "round_number": 2, + "tick": 1133 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1133 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "603294b838f45059777524848670baf110c8b939c3afb6d9a498e97c04122342", + "events": [ + { + "action_id": null, + "actor_id": null, + "amount": 1, + "blood": 0, + "detail": "", + "direction": 0, + "event_id": 50, + "kind": "count", + "target_id": "two", + "tick": 1134 + } + ], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 964, + "facing_y": -276, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -124, + "y": 87 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 270, + "result": null, + "round_number": 2, + "tick": 1134 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1134 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "9ccc2f080bc1871c9220ca8945b10007f306a963d2a0ae8eff8900b831cdee44", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 959, + "facing_y": -293, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -127, + "y": 91 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 269, + "result": null, + "round_number": 2, + "tick": 1135 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1135 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bccdc4e7c967849318708609bf229fcf04ea25b9ee2b046084d48a7cbb3622f5", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 955, + "facing_y": -308, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -130, + "y": 95 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 268, + "result": null, + "round_number": 2, + "tick": 1136 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1136 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "64a87c61a117d21ccf549551b0ebf89ac316dadf5f5fdad6725faf28d3b182a9", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 950, + "facing_y": -318, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -133, + "y": 98 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 267, + "result": null, + "round_number": 2, + "tick": 1137 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1137 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "17c072d33e4d74dc82456cb52e0bbbe8034c6c99ed96fa9889ce3a7fdf8399d3", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 946, + "facing_y": -331, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -137, + "y": 102 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 266, + "result": null, + "round_number": 2, + "tick": 1138 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1138 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "205d673eb764cb68eb769430440e8618450d20eb64bfb0e609a259ffcb8205f0", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 942, + "facing_y": -345, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -140, + "y": 106 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 265, + "result": null, + "round_number": 2, + "tick": 1139 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1139 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "bdea5de3aa5b8d46cf925aa61b62641f96cade0c22fcb49f580601da9eac2ccf", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 938, + "facing_y": -358, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -143, + "y": 110 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 264, + "result": null, + "round_number": 2, + "tick": 1140 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1140 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "7be16b84dbdb0ad3df75612289e4d989153ed986dbeea87be2feb80088df2320", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 930, + "facing_y": -368, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -147, + "y": 114 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 263, + "result": null, + "round_number": 2, + "tick": 1141 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1141 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "d6ec42f8cdf6ed4fd2d920f524f72443d5971615843d8aed47dd7b1f0c95f44f", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 926, + "facing_y": -375, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -150, + "y": 117 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 262, + "result": null, + "round_number": 2, + "tick": 1142 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1142 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "e50340c9d1161f80b1f8fb9e49cc51fcbb94d12bac00444071de7e113cee2fdb", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 923, + "facing_y": -387, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -153, + "y": 121 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 261, + "result": null, + "round_number": 2, + "tick": 1143 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1143 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "0453168b0a3316c2dd173dffd9b26585d96c85ca447047a6f2ca65cb0e36c247", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 920, + "facing_y": -399, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -156, + "y": 125 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 260, + "result": null, + "round_number": 2, + "tick": 1144 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1144 + }, + { + "attack_one": null, + "attack_two": null, + "snapshot": { + "payload": { + "checksum": "3b25b2f0392cd10db3ae298accf8f9f310bd5ed5c4e3d6dffb36018a3421086d", + "events": [], + "fighters": [ + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 954, + "corner_choice": "cut", + "deductions": 0, + "defense": "none", + "facing": 1, + "facing_x": 917, + "facing_y": -408, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 34, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": false, + "is_foul_recovery_target": false, + "knockdowns": 0, + "last_input_sequence": 284, + "maximum_stamina": 980, + "player_id": "one", + "poise": 600, + "queued_actions": 0, + "stamina": 877, + "stance": "orthodox", + "stunned_ticks": 0, + "style": "boxer", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 0, + "head": 0, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": -3, + "velocity_y": 4, + "warnings": 0, + "x": -160, + "y": 129 + }, + { + "action": null, + "action_active_ticks": 0, + "action_contact_tick": null, + "action_hand": null, + "action_id": null, + "action_key": null, + "action_power": null, + "action_recovery_ticks": 0, + "action_start_tick": 0, + "action_startup_ticks": 0, + "action_target": null, + "clinch_startup_ticks": 0, + "clinch_ticks": 0, + "conditioning": 881, + "corner_choice": "swelling", + "deductions": 0, + "defense": "none", + "facing": -1, + "facing_x": -709, + "facing_y": -709, + "get_up_count": 1, + "get_up_meter": 0, + "get_up_prompt": null, + "get_up_required": 0, + "get_up_window_end_tick": 0, + "get_up_window_start_tick": 0, + "guard": 700, + "is_downed": true, + "is_foul_recovery_target": false, + "knockdowns": 2, + "last_input_sequence": -1, + "maximum_stamina": 686, + "player_id": "two", + "poise": 0, + "queued_actions": 0, + "stamina": 255, + "stance": "orthodox", + "stunned_ticks": 1, + "style": "slugger", + "taunt_ticks": 0, + "trauma": { + "bleeding": 0, + "body": 784, + "head": 307, + "left_cut": 0, + "left_eye": 0, + "right_cut": 0, + "right_eye": 0, + "swelling": 0 + }, + "velocity_x": 0, + "velocity_y": 0, + "warnings": 1, + "x": 40, + "y": 40 + } + ], + "phase": "knockdown", + "phase_ticks_remaining": 259, + "result": null, + "round_number": 2, + "tick": 1145 + }, + "type": "snapshot", + "version": 4 + }, + "tick": 1145 } ] } diff --git a/web/hands/scripts/scan-build.mjs b/web/hands/scripts/scan-build.mjs index 7c49433f..d99a5e7f 100644 --- a/web/hands/scripts/scan-build.mjs +++ b/web/hands/scripts/scan-build.mjs @@ -19,6 +19,8 @@ const reviewedExternalUrls = new Set([ "https://staging.discord.co", "http://localhost:3333", "https://pax.discord.com", + // Discord's image host, for the players' own avatars. Discord's Activity proxy allows it in img-src. + "https://cdn.discordapp.com/avatars/", // Texel Boxer source and CC BY 4.0 license attribution shown in-app. "https://sketchfab.com/3d-models/boxer-84767168720948b38728ff78ee6f6090", "https://creativecommons.org/licenses/by/4.0/", diff --git a/web/hands/src/announcer.test.ts b/web/hands/src/announcer.test.ts new file mode 100644 index 00000000..2b02c142 --- /dev/null +++ b/web/hands/src/announcer.test.ts @@ -0,0 +1,220 @@ +import { AnnouncerVoice, pickVoice } from "./announcer"; +import type { Settings } from "./settings"; + +class FakeUtterance { + volume = 1; + rate = 1; + pitch = 1; + lang = ""; + voice: SpeechSynthesisVoice | null = null; + onend: (() => void) | null = null; + onerror: (() => void) | null = null; + constructor(readonly text: string) {} +} + +class FakeSynthesis { + readonly spoken: FakeUtterance[] = []; + cancels = 0; + voices: SpeechSynthesisVoice[] = []; + private readonly listeners = new Set<() => void>(); + addEventListener(type: string, listener: () => void): void { + if (type === "voiceschanged") this.listeners.add(listener); + } + removeEventListener(type: string, listener: () => void): void { + if (type === "voiceschanged") this.listeners.delete(listener); + } + /** The browser (re)lists its voices, as Chrome does a moment after the page loads. */ + listVoices(voices: SpeechSynthesisVoice[]): void { + this.voices = voices; + for (const listener of this.listeners) listener(); + } + speak(utterance: SpeechSynthesisUtterance): void { + this.spoken.push(utterance as unknown as FakeUtterance); + } + cancel(): void { + this.cancels += 1; + } + getVoices(): SpeechSynthesisVoice[] { + return this.voices; + } + finishLast(): void { + this.spoken.at(-1)!.onend?.(); + } +} + +const voice = (name: string, lang: string, localService: boolean, isDefault = false): SpeechSynthesisVoice => ({ name, lang, localService, default: isDefault, voiceURI: name }) as SpeechSynthesisVoice; +const deviceVoice = voice("Microsoft David", "en-US", true, true); + +describe("the announcer's voice", () => { + let settings: Settings; + let synthesis: FakeSynthesis; + const make = (): AnnouncerVoice => new AnnouncerVoice(() => settings, synthesis, FakeUtterance as unknown as new (text: string) => SpeechSynthesisUtterance); + + beforeEach(() => { + settings = { volume: 0.6, haptics: true, reducedMotion: false, blood: "full", camera: "broadcast", commentary: true, announcer: true }; + synthesis = new FakeSynthesis(); + synthesis.voices = [deviceVoice]; + }); + + it("speaks one line at a time, in order, at the player's volume", () => { + const announcer = make(); + announcer.speak(["In the blue corner... Azure Vector!", "And in the red corner... Crimson Geometry!"]); + expect(synthesis.spoken.map((utterance) => utterance.text)).toEqual(["In the blue corner... Azure Vector!"]); + expect(synthesis.spoken[0]!.volume).toBe(0.6); + announcer.speak(["Ladies and gentlemen, we go to the scorecards."]); + expect(synthesis.spoken).toHaveLength(1); + synthesis.finishLast(); + expect(synthesis.spoken.map((utterance) => utterance.text)).toEqual(["In the blue corner... Azure Vector!", "And in the red corner... Crimson Geometry!"]); + synthesis.finishLast(); + synthesis.finishLast(); + expect(synthesis.spoken).toHaveLength(3); + }); + + it("carries on past a line the browser refuses to speak", () => { + const announcer = make(); + announcer.speak(["one", "two"]); + synthesis.spoken[0]!.onerror?.(); + expect(synthesis.spoken.map((utterance) => utterance.text)).toEqual(["one", "two"]); + }); + + it("stays quiet when turned off or muted", () => { + settings = { ...settings, announcer: false }; + make().speak(["Your winner..."]); + settings = { ...settings, announcer: true, volume: 0 }; + make().speak(["Your winner..."]); + expect(synthesis.spoken).toHaveLength(0); + }); + + it("drops the rest of the script when it is turned off mid-way", () => { + const announcer = make(); + announcer.speak(["one", "two"]); + settings = { ...settings, announcer: false }; + synthesis.finishLast(); + expect(synthesis.spoken).toHaveLength(1); + }); + + it("stops the line being read when the player mutes, and keeps it when the volume only changes", () => { + const announcer = make(); + announcer.speak(["In the blue corner...", "And in the red corner..."]); + settings = { ...settings, volume: 0.3 }; + announcer.settingsChanged(); + expect(synthesis.cancels).toBe(0); + settings = { ...settings, volume: 0 }; + announcer.settingsChanged(); + expect(synthesis.cancels).toBe(1); + synthesis.finishLast(); + expect(synthesis.spoken).toHaveLength(1); + }); + + it("is cancelled for a rematch and after teardown says nothing", () => { + const announcer = make(); + announcer.speak(["one", "two"]); + announcer.cancel(); + expect(synthesis.cancels).toBe(1); + synthesis.finishLast(); + expect(synthesis.spoken).toHaveLength(1); + announcer.destroy(); + announcer.speak(["three"]); + expect(synthesis.spoken).toHaveLength(1); + }); + + it("says nothing while the Activity is hidden and stops the line it was reading when it hides", () => { + let hidden = true; + Object.defineProperty(document, "hidden", { configurable: true, get: () => hidden }); + try { + const announcer = make(); + // The final arrives with the player in another app: the scorecards line is not read over it. + announcer.speak(["Ladies and gentlemen, we go to the scorecards."]); + expect(synthesis.spoken).toHaveLength(0); + hidden = false; + announcer.speak(["In the blue corner, Azure Vector!", "And in the red corner, Crimson Geometry!"]); + expect(synthesis.spoken).toHaveLength(1); + hidden = true; + document.dispatchEvent(new Event("visibilitychange")); + expect(synthesis.cancels).toBe(1); + synthesis.finishLast(); + expect(synthesis.spoken).toHaveLength(1); + announcer.destroy(); + hidden = false; + document.dispatchEvent(new Event("visibilitychange")); + expect(synthesis.cancels).toBe(1); + } finally { + Reflect.deleteProperty(document, "hidden"); + } + }); + + it("lets the line being read finish its name, briefly, when its moment ends, and drops the rest", () => { + vi.useFakeTimers(); + const announcer = make(); + announcer.speak(["In the blue corner, Azure Vector!", "And in the red corner, Crimson Geometry!"]); + announcer.finishLine(1_500); + expect(synthesis.cancels).toBe(0); + synthesis.finishLast(); + expect(synthesis.spoken).toHaveLength(1); + vi.advanceTimersByTime(2_000); + expect(synthesis.cancels).toBe(0); + // A voice too slow to finish in time is cut off once the grace is over. + announcer.speak(["And in the red corner, Crimson Geometry!"]); + announcer.finishLine(1_500); + vi.advanceTimersByTime(1_499); + expect(synthesis.cancels).toBe(0); + vi.advanceTimersByTime(1); + expect(synthesis.cancels).toBe(1); + announcer.finishLine(1_500); + announcer.speak(["Ladies and gentlemen, we go to the scorecards."]); + vi.advanceTimersByTime(5_000); + expect(synthesis.cancels).toBe(1); + vi.useRealTimers(); + }); + + it("does nothing where the browser cannot speak", () => { + const announcer = new AnnouncerVoice(() => settings, null, null); + expect(announcer.supported).toBe(false); + expect(() => announcer.speak(["hello"])).not.toThrow(); + }); + + it("prefers an English voice on the device", () => { + const remote = voice("Remote English", "en-US", false); + const local = voice("Local English", "en-GB", true); + const french = voice("Local French", "fr-FR", true, true); + expect(pickVoice([french, remote, local])).toBe(local); + expect(pickVoice([french, remote])).toBeNull(); + expect(pickVoice([french])).toBeNull(); + synthesis.voices = [french, local]; + make().speak(["Azure Vector!"]); + expect(synthesis.spoken[0]!.voice).toBe(local); + expect(synthesis.spoken[0]!.lang).toBe("en-GB"); + }); + + it("never reads the fighters' names with a voice that speaks from a vendor's servers", () => { + // Chrome's "Google US English" synthesizes online: the names would leave the Activity. + synthesis.voices = [voice("Google US English", "en-US", false, true), voice("Local French", "fr-FR", true)]; + const announcer = make(); + expect(announcer.hasVoice).toBe(false); + announcer.speak(["In the blue corner, Azure Vector!"]); + expect(synthesis.spoken).toHaveLength(0); + }); + + it("waits for voices the browser lists late, and reads with the device's own once they come", () => { + synthesis.voices = []; + const changed = vi.fn(); + const announcer = make(); + announcer.onVoicesChanged = changed; + // Nothing is listed yet: Settings still offers the voice, but no line goes to the browser's own choice. + expect(announcer.hasVoice).toBe(true); + announcer.speak(["In the blue corner, Azure Vector!"]); + expect(synthesis.spoken).toHaveLength(0); + synthesis.listVoices([voice("Google UK English Male", "en-GB", false), deviceVoice]); + expect(changed).toHaveBeenCalledOnce(); + announcer.speak(["In the blue corner, Azure Vector!"]); + expect(synthesis.spoken[0]!.voice).toBe(deviceVoice); + // A device whose own voice goes away stops reading, and Settings is told. + synthesis.listVoices([voice("Google UK English Male", "en-GB", false)]); + expect(synthesis.cancels).toBe(1); + expect(announcer.hasVoice).toBe(false); + expect(changed).toHaveBeenCalledTimes(2); + announcer.destroy(); + synthesis.listVoices([deviceVoice]); + expect(changed).toHaveBeenCalledTimes(2); + }); +}); diff --git a/web/hands/src/announcer.ts b/web/hands/src/announcer.ts new file mode 100644 index 00000000..66ba3f4e --- /dev/null +++ b/web/hands/src/announcer.ts @@ -0,0 +1,162 @@ +import type { Settings } from "./settings"; + +type Synthesis = Pick & { + addEventListener?(type: "voiceschanged", listener: () => void): void; + removeEventListener?(type: "voiceschanged", listener: () => void): void; +}; +type UtteranceConstructor = new (text: string) => SpeechSynthesisUtterance; + +const MAX_QUEUE = 6; + +/** Whether this browser can speak. Discord's mobile webviews may not. */ +export function speechAvailable(): boolean { + return typeof window !== "undefined" && typeof window.speechSynthesis === "object" && window.speechSynthesis !== null && typeof window.SpeechSynthesisUtterance === "function"; +} + +/** + * An English voice that runs on the device, the default one if it qualifies. A voice that + * synthesizes on a vendor's servers would send the players' names off the device, so the + * announcer goes without rather than use one. + */ +export function pickVoice(voices: readonly SpeechSynthesisVoice[]): SpeechSynthesisVoice | null { + const local = voices.filter((voice) => voice.localService && voice.lang.toLowerCase().startsWith("en")); + return local.find((voice) => voice.default) ?? local[0] ?? null; +} + +/** Reads the ring announcements aloud, one at a time, at the player's volume. */ +export class AnnouncerVoice { + private readonly queue: string[] = []; + private speaking: SpeechSynthesisUtterance | null = null; + private destroyed = false; + /** The device's own English voice it reads with, once the device has listed one. */ + private voice: SpeechSynthesisVoice | null = null; + /** Chrome lists no voices until a moment after the page loads. */ + private voicesListed = false; + /** Called when the device's voices change, so Settings can offer the announcer or say why it cannot. */ + onVoicesChanged: (() => void) | null = null; + /** Cuts off a line allowed to run on past its moment (finishLine). */ + private cutoff: number | null = null; + + private readonly voicesChanged = (): void => { + this.listVoices(); + if (this.voice === null) this.cancel(); + this.onVoicesChanged?.(); + }; + + /** The rest of the game's sound is suspended while the Activity is hidden, and the voice goes quiet with it. */ + private readonly visibility = (): void => { + if (document.hidden) this.cancel(); + }; + + constructor( + private readonly settings: () => Settings, + private readonly synthesis: Synthesis | null = speechAvailable() ? window.speechSynthesis : null, + private readonly Utterance: UtteranceConstructor | null = speechAvailable() ? window.SpeechSynthesisUtterance : null, + ) { + // Asking for the voices is also what starts Chrome loading them. + this.listVoices(); + synthesis?.addEventListener?.("voiceschanged", this.voicesChanged); + document.addEventListener("visibilitychange", this.visibility); + } + + get supported(): boolean { + return this.synthesis !== null && this.Utterance !== null; + } + + /** False once the device has listed its voices and none of them is an English voice of its own. */ + get hasVoice(): boolean { + return this.supported && (this.voice !== null || !this.voicesListed); + } + + speak(lines: readonly string[]): void { + if (!this.supported || this.destroyed || document.hidden) return; + const settings = this.settings(); + if (!settings.announcer || settings.volume <= 0) return; + // A browser that never says its voices changed may still have listed them since. + if (this.voice === null) this.listVoices(); + if (this.voice === null) return; + this.queue.push(...lines); + if (this.queue.length > MAX_QUEUE) this.queue.splice(0, this.queue.length - MAX_QUEUE); + if (this.speaking === null) this.next(); + } + + /** A line already being read keeps the volume it started with, so muting, or switching the voice off, stops it. */ + settingsChanged(): void { + const settings = this.settings(); + if (!settings.announcer || settings.volume <= 0) this.cancel(); + } + + /** Forgets the lines still to come and lets the one being read finish, for `graceMs` at most, so a name is not cut in half. */ + finishLine(graceMs: number): void { + this.queue.length = 0; + this.clearCutoff(); + const line = this.speaking; + if (line === null) return; + this.cutoff = window.setTimeout(() => { + this.cutoff = null; + if (this.speaking === line) this.cancel(); + }, graceMs); + } + + /** Stops the current announcement and forgets the rest. */ + cancel(): void { + this.queue.length = 0; + this.clearCutoff(); + const speaking = this.speaking; + this.speaking = null; + if (speaking !== null) { + speaking.onend = null; + speaking.onerror = null; + this.synthesis?.cancel(); + } + } + + destroy(): void { + this.destroyed = true; + this.synthesis?.removeEventListener?.("voiceschanged", this.voicesChanged); + document.removeEventListener("visibilitychange", this.visibility); + this.onVoicesChanged = null; + this.cancel(); + } + + private clearCutoff(): void { + if (this.cutoff !== null) window.clearTimeout(this.cutoff); + this.cutoff = null; + } + + private listVoices(): void { + const voices = this.synthesis?.getVoices() ?? []; + if (voices.length > 0) this.voicesListed = true; + this.voice = pickVoice(voices); + } + + private next(): void { + const synthesis = this.synthesis; + const Utterance = this.Utterance; + const voice = this.voice; + const text = this.queue.shift(); + if (synthesis === null || Utterance === null || text === undefined) return; + const settings = this.settings(); + if (!settings.announcer || settings.volume <= 0 || voice === null || document.hidden) { + this.queue.length = 0; + return; + } + const utterance = new Utterance(text); + utterance.volume = Math.min(1, settings.volume); + utterance.rate = 0.96; + utterance.pitch = 0.82; + utterance.voice = voice; + utterance.lang = voice.lang; + // A blocked or failed line ends like a finished one so the queue never stalls. + const finished = (): void => { + if (this.speaking !== utterance) return; + this.speaking = null; + this.clearCutoff(); + this.next(); + }; + utterance.onend = finished; + utterance.onerror = finished; + this.speaking = utterance; + synthesis.speak(utterance); + } +} diff --git a/web/hands/src/api.test.ts b/web/hands/src/api.test.ts index 2f9a3b34..1832fad7 100644 --- a/web/hands/src/api.test.ts +++ b/web/hands/src/api.test.ts @@ -1,4 +1,5 @@ import { bootstrap, ClientError, exchangeToken } from "./api"; +import { PROTOCOL_VERSION } from "./types"; const rawResponse = (body: string): Response => new Response(body, { status: 200, @@ -8,7 +9,7 @@ const rawResponse = (body: string): Response => new Response(body, { describe("strict same-origin HTTP response parsing", () => { it("posts the launch instance so browsers send the same-origin Origin header", async () => { history.replaceState({}, "", "/"); - const fetchMock = vi.fn(async () => rawResponse('{"client_id":"123","state":"s","protocol":3,"simulation":{"tick_rate":30,"ring_half_width":500,"ring_half_height":500}}')); + const fetchMock = vi.fn(async () => rawResponse(`{"client_id":"123","state":"s","protocol":${PROTOCOL_VERSION},"simulation":{"tick_rate":30,"ring_half_width":500,"ring_half_height":500}}`)); vi.stubGlobal("fetch", fetchMock); await bootstrap("launch"); expect(fetchMock).toHaveBeenCalledWith( @@ -23,15 +24,15 @@ describe("strict same-origin HTTP response parsing", () => { it("rejects top-level duplicate bootstrap keys before schema decoding", async () => { history.replaceState({}, "", "/"); - vi.stubGlobal("fetch", vi.fn(async () => rawResponse('{"client_id":"123","client_id":"forged","state":"s","protocol":3,"simulation":{"tick_rate":30,"ring_half_width":500,"ring_half_height":500}}'))); + vi.stubGlobal("fetch", vi.fn(async () => rawResponse(`{"client_id":"123","client_id":"forged","state":"s","protocol":${PROTOCOL_VERSION},"simulation":{"tick_rate":30,"ring_half_width":500,"ring_half_height":500}}`))); await expect(bootstrap("launch")).rejects.toEqual(new ClientError("bootstrap_failed")); }); it("asks for a reload when the server speaks another protocol, and only then", async () => { history.replaceState({}, "", "/"); - vi.stubGlobal("fetch", vi.fn(async () => rawResponse('{"client_id":"123","state":"s","protocol":4,"simulation":{"tick_rate":30,"ring_half_width":500,"ring_half_height":500},"build":"next"}'))); + vi.stubGlobal("fetch", vi.fn(async () => rawResponse(`{"client_id":"123","state":"s","protocol":${PROTOCOL_VERSION + 1},"simulation":{"tick_rate":30,"ring_half_width":500,"ring_half_height":500},"build":"next"}`))); await expect(bootstrap("launch")).rejects.toEqual(new ClientError("client_outdated", true)); - vi.stubGlobal("fetch", vi.fn(async () => rawResponse('{"client_id":"123","state":"s","protocol":3,"simulation":{"tick_rate":30,"ring_half_width":500,"ring_half_height":500},"extra":1}'))); + vi.stubGlobal("fetch", vi.fn(async () => rawResponse(`{"client_id":"123","state":"s","protocol":${PROTOCOL_VERSION},"simulation":{"tick_rate":30,"ring_half_width":500,"ring_half_height":500},"extra":1}`))); await expect(bootstrap("launch")).rejects.toEqual(new ClientError("invalid_bootstrap")); }); diff --git a/web/hands/src/app.test.ts b/web/hands/src/app.test.ts index 16ed5848..4fbf5b9e 100644 --- a/web/hands/src/app.test.ts +++ b/web/hands/src/app.test.ts @@ -1,5 +1,5 @@ import type { NetworkCallbacks } from "./network"; -import type { EngineSnapshot, ServerMessage } from "./types"; +import { PROTOCOL_VERSION, type EngineSnapshot, type ServerMessage } from "./types"; import { fighter } from "./test/fixtures"; const mocks = vi.hoisted(() => { @@ -8,7 +8,7 @@ const mocks = vi.hoisted(() => { sessionDestroy, authorize: vi.fn(async () => ({ sdk: {}, - bootstrap: { client_id: "123", state: "state", protocol: 3, simulation: { tick_rate: 20, ring_half_width: 500, ring_half_height: 500 } }, + bootstrap: { client_id: "123", state: "state", protocol: PROTOCOL_VERSION, simulation: { tick_rate: 20, ring_half_width: 500, ring_half_height: 500 } }, player: { id: "one", name: "One", avatar: null, rating: 1500 }, takeTicket: () => "ticket", destroy: sessionDestroy, @@ -22,6 +22,13 @@ const mocks = vi.hoisted(() => { callbacks: null as NetworkCallbacks | null, rendererResyncs: 0, resultVisible: true, + cornerPicks: [] as string[], + styleChoices: [] as string[], + cpuRequests: [] as string[], + cpuAccepted: true, + renderers: [] as Array<{ onAnnouncement?: ((lines: readonly string[]) => void) | null; onCrowdCue?: ((cue: "chant") => void) | null; onRocked?: ((level: number, tick: number) => void) | null }>, + input: null as (() => { moveX: number; moveY: number }) | null, + viewForward: null as { x: number; z: number } | null, }; }); vi.mock("./discord", () => ({ @@ -32,22 +39,30 @@ vi.mock("./discord", () => ({ })); vi.mock("./network", () => ({ NetworkController: class { - constructor(_ticket: string, _input: unknown, callbacks: NetworkCallbacks) { mocks.callbacks = callbacks; } + constructor(_ticket: string, input: unknown, callbacks: NetworkCallbacks) { mocks.callbacks = callbacks; mocks.input = input as () => { moveX: number; moveY: number }; } start(): void {} setActive(active: boolean): void { mocks.networkSetActive(active); } notifyAction(): void {} + sendCornerChoice(kind: string): boolean { mocks.cornerPicks.push(kind); return true; } + chooseStyle(style: string, ready: boolean): boolean { mocks.styleChoices.push(`${style}:${ready}`); return true; } + requestCpu(level: string): boolean { mocks.cpuRequests.push(level); return mocks.cpuAccepted; } dispose(): void { mocks.networkDispose(); } }, })); vi.mock("./render/renderer", () => ({ FightRenderer: class { private readonly pushes: number[] = []; - constructor() { mocks.rendererPushes.push(this.pushes); } + onAnnouncement: ((lines: readonly string[]) => void) | null = null; + onCrowdCue: ((cue: "chant") => void) | null = null; + onRocked: ((level: number, tick: number) => void) | null = null; + constructor() { mocks.rendererPushes.push(this.pushes); mocks.renderers.push(this); } setPlayers(): void {} setFinal(): void {} setReconnect(): void {} setBloodLevel(): void {} setReducedMotion(): void {} + viewForward(): { x: number; z: number } | null { return mocks.viewForward; } + setCornerPanelTop(): void {} resyncClock(): void { mocks.rendererResyncs += 1; } push(snapshot: EngineSnapshot): void { this.pushes.push(snapshot.tick); } destroy(): void { mocks.rendererDestroy(); } @@ -59,6 +74,7 @@ vi.mock("./render/renderer", () => ({ import type { IDiscordSDK } from "@discord/embedded-app-sdk"; import { ClientError } from "./api"; import { HandsApp } from "./app"; +import { AudioFeedback } from "./audio"; const send = (message: ServerMessage): void => { mocks.callbacks?.onMessage(message); }; /** A Discord client that answers every command; `close` is what would close the Activity. */ @@ -70,7 +86,7 @@ const fakeDiscord = (instanceId: string) => ({ }); const json = (value: unknown): Response => new Response(JSON.stringify(value), { status: 200, headers: { "Content-Type": "application/json" } }); const handsBackend = () => vi.fn(async (input: URL | RequestInfo) => String(input).includes("bootstrap") - ? json({ client_id: "123", state: "state", protocol: 3, simulation: { tick_rate: 30, ring_half_width: 500, ring_half_height: 500 } }) + ? json({ client_id: "123", state: "state", protocol: PROTOCOL_VERSION, simulation: { tick_rate: 30, ring_half_width: 500, ring_half_height: 500 } }) : json({ access_token: "access", ticket: "ticket", player: { id: "one", name: "One", avatar: null, rating: 1500 } })); const players = [ { id: "one", name: "One", avatar: null, rating: 1500, connected: true }, @@ -92,9 +108,57 @@ describe("browser lifecycle and accessible overlays", () => { mocks.callbacks = null; mocks.rendererPushes.length = 0; mocks.resultVisible = true; + mocks.cpuRequests.length = 0; + mocks.cpuAccepted = true; vi.clearAllMocks(); }); + it("cycles the camera with K, keeps the settings panel in step and ignores K while typing", () => { + localStorage.clear(); + const root = document.createElement("div"); + document.body.append(root); + const app = new HandsApp(root); + const select = root.querySelector("[data-camera]")!; + const press = (target: EventTarget = window): void => { target.dispatchEvent(new KeyboardEvent("keydown", { code: "KeyK", bubbles: true, cancelable: true })); }; + const saved = (): unknown => JSON.parse(localStorage.getItem("hands.preferences.v1") ?? "{}").camera; + expect(select.value).toBe("broadcast"); + press(); + expect(select.value).toBe("close"); + press(); + expect(select.value).toBe("fighter"); + expect(saved()).toBe("fighter"); + press(); + expect(select.value).toBe("broadcast"); + press(root.querySelector("[data-volume]")!); + expect(select.value).toBe("broadcast"); + select.value = "fighter"; + select.dispatchEvent(new Event("change")); + expect(saved()).toBe("fighter"); + app.destroy(); + press(); + expect(saved()).toBe("fighter"); + root.remove(); + localStorage.clear(); + }); + + it("turns the controls with the player's own camera", async () => { + history.replaceState({}, "", "/?instance_id=turned"); + const root = document.createElement("div"); + const app = new HandsApp(root); + app.start(); + await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 0, next_sequence: 0, reconnect_ticket: "rotated" }); + send({ version: PROTOCOL_VERSION, type: "ready", players: [...players] }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(10) }); + window.dispatchEvent(new KeyboardEvent("keydown", { code: "KeyW", bubbles: true, cancelable: true })); + expect(mocks.input!()).toMatchObject({ moveX: 0, moveY: 1000 }); + mocks.viewForward = { x: 1, z: 0 }; + expect(mocks.input!()).toMatchObject({ moveX: 1000, moveY: 0 }); + window.dispatchEvent(new KeyboardEvent("keyup", { code: "KeyW", bubbles: true, cancelable: true })); + mocks.viewForward = null; + app.destroy(); + }); + it("labels graphic full mode and exposes the required model attribution", () => { const root = document.createElement("div"); const app = new HandsApp(root); @@ -112,8 +176,8 @@ describe("browser lifecycle and accessible overlays", () => { const app = new HandsApp(root); app.start(); await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); - send({ version: 3, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [players[0]], server_tick: 0, next_sequence: 0, reconnect_ticket: "rotated" }); - send({ version: 3, type: "waiting", open_seats: 1 }); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [players[0]], server_tick: 0, next_sequence: 0, reconnect_ticket: "rotated" }); + send({ version: PROTOCOL_VERSION, type: "waiting", open_seats: 1 }); expect(root.querySelector("[data-invite]")).toBeNull(); expect(root.querySelector("[data-status]")?.textContent).toContain("Play now"); expect(root.querySelector("[data-status]")?.hidden).toBe(false); @@ -121,9 +185,9 @@ describe("browser lifecycle and accessible overlays", () => { const hint = root.querySelector("[data-hint]")!; expect(hint.hidden).toBe(false); expect(hint.textContent).toContain("Jab"); - send({ version: 3, type: "ready", players: [...players] }); + send({ version: PROTOCOL_VERSION, type: "ready", players: [...players] }); const cards = ["A", "B", "C"].map((judge) => ({ judge, player_one: [10], player_two: [9] })); - send({ version: 3, type: "final", match_id: "m", winner_id: "one", method: "decision", round: 1, scorecards: cards, ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }); + send({ version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "one", method: "decision", round: 1, scorecards: cards, ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }); expect(root.querySelector("[data-final]")?.textContent).toContain("A: 10 to 9"); expect(root.querySelector("[data-hint]")!.hidden).toBe(true); expect(mocks.activityClose).not.toHaveBeenCalled(); @@ -145,9 +209,9 @@ describe("browser lifecycle and accessible overlays", () => { controls.click(); expect(root.querySelector("[data-controls-panel]")!.hidden).toBe(false); - send({ version: 3, type: "welcome", role: "spectator", player_id: "viewer", players: [...players], server_tick: 30, reconnect_ticket: "spectator-ticket" }); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "spectator", player_id: "viewer", players: [...players], server_tick: 30, reconnect_ticket: "spectator-ticket" }); const redacted = makeSnapshot(30, "knockdown"); - send({ version: 3, type: "snapshot", payload: redacted }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: redacted }); expect(root.querySelector("[data-role]")!.hidden).toBe(false); expect(root.querySelector("[data-role]")?.textContent).toContain("SPECTATING"); @@ -190,7 +254,7 @@ describe("browser lifecycle and accessible overlays", () => { app.start(); if (source === "the socket") { await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); - send({ version: 3, type: "error", code: "client_outdated" }); + send({ version: PROTOCOL_VERSION, type: "error", code: "client_outdated" }); mocks.callbacks?.onFatal("client_outdated"); } await vi.waitFor(() => expect(root.querySelector("[data-status]")?.textContent).toBe("Hands was updated. Reload to continue (client_outdated).")); @@ -209,12 +273,12 @@ describe("browser lifecycle and accessible overlays", () => { const app = new HandsApp(root); app.start(); await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); - send({ version: 3, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 100, next_sequence: 8, reconnect_ticket: "rotated" }); - send({ version: 3, type: "snapshot", payload: makeSnapshot(100) }); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 100, next_sequence: 8, reconnect_ticket: "rotated" }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(100) }); const overlay = root.querySelector("[data-overlay]")!; expect(overlay.hidden).toBe(false); vi.useFakeTimers(); - send({ version: 3, type: "snapshot", payload: makeSnapshot(101, "complete") }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(101, "complete") }); expect(overlay.hidden).toBe(true); vi.advanceTimersByTime(4_200); expect(overlay.hidden).toBe(false); @@ -227,13 +291,13 @@ describe("browser lifecycle and accessible overlays", () => { const app = new HandsApp(root); app.start(); await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); - send({ version: 3, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 100, next_sequence: 8, reconnect_ticket: "rotated" }); - send({ version: 3, type: "snapshot", payload: makeSnapshot(100) }); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 100, next_sequence: 8, reconnect_ticket: "rotated" }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(100) }); const overlay = root.querySelector("[data-overlay]")!; expect(overlay.hidden).toBe(false); mocks.resultVisible = false; vi.useFakeTimers(); - send({ version: 3, type: "final", match_id: "m-replay", winner_id: "one", method: "ko", round: 1, scorecards: [], ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }); + send({ version: PROTOCOL_VERSION, type: "final", match_id: "m-replay", winner_id: "one", method: "ko", round: 1, scorecards: [], ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }); expect(overlay.hidden).toBe(true); vi.advanceTimersByTime(1_000); expect(overlay.hidden).toBe(true); @@ -244,15 +308,53 @@ describe("browser lifecycle and accessible overlays", () => { app.destroy(); }); + it("waits for the named opponent after Rematch, with the computer only as a fallback", async () => { + history.replaceState({}, "", "/?instance_id=launch"); + const root = document.createElement("div"); + const app = new HandsApp(root); + app.start(); + await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 100, next_sequence: 8, reconnect_ticket: "rotated" }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(100) }); + vi.useFakeTimers(); + send({ version: PROTOCOL_VERSION, type: "final", match_id: "m1", winner_id: "one", method: "decision", round: 1, scorecards: [], ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }); + vi.advanceTimersByTime(11_500); + const first = mocks.callbacks; + root.querySelector("[data-rematch]")!.click(); + vi.useRealTimers(); + await vi.waitFor(() => expect(mocks.callbacks).not.toBe(first)); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1516, players: [players[0]], server_tick: 0, next_sequence: 0, reconnect_ticket: "again" }); + send({ version: PROTOCOL_VERSION, type: "waiting", open_seats: 1 }); + expect(root.querySelector("[data-status]")?.textContent).toBe("Waiting for Two to take the rematch…"); + expect(root.querySelector("[data-cpu-prompt]")?.textContent).toBe("Or fight the computer instead:"); + send({ version: PROTOCOL_VERSION, type: "ready", players: [...players] }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(1, "countdown") }); + expect(root.querySelector("[data-cpu-prompt]")?.textContent).toBe("No one here yet? Fight the computer."); + app.destroy(); + }); + + it("groups the controls into sections and keeps diagnostics behind a disclosure", () => { + const root = document.createElement("div"); + const app = new HandsApp(root); + const headings = [...root.querySelectorAll("[data-controls-panel] h3")].map((heading) => heading.textContent); + expect(headings).toEqual(["Keyboard", "Between rounds", "Controller", "Touch"]); + expect(root.querySelector("[data-controls-panel]")?.textContent).toContain("Shift+1 low blow"); + const diagnostics = root.querySelector("details.diagnostics"); + expect(diagnostics).not.toBeNull(); + expect(diagnostics?.hasAttribute("open")).toBe(false); + expect(diagnostics?.querySelector("[data-diagnostics]")).not.toBeNull(); + app.destroy(); + }); + it("offers a rematch after the result hold and retries while the ring is still clearing", async () => { history.replaceState({}, "", "/?instance_id=launch"); const root = document.createElement("div"); const app = new HandsApp(root); app.start(); await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); - send({ version: 3, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 100, next_sequence: 8, reconnect_ticket: "rotated" }); - send({ version: 3, type: "snapshot", payload: makeSnapshot(100) }); - const final = (matchId: string): ServerMessage => ({ version: 3, type: "final", match_id: matchId, winner_id: "one", method: "decision", round: 1, scorecards: [], ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 100, next_sequence: 8, reconnect_ticket: "rotated" }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(100) }); + const final = (matchId: string): ServerMessage => ({ version: PROTOCOL_VERSION, type: "final", match_id: matchId, winner_id: "one", method: "decision", round: 1, scorecards: [], ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }); const button = root.querySelector("[data-rematch]")!; expect(button.hidden).toBe(true); vi.useFakeTimers(); @@ -268,7 +370,7 @@ describe("browser lifecycle and accessible overlays", () => { vi.useRealTimers(); await vi.waitFor(() => expect(mocks.callbacks).not.toBe(first)); expect(button.hidden).toBe(true); - send({ version: 3, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1516, players: [...players], server_tick: 900, next_sequence: 0, reconnect_ticket: "again" }); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1516, players: [...players], server_tick: 900, next_sequence: 0, reconnect_ticket: "again" }); vi.useFakeTimers(); const held = mocks.callbacks; send(final("m1")); @@ -277,29 +379,243 @@ describe("browser lifecycle and accessible overlays", () => { vi.advanceTimersByTime(3_100); vi.useRealTimers(); await vi.waitFor(() => expect(mocks.callbacks).not.toBe(held)); - send({ version: 3, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1516, players: [...players], server_tick: 1, next_sequence: 0, reconnect_ticket: "fresh" }); - send({ version: 3, type: "snapshot", payload: makeSnapshot(1, "countdown") }); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1516, players: [...players], server_tick: 1, next_sequence: 0, reconnect_ticket: "fresh" }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(1, "countdown") }); expect(root.querySelector("[data-status]")?.textContent).toBe("Bout countdown."); expect(root.querySelector("[data-status]")?.hidden).toBe(true); expect(root.querySelector("[data-overlay]")?.hasAttribute("data-raised")).toBe(true); + expect(root.querySelector("[data-overlay]")?.hasAttribute("data-result")).toBe(false); send(final("m2")); expect(root.querySelector("[data-status]")?.textContent).toBe("Bout complete. Scorecards and rating changes are displayed."); - expect(root.querySelector("[data-status]")?.hidden).toBe(false); + expect(root.querySelector("[data-status]")?.hidden).toBe(true); + expect(root.querySelector("[data-overlay]")?.hasAttribute("data-result")).toBe(true); + expect(button.hidden).toBe(false); + expect(button.disabled).toBe(true); + app.destroy(); + }); + + it("lets a spectator stay for the next bout: he watches the corners fill, or takes a seat left empty", async () => { + history.replaceState({}, "", "/?instance_id=launch"); + const root = document.createElement("div"); + const app = new HandsApp(root); + app.start(); + await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "spectator", player_id: "viewer", players: [...players], server_tick: 100, reconnect_ticket: "watching" }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(100) }); + vi.useFakeTimers(); + send({ version: PROTOCOL_VERSION, type: "final", match_id: "m1", winner_id: "one", method: "decision", round: 1, scorecards: [], ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }); + const button = root.querySelector("[data-rematch]")!; expect(button.hidden).toBe(false); expect(button.disabled).toBe(true); + expect(button.textContent).toContain("Next bout in"); + vi.advanceTimersByTime(11_500); + expect(button.textContent).toBe("Next bout"); + const first = mocks.callbacks; + button.click(); + vi.useRealTimers(); + await vi.waitFor(() => expect(mocks.callbacks).not.toBe(first)); + // The fighters' seats are held for their rematch, so he watches the corners fill. + send({ version: PROTOCOL_VERSION, type: "welcome", role: "spectator", player_id: "viewer", players: [], server_tick: 0, reconnect_ticket: "again" }); + send({ version: PROTOCOL_VERSION, type: "waiting", open_seats: 2 }); + expect(root.querySelector("[data-status]")?.textContent).toBe("Spectating — Waiting for the fighters…"); + expect(root.querySelector("[data-cpu]")!.hidden).toBe(true); + expect(mocks.cpuRequests).toEqual([]); + // Nobody came back for a seat: it is his, on the connection he has. + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "viewer", seat: 1, rating: 1500, players: [{ id: "viewer", name: "Viewer", avatar: null, rating: 1500, connected: true }], server_tick: 0, next_sequence: 0, reconnect_ticket: "seated" }); + send({ version: PROTOCOL_VERSION, type: "waiting", open_seats: 1 }); + expect(root.querySelector("[data-status]")?.textContent).toBe("Waiting for an opponent. Anyone in this channel can join with Play now."); + expect(root.querySelector("[data-cpu]")!.hidden).toBe(false); + expect(root.querySelector("[data-role]")!.hidden).toBe(true); app.destroy(); }); + describe("fighting the computer", () => { + const computer = { id: "cpu:champion", name: "Viktor 'Iron' Volkov", avatar: null, rating: 1400, connected: true, cpu: true }; + const alone = (): void => { + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [players[0]], server_tick: 0, next_sequence: 0, reconnect_ticket: "rotated" }); + send({ version: PROTOCOL_VERSION, type: "waiting", open_seats: 1 }); + }; + const launch = async (): Promise<{ app: HandsApp; root: HTMLElement }> => { + history.replaceState({}, "", "/?instance_id=launch"); + const root = document.createElement("div"); + const app = new HandsApp(root); + app.start(); + await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); + return { app, root }; + }; + const pick = (root: HTMLElement, level: string): void => root.querySelector(`[data-cpu-level="${level}"]`)!.click(); + + it("offers a fighter waiting alone the computer at three levels and calls the one chosen", async () => { + const { app, root } = await launch(); + const picker = root.querySelector("[data-cpu]")!; + expect(picker.hidden).toBe(true); + alone(); + expect(picker.hidden).toBe(false); + expect([...picker.querySelectorAll("[data-cpu-level]")].map((button) => button.dataset.cpuLevel)).toEqual(["rookie", "contender", "champion"]); + pick(root, "contender"); + expect(mocks.cpuRequests).toEqual(["contender"]); + expect(picker.hidden).toBe(true); + expect(root.querySelector("[data-status]")?.textContent).toBe("Calling in the computer…"); + pick(root, "rookie"); + expect(mocks.cpuRequests).toEqual(["contender"]); + send({ version: PROTOCOL_VERSION, type: "ready", players: [players[0], computer] }); + expect(picker.hidden).toBe(true); + app.destroy(); + }); + + it("keeps the offer away from spectators and from a bout already under way", async () => { + const { app, root } = await launch(); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "spectator", player_id: "viewer", players: [...players], server_tick: 40, reconnect_ticket: "spectator" }); + send({ version: PROTOCOL_VERSION, type: "waiting", open_seats: 1 }); + expect(root.querySelector("[data-cpu]")!.hidden).toBe(true); + pick(root, "rookie"); + expect(mocks.cpuRequests).toEqual([]); + app.destroy(); + const second = await launch(); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 0, next_sequence: 0, reconnect_ticket: "rotated" }); + send({ version: PROTOCOL_VERSION, type: "ready", players: [...players] }); + expect(second.root.querySelector("[data-cpu]")!.hidden).toBe(true); + second.app.destroy(); + }); + + it("asks again when a reconnect lands back in the empty ring, and keeps offering when it could not ask", async () => { + const { app, root } = await launch(); + alone(); + mocks.cpuAccepted = false; + pick(root, "rookie"); + expect(root.querySelector("[data-cpu]")!.hidden).toBe(false); + expect(root.querySelector("[data-status]")?.textContent).toContain("Play now"); + mocks.cpuAccepted = true; + pick(root, "rookie"); + send({ version: PROTOCOL_VERSION, type: "waiting", open_seats: 1 }); + expect(mocks.cpuRequests).toEqual(["rookie", "rookie", "rookie"]); + app.destroy(); + }); + + it("calls the bout unrated and brings the same computer back for a rematch, after a wait for a friend", async () => { + const { app, root } = await launch(); + alone(); + pick(root, "champion"); + send({ version: PROTOCOL_VERSION, type: "ready", players: [players[0], computer] }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...makeSnapshot(100), fighters: [fighter("one", -100), fighter("cpu:champion", 100)] } }); + const summary = root.querySelector("[data-fight-summary]")?.textContent ?? ""; + expect(summary).toContain("Viktor 'Iron' Volkov, computer opponent"); + expect(summary).not.toContain("ELO 1400"); + vi.useFakeTimers(); + send({ version: PROTOCOL_VERSION, type: "final", match_id: "m1", winner_id: "cpu:champion", method: "decision", round: 1, scorecards: [], ratings: { one: { before: 1500, after: 1500 }, "cpu:champion": { before: 1400, after: 1400 } } }); + expect(root.querySelector("[data-final]")?.textContent).toContain("Unrated bout against the computer."); + expect(root.querySelector("[data-final]")?.textContent).not.toContain("Ratings:"); + expect(root.querySelector("[data-status]")?.textContent).toContain("unrated"); + vi.advanceTimersByTime(11_500); + const first = mocks.callbacks; + root.querySelector("[data-rematch]")!.click(); + vi.useRealTimers(); + await vi.waitFor(() => expect(mocks.callbacks).not.toBe(first)); + expect(mocks.cpuRequests).toEqual(["champion"]); + vi.useFakeTimers(); + alone(); + // Not straight back: a friend who joins in the next few seconds fights instead. + expect(mocks.cpuRequests).toEqual(["champion"]); + expect(root.querySelector("[data-status]")?.textContent).toBe("Rematch with the Champion in 5s, unless someone joins first."); + expect(root.querySelector("[data-cpu]")!.hidden).toBe(false); + expect(root.querySelector("[data-cpu-prompt]")?.textContent).toBe("Or start now:"); + vi.advanceTimersByTime(2_100); + expect(root.querySelector("[data-status]")?.textContent).toBe("Rematch with the Champion in 3s, unless someone joins first."); + vi.advanceTimersByTime(3_000); + expect(mocks.cpuRequests).toEqual(["champion", "champion"]); + expect(root.querySelector("[data-cpu]")!.hidden).toBe(true); + expect(root.querySelector("[data-status]")?.textContent).toBe("Calling in the computer…"); + vi.useRealTimers(); + app.destroy(); + }); + + it("lets a friend who joins during the wait fight the rematch instead of the computer", async () => { + const { app, root } = await launch(); + alone(); + pick(root, "rookie"); + send({ version: PROTOCOL_VERSION, type: "ready", players: [players[0], { ...computer, id: "cpu:rookie" }] }); + vi.useFakeTimers(); + send({ version: PROTOCOL_VERSION, type: "final", match_id: "m1", winner_id: "one", method: "decision", round: 1, scorecards: [], ratings: { one: { before: 1500, after: 1500 }, "cpu:rookie": { before: 900, after: 900 } } }); + vi.advanceTimersByTime(11_500); + const first = mocks.callbacks; + root.querySelector("[data-rematch]")!.click(); + vi.useRealTimers(); + await vi.waitFor(() => expect(mocks.callbacks).not.toBe(first)); + vi.useFakeTimers(); + alone(); + vi.advanceTimersByTime(1_000); + send({ version: PROTOCOL_VERSION, type: "select", deadline_ms: 9_000, players: [...players], ready: [] }); + vi.advanceTimersByTime(10_000); + expect(mocks.cpuRequests).toEqual(["rookie"]); + vi.useRealTimers(); + app.destroy(); + }); + + it("lets a tap call the computer at once during the wait", async () => { + const { app, root } = await launch(); + alone(); + pick(root, "contender"); + send({ version: PROTOCOL_VERSION, type: "ready", players: [players[0], { ...computer, id: "cpu:contender" }] }); + vi.useFakeTimers(); + send({ version: PROTOCOL_VERSION, type: "final", match_id: "m1", winner_id: "one", method: "decision", round: 1, scorecards: [], ratings: { one: { before: 1500, after: 1500 }, "cpu:contender": { before: 1200, after: 1200 } } }); + vi.advanceTimersByTime(11_500); + const first = mocks.callbacks; + root.querySelector("[data-rematch]")!.click(); + vi.useRealTimers(); + await vi.waitFor(() => expect(mocks.callbacks).not.toBe(first)); + vi.useFakeTimers(); + alone(); + pick(root, "champion"); + expect(mocks.cpuRequests).toEqual(["contender", "champion"]); + vi.advanceTimersByTime(10_000); + expect(mocks.cpuRequests).toEqual(["contender", "champion"]); + vi.useRealTimers(); + app.destroy(); + }); + + it("forgets the computer once a person takes its seat before the bell", async () => { + const { app, root } = await launch(); + alone(); + pick(root, "rookie"); + expect(mocks.cpuRequests).toEqual(["rookie"]); + // A friend arrived during the computer's pick and took its seat. + send({ version: PROTOCOL_VERSION, type: "select", deadline_ms: 9_000, players: [...players], ready: [] }); + // He drops out of the pick for good: back to waiting, and the computer is not called by itself. + send({ version: PROTOCOL_VERSION, type: "waiting", open_seats: 1 }); + expect(mocks.cpuRequests).toEqual(["rookie"]); + expect(root.querySelector("[data-cpu]")!.hidden).toBe(false); + app.destroy(); + }); + + it("does not call the computer for a rematch against a person", async () => { + const { app, root } = await launch(); + alone(); + send({ version: PROTOCOL_VERSION, type: "ready", players: [...players] }); + vi.useFakeTimers(); + send({ version: PROTOCOL_VERSION, type: "final", match_id: "m1", winner_id: "one", method: "decision", round: 1, scorecards: [], ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }); + expect(root.querySelector("[data-final]")?.textContent).toContain("Ratings:"); + vi.advanceTimersByTime(11_500); + const first = mocks.callbacks; + root.querySelector("[data-rematch]")!.click(); + vi.useRealTimers(); + await vi.waitFor(() => expect(mocks.callbacks).not.toBe(first)); + alone(); + expect(mocks.cpuRequests).toEqual([]); + expect(root.querySelector("[data-cpu]")!.hidden).toBe(false); + app.destroy(); + }); + }); + it("retries a rematch that reaches the old room while it is still closing, once per refusal", async () => { history.replaceState({}, "", "/?instance_id=launch"); const root = document.createElement("div"); const app = new HandsApp(root); app.start(); await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); - send({ version: 3, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 100, next_sequence: 8, reconnect_ticket: "rotated" }); - send({ version: 3, type: "snapshot", payload: makeSnapshot(100) }); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 100, next_sequence: 8, reconnect_ticket: "rotated" }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(100) }); vi.useFakeTimers(); - send({ version: 3, type: "final", match_id: "m1", winner_id: "one", method: "decision", round: 1, scorecards: [], ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }); + send({ version: PROTOCOL_VERSION, type: "final", match_id: "m1", winner_id: "one", method: "decision", round: 1, scorecards: [], ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }); vi.advanceTimersByTime(11_500); const first = mocks.callbacks; root.querySelector("[data-rematch]")!.click(); @@ -308,16 +624,16 @@ describe("browser lifecycle and accessible overlays", () => { const closing = mocks.callbacks; const authorizations = mocks.authorize.mock.calls.length; vi.useFakeTimers(); - send({ version: 3, type: "error", code: "room_closed" }); + send({ version: PROTOCOL_VERSION, type: "error", code: "room_closed" }); closing?.onFatal("room_closed"); expect(root.querySelector("[data-status]")?.textContent).toContain("still being cleared"); vi.advanceTimersByTime(3_100); vi.useRealTimers(); await vi.waitFor(() => expect(mocks.callbacks).not.toBe(closing)); expect(mocks.authorize.mock.calls.length).toBe(authorizations + 1); - send({ version: 3, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1516, players: [players[0]], server_tick: 0, next_sequence: 0, reconnect_ticket: "fresh" }); - send({ version: 3, type: "waiting", open_seats: 1 }); - send({ version: 3, type: "error", code: "room_closed" }); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1516, players: [players[0]], server_tick: 0, next_sequence: 0, reconnect_ticket: "fresh" }); + send({ version: PROTOCOL_VERSION, type: "waiting", open_seats: 1 }); + send({ version: PROTOCOL_VERSION, type: "error", code: "room_closed" }); expect(root.querySelector("[data-status]")?.textContent).toContain("already ended"); app.destroy(); }); @@ -329,12 +645,12 @@ describe("browser lifecycle and accessible overlays", () => { app.start(); await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); mocks.rendererResyncs = 0; - send({ version: 3, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 100, next_sequence: 8, reconnect_ticket: "rotated" }); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 100, next_sequence: 8, reconnect_ticket: "rotated" }); expect(mocks.rendererResyncs).toBe(1); - send({ version: 3, type: "snapshot", payload: makeSnapshot(100) }); - send({ version: 3, type: "paused", player_id: "two", grace_ms: 20_000 }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(100) }); + send({ version: PROTOCOL_VERSION, type: "paused", player_id: "two", grace_ms: 20_000 }); expect(mocks.rendererResyncs).toBe(1); - send({ version: 3, type: "resumed", player_id: "two" }); + send({ version: PROTOCOL_VERSION, type: "resumed", player_id: "two" }); expect(mocks.rendererResyncs).toBe(2); app.destroy(); }); @@ -370,8 +686,8 @@ describe("browser lifecycle and accessible overlays", () => { const app = new HandsApp(root); app.start(); await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); - send({ version: 3, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 100, next_sequence: 8, reconnect_ticket: "rotated" }); - send({ version: 3, type: "snapshot", payload: makeSnapshot(100) }); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 100, next_sequence: 8, reconnect_ticket: "rotated" }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(100) }); const firstCallbacks = mocks.callbacks!; firstCallbacks.onFatal("persistence_failed"); expect(mocks.rendererDestroy).toHaveBeenCalledOnce(); @@ -382,9 +698,9 @@ describe("browser lifecycle and accessible overlays", () => { expect(mocks.rendererDestroy).toHaveBeenCalledOnce(); expect(mocks.sessionDestroy).toHaveBeenCalledOnce(); expect(mocks.rendererPushes).toHaveLength(1); - send({ version: 3, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 1, next_sequence: 0, reconnect_ticket: "new" }); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 1, next_sequence: 0, reconnect_ticket: "new" }); expect(mocks.rendererPushes).toHaveLength(2); - send({ version: 3, type: "snapshot", payload: makeSnapshot(1, "countdown") }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(1, "countdown") }); expect(mocks.rendererPushes).toEqual([[100], [1]]); expect(root.querySelector("[data-final]")?.textContent).toBe(""); expect(root.querySelector("[data-status]")?.textContent).toBe("Bout countdown."); @@ -398,10 +714,10 @@ describe("browser lifecycle and accessible overlays", () => { app.start(); await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); const longName = "A very long authoritative fighter name that canvas must truncate but semantics retain"; - send({ version: 3, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [{ ...players[0], name: longName }, players[1]], server_tick: 1, next_sequence: 0, reconnect_ticket: "new" }); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [{ ...players[0], name: longName }, players[1]], server_tick: 1, next_sequence: 0, reconnect_ticket: "new" }); const downed = { ...fighter("one", -100), is_downed: true, get_up_prompt: "get_up_left" as const, get_up_meter: 12, get_up_required: 50, get_up_count: 0 }; const snapshot = { ...makeSnapshot(2, "knockdown"), fighters: [downed, fighter("two", 100)] as const }; - send({ version: 3, type: "snapshot", payload: snapshot }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: snapshot }); const summary = root.querySelector("[data-fight-summary]")!; const live = root.querySelector("[data-fight-status]")!; expect(summary.hasAttribute("aria-live")).toBe(false); @@ -411,10 +727,10 @@ describe("browser lifecycle and accessible overlays", () => { expect(live.textContent).toBe("Knockdown. Count 0. Press left now."); const firstLiveNode = live.firstChild; const staminaChanged = { ...snapshot, tick: 3, fighters: [{ ...downed, stamina: downed.stamina - 1 }, snapshot.fighters[1]] as const }; - send({ version: 3, type: "snapshot", payload: staminaChanged }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: staminaChanged }); expect(summary.textContent).toContain(`stamina ${downed.stamina - 1}`); expect(live.firstChild).toBe(firstLiveNode); - send({ version: 3, type: "snapshot", payload: { ...staminaChanged, tick: 4, fighters: [{ ...staminaChanged.fighters[0], get_up_prompt: "get_up_right" }, staminaChanged.fighters[1]] } }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...staminaChanged, tick: 4, fighters: [{ ...staminaChanged.fighters[0], get_up_prompt: "get_up_right" }, staminaChanged.fighters[1]] } }); expect(live.textContent).toBe("Knockdown. Count 0. Press right now."); expect(live.firstChild).not.toBe(firstLiveNode); app.destroy(); @@ -428,19 +744,19 @@ describe("browser lifecycle and accessible overlays", () => { app.start(); await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); const controls = root.querySelector(".touch-controls")!; - send({ version: 3, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [players[0]], server_tick: 0, next_sequence: 0, reconnect_ticket: "rotated" }); - send({ version: 3, type: "waiting", open_seats: 1 }); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [players[0]], server_tick: 0, next_sequence: 0, reconnect_ticket: "rotated" }); + send({ version: PROTOCOL_VERSION, type: "waiting", open_seats: 1 }); expect(controls.classList.contains("disabled")).toBe(true); - send({ version: 3, type: "ready", players: [...players] }); - send({ version: 3, type: "snapshot", payload: makeSnapshot(100) }); + send({ version: PROTOCOL_VERSION, type: "ready", players: [...players] }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(100) }); expect(controls.classList.contains("disabled")).toBe(false); expect(controls.classList.contains("resting")).toBe(false); - send({ version: 3, type: "snapshot", payload: makeSnapshot(101, "rest") }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(101, "rest") }); expect(controls.classList.contains("disabled")).toBe(false); expect(controls.classList.contains("resting")).toBe(true); - send({ version: 3, type: "snapshot", payload: makeSnapshot(102, "fight") }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(102, "fight") }); expect(controls.classList.contains("resting")).toBe(false); - send({ version: 3, type: "snapshot", payload: makeSnapshot(103, "complete") }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(103, "complete") }); expect(controls.classList.contains("disabled")).toBe(true); app.destroy(); }); @@ -455,10 +771,10 @@ describe("browser lifecycle and accessible overlays", () => { const app = new HandsApp(root, vi.fn(), new DiscordActivity(makeSdk)); app.start(); await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); - send({ version: 3, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 100, next_sequence: 8, reconnect_ticket: "rotated" }); - send({ version: 3, type: "snapshot", payload: makeSnapshot(100) }); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 100, next_sequence: 8, reconnect_ticket: "rotated" }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(100) }); vi.useFakeTimers(); - send({ version: 3, type: "final", match_id: "m1", winner_id: "one", method: "decision", round: 1, scorecards: [], ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }); + send({ version: PROTOCOL_VERSION, type: "final", match_id: "m1", winner_id: "one", method: "decision", round: 1, scorecards: [], ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }); vi.advanceTimersByTime(11_500); const first = mocks.callbacks; root.querySelector("[data-rematch]")!.click(); @@ -501,3 +817,259 @@ describe("browser lifecycle and accessible overlays", () => { vi.unstubAllGlobals(); }); }); + +describe("the corner panel", () => { + beforeEach(() => { + mocks.callbacks = null; + mocks.cornerPicks.length = 0; + vi.clearAllMocks(); + }); + + it("appears to a fighter between rounds and sends the pick to the corner", async () => { + history.replaceState({}, "", "/?instance_id=corner"); + const root = document.createElement("div"); + const app = new HandsApp(root); + app.start(); + await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 0, next_sequence: 0, reconnect_ticket: "rotated" }); + send({ version: PROTOCOL_VERSION, type: "ready", players: [...players] }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(10) }); + const panel = root.querySelector("[data-corner]")!; + expect(panel.hidden).toBe(true); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(11, "rest") }); + expect(panel.hidden).toBe(false); + root.querySelector('[data-corner-pick="corner_breath"]')!.click(); + expect(mocks.cornerPicks).toEqual(["corner_breath"]); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(12, "fight") }); + expect(panel.hidden).toBe(true); + app.destroy(); + }); + + it("is never shown to a spectator", async () => { + history.replaceState({}, "", "/?instance_id=corner-watch"); + const root = document.createElement("div"); + const app = new HandsApp(root); + app.start(); + await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "spectator", player_id: "viewer", players: [...players], server_tick: 0, reconnect_ticket: "spectator" }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(11, "rest") }); + expect(root.querySelector("[data-corner]")!.hidden).toBe(true); + app.destroy(); + }); +}); + +describe("the pick of styles", () => { + beforeEach(() => { + mocks.callbacks = null; + mocks.styleChoices.length = 0; + vi.clearAllMocks(); + }); + + const launch = async (instance: string): Promise<{ app: HandsApp; root: HTMLElement }> => { + history.replaceState({}, "", `/?instance_id=${instance}`); + const root = document.createElement("div"); + const app = new HandsApp(root); + app.start(); + await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); + return { app, root }; + }; + + it("offers last time's style when the pick begins, without settling on it, and never for a spectator", async () => { + localStorage.setItem("hands.style.v1", "swarmer"); + const select = { version: PROTOCOL_VERSION, type: "select", deadline_ms: 9_000, players: [...players], ready: [] } as const; + const fighterView = await launch("styles-join"); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [players[0]], server_tick: 0, next_sequence: 0 }); + expect(mocks.styleChoices).toEqual([]); + send(select); + expect(mocks.styleChoices).toEqual(["swarmer:false"]); + fighterView.app.destroy(); + const watcher = await launch("styles-watch"); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "spectator", player_id: "viewer", players: [...players], server_tick: 0 }); + send(select); + expect(mocks.styleChoices).toEqual(["swarmer:false"]); + watcher.app.destroy(); + }); + + it("shows the pick while the room is choosing and sends a tap as the fighter's settled style", async () => { + const { app, root } = await launch("styles-pick"); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [players[0]], server_tick: 0, next_sequence: 0 }); + send({ version: PROTOCOL_VERSION, type: "waiting", open_seats: 1 }); + const picker = root.querySelector("[data-style-picker]")!; + expect(picker.hidden).toBe(true); + send({ version: PROTOCOL_VERSION, type: "select", deadline_ms: 9_000, players: [...players], ready: ["two"] }); + expect(picker.hidden).toBe(false); + expect(root.querySelector("[data-status]")!.hidden).toBe(true); + expect(root.querySelector("[data-cpu]")!.hidden).toBe(true); + expect(picker.textContent).toContain("Two is ready."); + picker.querySelector('[data-style="slugger"]')!.click(); + expect(mocks.styleChoices.at(-1)).toBe("slugger:true"); + send({ version: PROTOCOL_VERSION, type: "ready", players: [{ ...players[0], style: "slugger" }, { ...players[1], style: "boxer" }] }); + expect(localStorage.getItem("hands.style.v1")).toBe("slugger"); + expect(picker.hidden).toBe(true); + app.destroy(); + }); + + it("remembers the style the room says the fighter boxes in, picked or left to the deadline", async () => { + localStorage.setItem("hands.style.v1", "boxer"); + const { app, root } = await launch("styles-remember"); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [players[0]], server_tick: 0, next_sequence: 0 }); + send({ version: PROTOCOL_VERSION, type: "select", deadline_ms: 9_000, players: [...players], ready: [] }); + root.querySelector('[data-style="slugger"]')!.click(); + // The tap reached the room after it closed: it boxes, and next time offers, the style the room used. + expect(localStorage.getItem("hands.style.v1")).toBe("boxer"); + send({ version: PROTOCOL_VERSION, type: "ready", players: [{ ...players[0], style: "counter_puncher" }, { ...players[1], style: "swarmer" }] }); + expect(localStorage.getItem("hands.style.v1")).toBe("counter_puncher"); + app.destroy(); + const watcher = await launch("styles-remember-watch"); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "spectator", player_id: "viewer", players: [...players], server_tick: 0 }); + send({ version: PROTOCOL_VERSION, type: "ready", players: [{ ...players[0], style: "slugger" }, { ...players[1], style: "swarmer" }] }); + expect(localStorage.getItem("hands.style.v1")).toBe("counter_puncher"); + watcher.app.destroy(); + }); +}); + +describe("the broadcast", () => { + /** An English voice that runs on the device, as Windows, macOS, iOS and Android list theirs. */ + const deviceVoices = [{ name: "Microsoft David", lang: "en-US", localService: true, default: true, voiceURI: "David" }]; + + beforeEach(() => { + mocks.callbacks = null; + mocks.renderers.length = 0; + vi.clearAllMocks(); + }); + + afterEach(() => { + Reflect.deleteProperty(window, "speechSynthesis"); + Reflect.deleteProperty(window, "SpeechSynthesisUtterance"); + }); + + const launch = async (): Promise<{ root: HTMLElement; app: HandsApp }> => { + history.replaceState({}, "", "/?instance_id=broadcast"); + const root = document.createElement("div"); + const app = new HandsApp(root); + app.start(); + await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [...players], server_tick: 0, next_sequence: 0 }); + return { root, app }; + }; + + it("offers caption and announcer settings, with the voice switched off where the browser cannot speak", async () => { + const { root, app } = await launch(); + const captions = root.querySelector("[data-commentary]")!; + const voice = root.querySelector("[data-announcer]")!; + expect(captions.checked).toBe(true); + expect(voice.disabled).toBe(true); + expect(voice.checked).toBe(false); + captions.checked = false; + captions.dispatchEvent(new Event("change")); + expect(JSON.parse(localStorage.getItem("hands.preferences.v1")!)).toMatchObject({ commentary: false }); + app.destroy(); + }); + + it("reads the ring announcements aloud and stops when the voice is switched off", async () => { + const spoken: string[] = []; + const cancel = vi.fn(); + Object.defineProperty(window, "speechSynthesis", { configurable: true, value: { speak: (utterance: { text: string }) => spoken.push(utterance.text), cancel, getVoices: () => deviceVoices } }); + Object.defineProperty(window, "SpeechSynthesisUtterance", { configurable: true, value: class { onend = null; onerror = null; constructor(readonly text: string) {} } }); + const { root, app } = await launch(); + const voice = root.querySelector("[data-announcer]")!; + expect(voice.disabled).toBe(false); + expect(voice.checked).toBe(true); + mocks.renderers.at(-1)!.onAnnouncement!(["In the blue corner... One!", "And in the red corner... Two!"]); + expect(spoken).toEqual(["In the blue corner... One!"]); + voice.checked = false; + voice.dispatchEvent(new Event("change")); + expect(cancel).toHaveBeenCalled(); + app.destroy(); + }); + + it("keeps the voice off where the device's only English voices speak from a vendor's servers", async () => { + const spoken: string[] = []; + const online = [{ name: "Google US English", lang: "en-US", localService: false, default: true, voiceURI: "Google US English" }]; + Object.defineProperty(window, "speechSynthesis", { configurable: true, value: { speak: (utterance: { text: string }) => spoken.push(utterance.text), cancel: vi.fn(), getVoices: () => online } }); + Object.defineProperty(window, "SpeechSynthesisUtterance", { configurable: true, value: class { onend = null; onerror = null; constructor(readonly text: string) {} } }); + const { root, app } = await launch(); + const voice = root.querySelector("[data-announcer]")!; + expect(voice.disabled).toBe(true); + expect(voice.checked).toBe(false); + expect(voice.parentElement!.title).toBe("This device has no English voice of its own"); + mocks.renderers.at(-1)!.onAnnouncement!(["In the blue corner, One!"]); + expect(spoken).toEqual([]); + app.destroy(); + }); + + it("stops the announcer mid-line when the player turns the volume down to nothing", async () => { + const cancel = vi.fn(); + Object.defineProperty(window, "speechSynthesis", { configurable: true, value: { speak: () => undefined, cancel, getVoices: () => deviceVoices } }); + Object.defineProperty(window, "SpeechSynthesisUtterance", { configurable: true, value: class { onend = null; onerror = null; constructor(readonly text: string) {} } }); + const { root, app } = await launch(); + mocks.renderers.at(-1)!.onAnnouncement!(["In the blue corner... One!"]); + const volume = root.querySelector("[data-volume]")!; + volume.value = "0"; + volume.dispatchEvent(new Event("input")); + expect(cancel).toHaveBeenCalled(); + app.destroy(); + }); + + it("lets the announcer finish the name it is reading at the opening bell, then stops it and drops the rest", async () => { + const spoken: string[] = []; + const cancel = vi.fn(); + Object.defineProperty(window, "speechSynthesis", { configurable: true, value: { speak: (utterance: { text: string }) => spoken.push(utterance.text), cancel, getVoices: () => deviceVoices } }); + Object.defineProperty(window, "SpeechSynthesisUtterance", { configurable: true, value: class { onend = null; onerror = null; constructor(readonly text: string) {} } }); + const { app } = await launch(); + vi.useFakeTimers(); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(1, "countdown") }); + mocks.renderers.at(-1)!.onAnnouncement!(["In the blue corner, One!"]); + mocks.renderers.at(-1)!.onAnnouncement!(["And in the red corner, Two!"]); + cancel.mockClear(); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(2, "countdown") }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: makeSnapshot(3, "fight") }); + // A slow voice still reading the blue corner's name at the bell gets to finish it, briefly. + expect(cancel).not.toHaveBeenCalled(); + vi.advanceTimersByTime(1_499); + expect(cancel).not.toHaveBeenCalled(); + vi.advanceTimersByTime(1); + expect(cancel).toHaveBeenCalledOnce(); + // The line still waiting was never started: round 1 is under way. + expect(spoken).toEqual(["In the blue corner, One!"]); + vi.useRealTimers(); + app.destroy(); + }); + + it("muffles the sound as the renderer shows the player's own fighter rocked, not as the snapshot arrives", async () => { + // The stun arrives a playback delay before the punch that caused it is on screen; the renderer, which + // shows that punch, says when he is rocked (and never does for a spectator). + const rocked = vi.spyOn(AudioFeedback.prototype, "rocked"); + const { app } = await launch(); + send({ version: PROTOCOL_VERSION, type: "ready", players: [...players] }); + const hit = makeSnapshot(40); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...hit, fighters: [{ ...hit.fighters[0], stunned_ticks: 45 }, hit.fighters[1]] } }); + expect(rocked).not.toHaveBeenCalled(); + mocks.renderers.at(-1)!.onRocked!(1, 40); + expect(rocked).toHaveBeenLastCalledWith(1, 40); + app.destroy(); + rocked.mockClear(); + history.replaceState({}, "", "/?instance_id=watch"); + const root = document.createElement("div"); + const watcher = new HandsApp(root); + watcher.start(); + await vi.waitFor(() => expect(mocks.callbacks).not.toBeNull()); + send({ version: PROTOCOL_VERSION, type: "welcome", role: "spectator", player_id: "three", players: [...players], server_tick: 0 }); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...hit, fighters: [{ ...hit.fighters[0], stunned_ticks: 45 }, hit.fighters[1]] } }); + expect(rocked).not.toHaveBeenCalled(); + watcher.destroy(); + }); + + it("lets the crowd follow the fight", async () => { + const tension = vi.spyOn(AudioFeedback.prototype, "tension"); + const chant = vi.spyOn(AudioFeedback.prototype, "chant"); + const { app } = await launch(); + send({ version: PROTOCOL_VERSION, type: "ready", players: [...players] }); + const hurt = makeSnapshot(40); + send({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...hurt, fighters: [hurt.fighters[0], { ...hurt.fighters[1], stunned_ticks: 12 }] } }); + expect(tension).toHaveBeenLastCalledWith(0.75); + mocks.renderers.at(-1)!.onCrowdCue!("chant"); + expect(chant).toHaveBeenCalledOnce(); + app.destroy(); + }); +}); diff --git a/web/hands/src/app.ts b/web/hands/src/app.ts index d609ecea..3454f1b3 100644 --- a/web/hands/src/app.ts +++ b/web/hands/src/app.ts @@ -1,29 +1,42 @@ import { RoundClock } from "./render/hud"; +import { AnnouncerVoice } from "./announcer"; import { AudioFeedback } from "./audio"; import { ClientError, safeError } from "./api"; import { DiscordActivity, type ActivityAuthorizer, type DiscordSession } from "./discord"; +import { CornerPanel } from "./corner"; +import { saveStyle, StylePicker } from "./styles"; import { describeError } from "./errors"; import { HapticFeedback } from "./haptics"; -import { CONTROL_HELP } from "./input/bindings"; +import { CONTROL_SECTIONS, controlHint } from "./input/bindings"; import { coarsePointer } from "./input/touch"; import { InputController } from "./input/input"; import { EventDeduplicator } from "./interpolation"; import { NetworkController } from "./network"; +import { CPU_LEVELS } from "./protocol"; +import { crowdTension } from "./render/commentary"; import { FightRenderer } from "./render/renderer"; -import { SettingsStore, type BloodLevel } from "./settings"; +import { CAMERA_MODES, SettingsStore, type BloodLevel } from "./settings"; import { initialState, reduceState, type GameState } from "./state"; -import type { EngineSnapshot, ServerMessage } from "./types"; +import type { CpuLevel, EngineSnapshot, PublicPlayer, ServerMessage } from "./types"; -const CONTACT_FEEDBACK_KINDS = new Set(["hit", "counter_hit", "block", "perfect_block", "guard_break", "knockdown"]); +const CONTACT_FEEDBACK_KINDS = new Set(["hit", "counter_hit", "block", "perfect_block", "guard_break", "knockdown", "parry", "body_collapse", "eye_shut"]); // The room keeps the finished bout for its result hold (ten seconds by default); a rejoin inside // that window only replays the old final, so the rematch waits it out and retries if it still hits it. -const CONTROL_HINT_KEYBOARD = "Move WASD · Jab F/J · Straight R/U · Hook G/H · Uppercut T/Y · Guard Q/E · Body Shift · Power Alt"; -const CONTROL_HINT_TOUCH = "Left side: drag to move · Right pads: L/R punches · Hold BODY, POWER or GUARD · Tap SLIP, WEAVE, PULL or CLINCH"; const REMATCH_HOLD_MS = 11_000; +/** How long the announcer may run on past the opening bell to finish the name it is reading. */ +const BELL_GRACE_MS = 1_500; /** How long the finish plays without the overlay while the result is still on its way. */ const RESULT_WAIT_MS = 4_000; const REMATCH_RETRY_MS = 3_000; const REMATCH_MAX_ATTEMPTS = 6; +/** A rematch against the computer waits this long first, so a friend who joins in the meantime fights instead. */ +const CPU_REMATCH_DELAY_MS = 5_000; +const CPU_CHOICES: readonly { readonly level: CpuLevel; readonly label: string; readonly detail: string }[] = [ + { level: "rookie", label: "Rookie", detail: "Slow hands, leaves openings" }, + { level: "contender", label: "Contender", detail: "Works the body, punishes mistakes" }, + { level: "champion", label: "Champion", detail: "Slips, counters, finishes" }, +]; +const isCpuLevel = (value: string | undefined): value is CpuLevel => CPU_LEVELS.includes(value as CpuLevel); export class HandsApp { private state: GameState = initialState; @@ -31,6 +44,7 @@ export class HandsApp { private readonly input = new InputController(); private readonly audio = new AudioFeedback(() => this.settings.current); private readonly haptics = new HapticFeedback(() => this.settings.current); + private readonly voice = new AnnouncerVoice(() => this.settings.current); private readonly feedbackEvents = new EventDeduplicator(); private renderer: FightRenderer | null = null; private network: NetworkController | null = null; @@ -50,6 +64,17 @@ export class HandsApp { private readonly retry: HTMLButtonElement; private readonly rematchButton: HTMLButtonElement; private readonly hint: HTMLElement; + private readonly cpuPicker: HTMLElement; + private readonly cpuPrompt: HTMLElement; + /** The computer asked for in this bout, and the one a rematch asks for again. */ + private cpuLevel: CpuLevel | null = null; + /** The fighter a pressed Rematch is waiting for, until the new bout is ready. */ + private rematchOpponent: string | null = null; + private lastPhase: EngineSnapshot["phase"] | null = null; + private cpuRematchLevel: CpuLevel | null = null; + /** The computer a rematch calls back once its short wait is over, while it lasts. */ + private cpuRematch: { readonly level: CpuLevel; readonly at: number } | null = null; + private cpuRematchTimer: number | null = null; private diagnosticsTimer: number | null = null; private finalReceivedAt = 0; private lastFinalMatchId: string | null = null; @@ -63,13 +88,15 @@ export class HandsApp { private readonly fightSummary: HTMLElement; private readonly liveFightStatus: HTMLElement; private readonly finalSummary: HTMLElement; + private readonly corner: CornerPanel; + private readonly stylePicker: StylePicker; constructor( private readonly root: HTMLElement, private readonly reloadPage: () => void = () => window.location.reload(), private readonly authorizer: ActivityAuthorizer = new DiscordActivity(), ) { - root.innerHTML = `
HANDSauthoritative two-player boxing

`; + root.innerHTML = `
HANDStwo-player boxing

`; this.canvas = root.querySelector("canvas")!; this.status = root.querySelector("[data-status]")!; this.overlay = root.querySelector("[data-overlay]")!; @@ -79,6 +106,9 @@ export class HandsApp { this.retry = root.querySelector("[data-retry]")!; this.rematchButton = root.querySelector("[data-rematch]")!; this.hint = root.querySelector("[data-hint]")!; + this.cpuPicker = root.querySelector("[data-cpu]")!; + this.cpuPrompt = root.querySelector("[data-cpu-prompt]")!; + this.cpuPicker.addEventListener("click", this.onCpuPick); this.rematchButton.addEventListener("click", this.onRematch); this.fightSummary = root.querySelector("[data-fight-summary]")!; this.liveFightStatus = root.querySelector("[data-fight-status]")!; @@ -87,8 +117,22 @@ export class HandsApp { this.bindPanels(); this.syncSettings(); this.input.attachTouch(root.querySelector(".activity")!); + this.corner = new CornerPanel(root.querySelector(".activity")!, (kind) => this.network?.sendCornerChoice(kind) ?? false); + this.stylePicker = new StylePicker(root.querySelector(".activity")!, (style, ready) => this.network?.chooseStyle(style, ready) ?? false); + this.input.setViewForward(() => this.renderer?.viewForward() ?? null); + window.addEventListener("keydown", this.onCameraKey); } + private readonly onCameraKey = (event: KeyboardEvent): void => { + if (event.code !== "KeyK" || event.repeat || event.ctrlKey || event.metaKey || event.altKey) return; + const target = event.target; + if (target instanceof HTMLInputElement || target instanceof HTMLSelectElement || target instanceof HTMLTextAreaElement) return; + event.preventDefault(); + const next = CAMERA_MODES[(CAMERA_MODES.indexOf(this.settings.current.camera) + 1) % CAMERA_MODES.length]!; + this.settings.update({ camera: next }); + this.root.querySelector("[data-camera]")!.value = next; + }; + start(): void { void this.authorize(); } @@ -101,6 +145,8 @@ export class HandsApp { this.session = null; this.renderer?.destroy(); this.renderer = null; + this.voice.cancel(); + this.audio.reset(); this.abort.abort(); this.abort = new AbortController(); this.state = initialState; @@ -112,11 +158,31 @@ export class HandsApp { this.liveFightStatus.textContent = ""; this.clearRematchTimers(); this.rematchButton.hidden = true; + this.cpuLevel = null; + this.lastPhase = null; + } + + private readonly onCpuPick = (event: Event): void => { + const level = (event.target as Element | null)?.closest("[data-cpu-level]")?.dataset.cpuLevel; + if (!isCpuLevel(level) || this.state.stage !== "waiting" || this.state.role !== "fighter" || this.cpuLevel !== null) return; + this.callCpu(level); + }; + + private callCpu(level: CpuLevel): void { + if (this.network?.requestCpu(level) !== true) return; + this.cpuLevel = level; + this.renderState(); + } + + private get cpuBout(): boolean { + return Object.values(this.state.players).some((player) => player.cpu === true); } private readonly onRematch = (): void => { if (this.rematchButton.disabled || this.state.stage !== "complete") return; this.rematchAttempts = 1; + // Named while the old bout's players are still known, for the wait in the new room. + this.rematchOpponent = this.cpuBout || this.state.role !== "fighter" ? null : Object.values(this.state.players).find((player) => player.id !== this.state.playerId)?.name ?? null; void this.authorize(); }; @@ -125,6 +191,31 @@ export class HandsApp { if (this.rematchCountdownTimer !== null) window.clearInterval(this.rematchCountdownTimer); this.rematchTimer = null; this.rematchCountdownTimer = null; + this.clearCpuRematch(); + } + + /** + * A rematch against the computer does not call it straight back: a friend who joins in the next + * few seconds takes the seat instead. A tap on a level calls the computer at once. + */ + private scheduleCpuRematch(level: CpuLevel): void { + this.clearCpuRematch(); + this.cpuRematch = { level, at: Date.now() + CPU_REMATCH_DELAY_MS }; + this.cpuRematchTimer = window.setInterval(() => { + const pending = this.cpuRematch; + if (pending === null || this.state.stage !== "waiting" || this.state.role !== "fighter" || this.cpuLevel !== null) this.clearCpuRematch(); + else if (Date.now() >= pending.at) { + this.clearCpuRematch(); + this.callCpu(pending.level); + } + this.renderState(); + }, 250); + } + + private clearCpuRematch(): void { + if (this.cpuRematchTimer !== null) window.clearInterval(this.cpuRematchTimer); + this.cpuRematchTimer = null; + this.cpuRematch = null; } /** Keeps the overlay out of the knockout replay's way until the result panel is on screen. */ @@ -144,12 +235,14 @@ export class HandsApp { private startRematchCountdown(): void { this.clearRematchTimers(); - if (this.state.role !== "fighter") return; + // A spectator can stay for the channel's next bout: the fighters keep their seats for a + // rematch, so he watches it, or takes a seat they leave empty. + const label = this.state.role === "fighter" ? "Rematch" : "Next bout"; const tick = (): void => { const remaining = Math.ceil((this.finalReceivedAt + REMATCH_HOLD_MS - Date.now()) / 1000); this.rematchButton.hidden = this.state.stage !== "complete"; this.rematchButton.disabled = remaining > 0; - this.setText(this.rematchButton, remaining > 0 ? `Rematch in ${remaining}s` : "Rematch"); + this.setText(this.rematchButton, remaining > 0 ? `${label} in ${remaining}s` : label); if (remaining <= 0 && this.rematchCountdownTimer !== null) { window.clearInterval(this.rematchCountdownTimer); this.rematchCountdownTimer = null; @@ -234,6 +327,10 @@ export class HandsApp { this.haptics.event(event); }; renderer.onArcadeInjury = (injury) => this.audio.injury(injury); + renderer.onAnnouncement = (lines) => this.voice.speak(lines); + renderer.onCrowdCue = () => this.audio.chant(); + // The muffle follows the hurt vision: from the punch that rocked him as the screen shows it. + renderer.onRocked = (level, tick) => this.audio.rocked(level, tick); this.renderer = renderer; } @@ -244,14 +341,26 @@ export class HandsApp { } if (message.type === "welcome") this.ensureRenderer(); if (message.type === "welcome" || message.type === "resumed") this.renderer?.resyncClock(); + const rematching = this.rematchAttempts > 0; if (message.type === "waiting" || message.type === "ready") this.rematchAttempts = 0; + if (message.type === "ready") this.rematchOpponent = null; + if (message.type === "ready") this.rememberStyle(message.players); if (message.type === "final" && this.rematchAttempts > 0 && message.match_id === this.lastFinalMatchId) { this.scheduleRematchRetry(); return; } this.dispatch({ type: "message", message }); if (message.type === "snapshot") this.receiveSnapshot(message.payload); + // A person took the computer's seat before the bell: the request is spent, and a later wait + // for an opponent does not bring the computer back by itself. + if ((message.type === "select" || message.type === "ready") && !message.players.some((player) => player.cpu === true)) this.cpuLevel = null; + if (message.type === "waiting") { + // Asked again after a reconnect lost the request; a rematch against the computer waits first. + if (this.cpuLevel !== null) this.callCpu(this.cpuLevel); + else if (rematching && this.cpuRematchLevel !== null) this.scheduleCpuRematch(this.cpuRematchLevel); + } if (message.type === "final") { + this.cpuRematchLevel = this.cpuBout ? this.cpuLevel : null; this.rematchAttempts = 0; this.lastFinalMatchId = message.match_id; this.finalReceivedAt = Date.now(); @@ -271,14 +380,17 @@ export class HandsApp { } private receiveSnapshot(snapshot: EngineSnapshot): void { + // The introductions belong to the countdown: at the opening bell the rest of the script goes + // unsaid, and a line still being read by a slow voice gets a moment to finish its name. + if (this.lastPhase === "countdown" && snapshot.phase !== "countdown") this.voice.finishLine(BELL_GRACE_MS); + this.lastPhase = snapshot.phase; this.renderer?.setInputLatency(this.network?.inputLatencyMs ?? null); this.renderer?.push(snapshot); const viewer = snapshot.fighters.find((fighter) => fighter.player_id === this.state.playerId); this.input.setKnockdown(viewer?.is_downed === true); - if (viewer !== undefined) { - this.audio.snapshot(snapshot.tick, viewer.stamina, viewer.maximum_stamina, viewer.trauma.head + viewer.trauma.body); - } + if (viewer !== undefined) this.audio.snapshot(snapshot.tick, viewer.stamina, viewer.maximum_stamina, viewer.trauma.head + viewer.trauma.body); this.audio.roundClock(snapshot.phase, snapshot.round_number, snapshot.phase_ticks_remaining, this.state.simulation?.tick_rate ?? 30); + this.audio.tension(crowdTension(snapshot)); for (const event of this.feedbackEvents.accept(snapshot.events)) { if (CONTACT_FEEDBACK_KINDS.has(event.kind)) continue; this.audio.event(event); @@ -286,6 +398,12 @@ export class HandsApp { } } + /** The style the room says this fighter boxes in, whether he settled on it or the deadline chose it, is offered first next time. */ + private rememberStyle(players: readonly PublicPlayer[]): void { + const style = players.find((player) => player.id === this.state.playerId)?.style; + if (this.state.role === "fighter" && style !== undefined) saveStyle(style); + } + private dispatch(action: Parameters[1]): void { this.state = reduceState(this.state, action); } @@ -299,20 +417,24 @@ export class HandsApp { bootstrapping: "Loading…", authorizing: "Authorizing with Discord…", connecting: "Connecting securely…", - waiting: "Waiting for one opponent to use Play now in this channel.", + waiting: this.state.role === "spectator" ? "Waiting for the fighters…" : this.cpuLevel !== null ? "Calling in the computer…" : this.cpuRematch !== null ? `Rematch with the ${CPU_CHOICES.find((choice) => choice.level === this.cpuRematch?.level)?.label ?? "computer"} in ${Math.max(1, Math.ceil((this.cpuRematch.at - Date.now()) / 1000))}s, unless someone joins first.` : this.rematchOpponent !== null ? `Waiting for ${this.rematchOpponent} to take the rematch…` : "Waiting for an opponent. Anyone in this channel can join with Play now.", + select: "Pick how your fighter boxes.", countdown: "Bout countdown.", fight: `Round ${this.state.snapshot?.round_number ?? 1} in progress.`, knockdown: `Knockdown count ${this.state.snapshot?.fighters.find((fighter) => fighter.player_id === this.state.playerId)?.get_up_count ?? 0}.`, foul_recovery: "Foul recovery in progress.", rest: "Between-round rest.", paused: `Connection paused. ${Math.ceil(this.state.reconnectMs / 1000)} seconds remain.`, - complete: "Bout complete. Scorecards and rating changes are displayed.", + complete: this.cpuBout ? "Bout complete. Scorecards are displayed. Bouts against the computer are unrated." : "Bout complete. Scorecards and rating changes are displayed.", fatal: describeError(this.state.safeError ?? "safe_error"), }; const spectating = this.state.role === "spectator"; this.setText(this.status, spectating ? `Spectating — ${labels[this.state.stage]}` : labels[this.state.stage]); - this.status.hidden = ["countdown", "fight", "knockdown", "foul_recovery", "rest"].includes(this.state.stage); + // The result card says how the bout ended; the button sits in the card's footer. + const resulted = this.state.stage === "complete" && this.state.final !== null; + this.status.hidden = resulted || ["select", "countdown", "fight", "knockdown", "foul_recovery", "rest"].includes(this.state.stage); this.overlay.toggleAttribute("data-raised", this.state.snapshot !== null); + this.overlay.toggleAttribute("data-result", resulted); // The engine's last snapshot arrives before the result does; the finish plays without the overlay. if (this.state.stage !== "complete") { this.overlay.hidden = false; @@ -343,12 +465,20 @@ export class HandsApp { ? "Connection paused." : labels[this.state.stage]; this.setText(this.liveFightStatus, liveStatus); + this.corner.update(this.state.snapshot, this.state.playerId, this.state.role === "fighter" && this.state.stage === "rest"); + this.renderer?.setCornerPanelTop(this.corner.top()); + this.stylePicker.update(this.state.stage === "select" ? this.state.select : null, this.state.playerId, this.state.role); this.retry.hidden = this.state.stage !== "fatal"; this.setText(this.retry, this.reloadOnRetry ? "Reload" : "Retry securely"); if (this.state.stage !== "complete") this.rematchButton.hidden = true; + this.cpuPicker.hidden = spectating || this.state.stage !== "waiting" || this.cpuLevel !== null; + // While a rematch waits for the other fighter, the computer is the fallback rather than the invitation. + this.setText(this.cpuPrompt, this.cpuRematch !== null ? "Or start now:" : this.rematchOpponent !== null ? "Or fight the computer instead:" : "No one here yet? Fight the computer."); const showHint = !spectating && (this.state.stage === "waiting" || this.state.stage === "countdown"); this.hint.hidden = !showHint; - if (showHint) this.setText(this.hint, coarsePointer() ? CONTROL_HINT_TOUCH : CONTROL_HINT_KEYBOARD); + if (showHint) this.setText(this.hint, controlHint(coarsePointer(), window.innerWidth)); + // Once the pads are up for the countdown, the touch hint moves to the empty stick side so it covers no pad. + this.overlay.toggleAttribute("data-touch-hint", showHint && this.state.stage === "countdown" && coarsePointer()); const active = !spectating && ["countdown", "fight", "knockdown", "foul_recovery"].includes(this.state.stage); // Between rounds and through pauses the touch controls stay up, inert, so a thumb already in place counts at the bell. const resting = !spectating && this.state.snapshot !== null && (this.state.stage === "rest" || this.state.stage === "paused"); @@ -357,7 +487,7 @@ export class HandsApp { this.renderFightSummary(); if (this.state.final !== null) { const final = this.state.final; - this.setText(this.finalSummary, `${final.method.replaceAll("_", " ")}. ${final.winner_id === null ? "Draw" : `${this.state.players[final.winner_id]?.name ?? "Winner"} wins`}. Scorecards: ${final.scorecards.map((card) => `${card.judge}: ${card.player_one.reduce((a, b) => a + b, 0)} to ${card.player_two.reduce((a, b) => a + b, 0)}`).join("; ")}. Ratings: ${Object.entries(final.ratings).map(([id, rating]) => `${this.state.players[id]?.name ?? "fighter"} ${rating.before} to ${rating.after}`).join("; ")}.`); + this.setText(this.finalSummary, `${final.method.replaceAll("_", " ")}. ${final.winner_id === null ? "Draw" : `${this.state.players[final.winner_id]?.name ?? "Winner"} wins`}. Scorecards: ${final.scorecards.map((card) => `${card.judge}: ${card.player_one.reduce((a, b) => a + b, 0)} to ${card.player_two.reduce((a, b) => a + b, 0)}`).join("; ")}. ${this.cpuBout ? "Unrated bout against the computer." : `Ratings: ${Object.entries(final.ratings).map(([id, rating]) => `${this.state.players[id]?.name ?? "fighter"} ${rating.before} to ${rating.after}`).join("; ")}.`}`); } } @@ -369,7 +499,7 @@ export class HandsApp { const clock = `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`; const fighters = snapshot.fighters.map((fighter) => { const player = this.state.players[fighter.player_id]; - return `${player?.name ?? "Fighter"}, ELO ${player?.rating ?? "unknown"}, stamina ${Math.round(fighter.stamina)} of ${Math.round(fighter.maximum_stamina)}, guard ${Math.round(fighter.guard)}, poise ${Math.round(fighter.poise)}, conditioning ${Math.round(fighter.conditioning)}, ${fighter.warnings} warnings, ${fighter.knockdowns} knockdowns`; + return `${player?.name ?? "Fighter"}, ${player?.cpu === true ? "computer opponent" : `ELO ${player?.rating ?? "unknown"}`}, stamina ${Math.round(fighter.stamina)} of ${Math.round(fighter.maximum_stamina)}, guard ${Math.round(fighter.guard)}, poise ${Math.round(fighter.poise)}, conditioning ${Math.round(fighter.conditioning)}, ${fighter.warnings} warnings, ${fighter.knockdowns} knockdowns`; }); const viewer = snapshot.fighters.find((fighter) => fighter.player_id === this.state.playerId); const getUp = viewer?.is_downed === true @@ -437,6 +567,7 @@ export class HandsApp { this.root.querySelector("[data-volume]")!.addEventListener("input", (event) => { this.settings.update({ volume: Number((event.target as HTMLInputElement).value) }); this.audio.setVolume(); + this.voice.settingsChanged(); }); this.root.querySelector("[data-haptics]")!.addEventListener("change", (event) => { this.settings.update({ haptics: (event.target as HTMLInputElement).checked }); @@ -446,11 +577,23 @@ export class HandsApp { this.settings.update({ reducedMotion }); this.renderer?.setReducedMotion(reducedMotion); }); + this.root.querySelector("[data-commentary]")!.addEventListener("change", (event) => { + this.settings.update({ commentary: (event.target as HTMLInputElement).checked }); + }); + this.root.querySelector("[data-announcer]")!.addEventListener("change", (event) => { + const announcer = (event.target as HTMLInputElement).checked; + this.settings.update({ announcer }); + if (!announcer) this.voice.cancel(); + }); this.root.querySelector("[data-blood]")!.addEventListener("change", (event) => { const blood = (event.target as HTMLSelectElement).value as BloodLevel; this.settings.update({ blood }); this.renderer?.setBloodLevel(blood); }); + this.root.querySelector("[data-camera]")!.addEventListener("change", (event) => { + const camera = CAMERA_MODES.find((mode) => mode === (event.target as HTMLSelectElement).value); + if (camera !== undefined) this.settings.update({ camera }); + }); } /** One block of text a player can paste into the server when reporting a problem. */ @@ -481,6 +624,21 @@ export class HandsApp { this.root.querySelector("[data-haptics]")!.checked = settings.haptics; this.root.querySelector("[data-motion]")!.checked = settings.reducedMotion; this.root.querySelector("[data-blood]")!.value = settings.blood; + this.root.querySelector("[data-commentary]")!.checked = settings.commentary; + this.syncAnnouncerSetting(); + this.voice.onVoicesChanged = () => this.syncAnnouncerSetting(); + this.root.querySelector("[data-camera]")!.value = settings.camera; + } + + /** The voice is offered only where the device can read the names itself; it never uses an online voice. */ + private syncAnnouncerSetting(): void { + const announcer = this.root.querySelector("[data-announcer]")!; + const usable = this.voice.hasVoice; + announcer.checked = this.settings.current.announcer && usable; + announcer.disabled = !usable; + const why = !this.voice.supported ? "This browser cannot speak" : usable ? null : "This device has no English voice of its own"; + if (why === null) announcer.parentElement!.removeAttribute("title"); + else announcer.parentElement!.title = why; } private fail(code: string): void { @@ -492,6 +650,8 @@ export class HandsApp { // A server on a newer protocol cannot be played from this page; Retry reloads it. if (code === "client_outdated") this.reloadOnRetry = true; this.dispatch({ type: "fatal", code }); + this.rematchOpponent = null; + this.voice.cancel(); this.network?.dispose(); this.network = null; this.session?.destroy(); @@ -512,11 +672,16 @@ export class HandsApp { this.session?.destroy(); if (!this.reloading) this.authorizer.close(); this.renderer?.destroy(); + window.removeEventListener("keydown", this.onCameraKey); this.input.destroy(); + this.corner.destroy(); + this.stylePicker.destroy(); this.audio.destroy(); + this.voice.destroy(); this.settings.destroy(); this.retry.removeEventListener("click", this.onRetry); this.rematchButton.removeEventListener("click", this.onRematch); + this.cpuPicker.removeEventListener("click", this.onCpuPick); this.clearRematchTimers(); if (this.diagnosticsTimer !== null) window.clearInterval(this.diagnosticsTimer); if (this.resultRevealTimer !== null) window.clearInterval(this.resultRevealTimer); diff --git a/web/hands/src/audio.ts b/web/hands/src/audio.ts index d8922dca..573edce5 100644 --- a/web/hands/src/audio.ts +++ b/web/hands/src/audio.ts @@ -1,5 +1,8 @@ import { INJURY_SOUNDS } from "./assets/injury-sounds"; import type { ArcadeInjury } from "./render/renderer"; + +/** Finishers without a voice of their own borrow the nearest one. */ +const BORROWED_VOICE: Partial>> = { head_burst: "decapitation", eye_left: "jaw_dislocation", eye_right: "jaw_dislocation", ribs_left: "shoulder_left", ribs_right: "shoulder_right" }; import type { Settings } from "./settings"; import type { CombatEvent, FinalMessage, PunchClass } from "./types"; @@ -10,8 +13,33 @@ interface NoiseSpec { readonly type?: BiquadFilterType; readonly q?: number; readonly sweepTo?: number; + readonly delay?: number; + /** Seconds into the noise loop to start from, so layered bursts do not line up. */ + readonly offset?: number; } +/** Many voices on one vowel: noise through two formant filters gliding between the given centres. */ +interface CrowdVoiceSpec { + readonly duration: number; + readonly first: readonly [number, number]; + readonly second: readonly [number, number]; + readonly gain: number; + readonly delay?: number; +} + +/** The crowd's "ooh" after a big shot lands: the vowel opens and falls away. */ +const OOH: CrowdVoiceSpec = { duration: 1.1, first: [330, 420], second: [760, 880], gain: 0.16 }; +/** The groan at a low blow, sliding down. */ +const GROAN: CrowdVoiceSpec = { duration: 1.2, first: [520, 360], second: [980, 720], gain: 0.13 }; +const BOO: CrowdVoiceSpec = { duration: 1.3, first: [300, 280], second: [640, 600], gain: 0.11, delay: 0.75 }; +const OOH_COOLDOWN_SECONDS = 2.5; +const CHANT_COOLDOWN_SECONDS = 20; +/** Murmur level at full tension. */ +const MURMUR_GAIN = 0.05; +/** The master bus is open to here, and muffled down to here when the player's fighter is badly rocked. */ +const MUFFLE_OPEN = 20_000; +const MUFFLE_CLOSED = 650; + interface ToneSpec { readonly from: number; readonly to?: number; @@ -31,6 +59,11 @@ const WHOOSH: Record uppercut: { from: 240, to: 820, duration: 0.15 }, }; +/** Ticks between heartbeats while rocked: racing at first, slowing as the fighter recovers. */ +export function rockedBeatTicks(level: number): number { + return Math.round(14 + (1 - Math.max(0, Math.min(1, level))) * 22); +} + export class AudioFeedback { private context: AudioContext | null = null; private master: GainNode | null = null; @@ -45,6 +78,14 @@ export class AudioFeedback { private lastBreathTick = -300; private lastHeartbeatTick = -300; private clapperRound = 0; + private murmurGain: GainNode | null = null; + private murmurLevel = 0; + private lastOohAt = -Infinity; + private lastChantAt = -Infinity; + private muffle: BiquadFilterNode | null = null; + private rockedLevel = 0; + private lastRockedBeat = -300; + private lastTick = -1; private readonly unlockListener = (): void => { // Every gesture resumes the context itself until audio is unlocked: a resume() made during a @@ -82,9 +123,14 @@ export class AudioFeedback { context = new AudioContext(); const master = context.createGain(); master.gain.value = Math.min(0.8, Math.max(0, this.settings().volume)); - master.connect(context.destination); + const muffle = context.createBiquadFilter(); + muffle.type = "lowpass"; + muffle.frequency.value = MUFFLE_OPEN; + muffle.Q.value = 0.7; + master.connect(muffle).connect(context.destination); this.context = context; this.master = master; + this.muffle = muffle; } if (context.state === "suspended") await context.resume(); if (this.destroyed || this.context !== context) return; @@ -122,6 +168,7 @@ export class AudioFeedback { const loud = Math.min(0.36, 0.14 + weight * 0.2) * (event.kind === "counter_hit" ? 1.25 : 1); this.impact(loud, body, weight); if (event.kind === "counter_hit" || weight > 0.7) this.crowdSwell(0.35 + weight * 0.4); + if ((event.kind === "counter_hit" && event.amount >= 50) || event.amount >= 90) this.ooh(Math.min(1, event.amount / 160)); break; } case "block": @@ -137,16 +184,34 @@ export class AudioFeedback { this.impact(0.3, false, 0.9); this.tone({ from: 230, to: 78, duration: 0.2, type: "sawtooth", gain: 0.08 }); this.crowdSwell(0.5); + this.ooh(0.6); break; case "stun": this.tone({ from: 96, to: 74, duration: 0.22, type: "sine", gain: 0.1 }); this.crowdSwell(0.3); break; + case "parry": + this.noise({ duration: 0.06, frequency: 2400, sweepTo: 1100, gain: 0.18, type: "bandpass", q: 1.8 }); + this.tone({ from: 330, to: 180, duration: 0.09, type: "triangle", gain: 0.07 }); + this.crowdSwell(0.35); + break; + case "body_collapse": + this.tone({ from: 118, to: 52, duration: 0.42, type: "sine", gain: 0.16 }); + this.noise({ duration: 0.26, frequency: 420, sweepTo: 180, gain: 0.05, type: "bandpass", q: 0.9 }); + this.crowdSwell(0.6); + break; + case "eye_shut": + this.crowdSwell(0.25); + break; + case "corner": + this.noise({ duration: 0.22, frequency: 2600, sweepTo: 1500, gain: 0.05, type: "bandpass", q: 0.8 }); + break; case "knockdown": this.impact(0.34, false, 1); this.tone({ from: 60, to: 28, duration: 0.42, type: "sine", gain: 0.3 }); this.noise({ duration: 0.3, frequency: 140, gain: 0.24 }); this.crowdRoar(1); + this.hush(); break; case "bell": { const strikes = event.detail === "round_start" ? 3 : 1; @@ -158,8 +223,13 @@ export class AudioFeedback { break; case "get_up": this.tone({ from: 420, to: 840, duration: 0.16, type: "triangle", gain: 0.1 }); + this.crowdRoar(0.9); break; case "foul": + this.whistle(); + if (event.detail === "low_blow") this.crowdVoice(GROAN); + this.crowdVoice(BOO); + break; case "referee_break": this.whistle(); break; @@ -188,6 +258,32 @@ export class AudioFeedback { } } + /** + * A new bout starts its clock at zero again: the heartbeats, the breathing and the clapper start over, + * and the world is no longer muffled. + */ + reset(): void { + this.lastBreathTick = -300; + this.lastHeartbeatTick = -300; + this.lastRockedBeat = -300; + this.clapperRound = 0; + this.lastTick = -1; + this.openMuffle(); + } + + private openMuffle(): void { + if (this.rockedLevel === 0) return; + this.rockedLevel = 0; + const context = this.context; + if (context !== null && this.muffle !== null) this.muffle.frequency.setTargetAtTime(MUFFLE_OPEN, context.currentTime, 0.9); + } + + /** A tick earlier than the last one heard is a new bout's clock: the old bout's timers would silence it. */ + private follow(tick: number): void { + if (tick < this.lastTick) this.reset(); + this.lastTick = tick; + } + /** The ten-second clapper: two wood-block cracks once per round when ten seconds remain. */ roundClock(phase: string, roundNumber: number, ticksRemaining: number, tickRate: number): void { if (phase !== "fight" || ticksRemaining > 10 * tickRate || this.clapperRound === roundNumber) return; @@ -203,20 +299,67 @@ export class AudioFeedback { this.timers.add(timer); } + /** The crowd takes up a rhythmic clap behind a fighter who has taken over. */ + chant(): void { + const context = this.context; + if (!this.unlocked || context === null || context.currentTime - this.lastChantAt < CHANT_COOLDOWN_SECONDS) return; + this.lastChantAt = context.currentTime; + for (let beat = 0; beat < 8; beat += 1) { + const swell = 0.5 + beat / 14; + for (let layer = 0; layer < 3; layer += 1) { + this.noise({ duration: 0.045, frequency: 1500 + layer * 450, gain: 0.07 * swell, type: "bandpass", q: 1.1, delay: beat * 0.42 + layer * 0.013, offset: (beat * 3 + layer) * 0.131 }); + } + } + this.crowdVoice({ duration: 1.4, first: [380, 460], second: [820, 940], gain: 0.08, delay: 3.2 }); + } + + /** The murmur under the crowd: 0 calm, 1 with a fighter in trouble or a count running. */ + tension(level: number): void { + const context = this.context; + const murmur = this.murmurGain; + const target = Math.max(0, Math.min(1, level)); + if (context === null || murmur === null || Math.abs(target - this.murmurLevel) < 0.02) return; + const rising = target > this.murmurLevel; + this.murmurLevel = target; + murmur.gain.setTargetAtTime(target * MURMUR_GAIN, context.currentTime, rising ? 0.3 : 0.8); + } + + /** The player's own fighter is rocked: the world goes muffled and the heartbeat pounds, slowing as the head clears. */ + rocked(level: number, tick: number): void { + this.follow(tick); + const context = this.context; + const muffle = this.muffle; + if (!this.unlocked || context === null || muffle === null) return; + const target = Math.max(0, Math.min(1, level)); + if (Math.abs(target - this.rockedLevel) >= 0.02 || (target === 0 && this.rockedLevel !== 0)) { + const rising = target > this.rockedLevel; + this.rockedLevel = target; + muffle.frequency.setTargetAtTime(MUFFLE_OPEN * Math.pow(MUFFLE_CLOSED / MUFFLE_OPEN, target), context.currentTime, rising ? 0.05 : 0.9); + } + if (target > 0.15 && tick - this.lastRockedBeat >= rockedBeatTicks(target)) { + this.lastRockedBeat = tick; + this.tone({ from: 58, to: 40, duration: 0.12, type: "sine", gain: 0.09 + target * 0.08 }); + this.tone({ from: 50, to: 36, duration: 0.14, type: "sine", gain: 0.07 + target * 0.06, delay: 0.16 }); + } + } + snapshot(tick: number, stamina: number, maximumStamina: number, trauma: number): void { + this.follow(tick); if (!this.unlocked) return; const fatigue = 1 - stamina / Math.max(1, maximumStamina); if (fatigue > 0.55 && tick - this.lastBreathTick >= 75) { this.lastBreathTick = tick; this.noise({ duration: 0.18, frequency: 420, gain: 0.045 + fatigue * 0.04 }); } - if ((fatigue > 0.72 || trauma > 500) && tick - this.lastHeartbeatTick >= 24) { + if ((fatigue > 0.72 || trauma > 500) && this.rockedLevel <= 0.15 && tick - this.lastHeartbeatTick >= 24) { this.lastHeartbeatTick = tick; this.tone({ from: 52, duration: 0.09, type: "sine", gain: 0.055 }); } } result(final: FinalMessage): void { + // The bout is over, however it ended (a forfeit sends no last snapshot to clear it). + this.openMuffle(); if (!this.unlocked) return; this.crowdSwell(1); this.tone({ from: final.winner_id === null ? 280 : 520, duration: 0.45, type: "triangle", gain: 0.18 }); @@ -230,7 +373,7 @@ export class AudioFeedback { injury(injury: ArcadeInjury): void { const context = this.context; const master = this.master; - const buffer = this.injuryBuffers.get(injury); + const buffer = this.injuryBuffers.get(BORROWED_VOICE[injury] ?? injury); const current = this.settings(); if (!this.unlocked || context === null || master === null || buffer === undefined) return; if (current.blood !== "full" || current.reducedMotion) return; @@ -276,11 +419,58 @@ export class AudioFeedback { const crowd = this.crowdGain; if (crowd !== null) { const now = context.currentTime; + crowd.gain.cancelScheduledValues(now); crowd.gain.setValueAtTime(Math.min(0.16, 0.06 + intensity * 0.1), now); crowd.gain.exponentialRampToValueAtTime(0.022, now + 4.5); } } + /** After the roar at a knockdown the arena goes quiet for the count. */ + private hush(): void { + const context = this.context; + const crowd = this.crowdGain; + if (context === null || crowd === null) return; + const now = context.currentTime; + crowd.gain.cancelScheduledValues(now); + crowd.gain.setValueAtTime(0.16, now); + crowd.gain.exponentialRampToValueAtTime(0.1, now + 1.2); + crowd.gain.exponentialRampToValueAtTime(0.004, now + 3); + } + + private ooh(weight: number): void { + const context = this.context; + if (context === null || context.currentTime - this.lastOohAt < OOH_COOLDOWN_SECONDS) return; + this.lastOohAt = context.currentTime; + this.crowdVoice({ ...OOH, gain: OOH.gain * (0.6 + 0.4 * weight), delay: 0.12 }); + } + + private crowdVoice(spec: CrowdVoiceSpec): void { + const context = this.context; + const master = this.master; + if (context === null || master === null) return; + const buffer = this.noiseLoop(context); + const start = context.currentTime + (spec.delay ?? 0); + const end = start + spec.duration; + const source = context.createBufferSource(); + source.buffer = buffer; + source.loop = true; + const envelope = context.createGain(); + envelope.gain.setValueAtTime(0.0001, start); + envelope.gain.exponentialRampToValueAtTime(Math.min(0.3, spec.gain), start + spec.duration * 0.22); + envelope.gain.exponentialRampToValueAtTime(0.0001, end); + for (const [from, to] of [spec.first, spec.second]) { + const formant = context.createBiquadFilter(); + formant.type = "bandpass"; + formant.Q.value = 5; + formant.frequency.setValueAtTime(from, start); + formant.frequency.exponentialRampToValueAtTime(to, end); + source.connect(formant).connect(envelope); + } + envelope.connect(master); + source.start(start, (spec.duration * 0.37) % 1); + source.stop(end + 0.05); + } + private tone(spec: ToneSpec): void { const context = this.context; const master = this.master; @@ -301,10 +491,7 @@ export class AudioFeedback { oscillator.stop(start + Math.min(1.4, spec.duration) + 0.03); } - private noise(spec: NoiseSpec): void { - const context = this.context; - const master = this.master; - if (context === null || master === null) return; + private noiseLoop(context: AudioContext): AudioBuffer { if (this.noiseBuffer === null) { const length = Math.floor(context.sampleRate * 1.2); const buffer = context.createBuffer(1, length, context.sampleRate); @@ -319,12 +506,20 @@ export class AudioFeedback { } this.noiseBuffer = buffer; } - const start = context.currentTime; + return this.noiseBuffer; + } + + private noise(spec: NoiseSpec): void { + const context = this.context; + const master = this.master; + if (context === null || master === null) return; + const loop = this.noiseLoop(context); + const start = context.currentTime + (spec.delay ?? 0); const duration = Math.min(0.5, spec.duration); const source = context.createBufferSource(); const filter = context.createBiquadFilter(); const envelope = context.createGain(); - source.buffer = this.noiseBuffer; + source.buffer = loop; source.loop = true; filter.type = spec.type ?? "lowpass"; filter.frequency.setValueAtTime(Math.max(60, Math.min(4000, spec.frequency)), start); @@ -336,7 +531,7 @@ export class AudioFeedback { envelope.gain.exponentialRampToValueAtTime(Math.min(0.35, spec.gain), start + 0.008); envelope.gain.exponentialRampToValueAtTime(0.0001, start + duration); source.connect(filter).connect(envelope).connect(master); - source.start(); + source.start(start, (spec.offset ?? 0) % 1.1); source.stop(start + duration + 0.02); } @@ -359,6 +554,7 @@ export class AudioFeedback { const crowd = this.crowdGain; if (crowd !== null) { const now = context.currentTime; + crowd.gain.cancelScheduledValues(now); crowd.gain.setValueAtTime(Math.min(0.09, 0.03 + intensity * 0.05), now); crowd.gain.exponentialRampToValueAtTime(0.022, now + 1.4); } @@ -397,6 +593,14 @@ export class AudioFeedback { source.connect(filter).connect(gain).connect(master); source.start(); lfo.start(); + const murmurFilter = context.createBiquadFilter(); + murmurFilter.type = "bandpass"; + murmurFilter.frequency.value = 620; + murmurFilter.Q.value = 0.9; + const murmur = context.createGain(); + murmur.gain.value = 0; + this.murmurGain = murmur; + source.connect(murmurFilter).connect(murmur).connect(master); } destroy(): void { @@ -411,6 +615,8 @@ export class AudioFeedback { this.noiseBuffer = null; this.injuryBuffers = new Map(); this.crowdGain = null; + this.murmurGain = null; + this.muffle = null; this.unlocked = false; if (context !== null) void context.close().catch(() => undefined); } diff --git a/web/hands/src/corner.test.ts b/web/hands/src/corner.test.ts new file mode 100644 index 00000000..a7de06f9 --- /dev/null +++ b/web/hands/src/corner.test.ts @@ -0,0 +1,195 @@ +import { CORNER_TREATMENTS, EYE_SHUT_TRAUMA } from "./manifest"; +import { CORNER_PAD_BUTTONS, CornerPanel, cornerPreview, cutWord } from "./corner"; +import { fighter, snapshot } from "./test/fixtures"; +import type { CornerChoice, CornerKind, EngineSnapshot } from "./types"; + +/** A fighter with a cut and a swollen eye, so every instruction has something to work on. */ +const resting = (choice: CornerChoice | null = null, trauma: Partial = {}): EngineSnapshot => { + const base = snapshot(); + return { ...base, phase: "rest", phase_ticks_remaining: 300, fighters: [{ ...base.fighters[0], corner_choice: choice, trauma: { ...base.fighters[0].trauma, left_cut: 200, left_eye: 300, ...trauma } }, base.fighters[1]] }; +}; + +function panel(pick: (kind: CornerKind) => boolean = () => true, now: () => number = () => 0): { panel: CornerPanel; root: HTMLElement; buttons: () => HTMLButtonElement[]; status: () => string } { + const root = document.createElement("div"); + document.body.append(root); + const created = new CornerPanel(root, pick, now); + return { + panel: created, + root, + buttons: () => [...root.querySelectorAll("[data-corner-pick]")], + status: () => root.querySelector(".corner-status")?.textContent ?? "", + }; +} + +const press = (code: string): void => { window.dispatchEvent(new KeyboardEvent("keydown", { code })); }; + +describe("the corner between rounds", () => { + afterEach(() => { document.body.replaceChildren(); vi.restoreAllMocks(); }); + + it("is shown only to a fighter, and only during the rest", () => { + const { panel: corner, root } = panel(); + corner.update(resting(), "one", true); + expect(root.querySelector("[data-corner]")!.hidden).toBe(false); + corner.update(resting(), "one", false); + expect(root.querySelector("[data-corner]")!.hidden).toBe(true); + corner.update(snapshot(), "one", true); + expect(root.querySelector("[data-corner]")!.hidden).toBe(true); + corner.update(resting(), "nobody", true); + expect(root.querySelector("[data-corner]")!.hidden).toBe(true); + corner.destroy(); + }); + + it("calls a click to the corner once and waits for the server", () => { + const picks: CornerKind[] = []; + const { panel: corner, buttons, status } = panel((kind) => { picks.push(kind); return true; }); + corner.update(resting(), "one", true); + buttons()[1]!.click(); + buttons()[0]!.click(); + expect(picks).toEqual(["corner_swelling"]); + expect(buttons().map((button) => button.disabled)).toEqual([true, true, true]); + expect(buttons().map((button) => button.hasAttribute("data-picked"))).toEqual([false, true, false]); + expect(status()).toMatch(/calling/i); + corner.destroy(); + }); + + it("takes the number keys only while it is on screen", () => { + const picks: CornerKind[] = []; + const { panel: corner } = panel((kind) => { picks.push(kind); return true; }); + const during = new KeyboardEvent("keydown", { code: "Digit1", cancelable: true }); + window.dispatchEvent(during); + expect(during.defaultPrevented).toBe(false); + press("Digit3"); + expect(picks).toEqual([]); + corner.update(resting(), "one", true); + press("Numpad3"); + expect(picks).toEqual(["corner_breath"]); + corner.destroy(); + press("Digit1"); + expect(picks).toEqual(["corner_breath"]); + }); + + it("shows what the corner is doing once the server confirms it", () => { + const { panel: corner, buttons, status } = panel(); + corner.update(resting("cut"), "one", true); + expect(status()).toBe("Your corner is closing the cut."); + expect(buttons().map((button) => button.disabled)).toEqual([true, true, true]); + expect(buttons().map((button) => button.hasAttribute("data-picked"))).toEqual([true, false, false]); + corner.destroy(); + }); + + it("offers the choice again when the server never confirms it", () => { + let time = 0; + const picks: CornerKind[] = []; + const { panel: corner, buttons } = panel((kind) => { picks.push(kind); return true; }, () => time); + corner.update(resting(), "one", true); + buttons()[0]!.click(); + time = 1000; + corner.update(resting(), "one", true); + expect(buttons()[2]!.disabled).toBe(true); + time = 1600; + corner.update(resting(), "one", true); + expect(buttons().map((button) => button.disabled)).toEqual([false, false, false]); + buttons()[2]!.click(); + expect(picks).toEqual(["corner_cut", "corner_breath"]); + corner.destroy(); + }); + + it("stays open when the instruction could not be sent", () => { + const { panel: corner, buttons } = panel(() => false); + corner.update(resting(), "one", true); + buttons()[0]!.click(); + expect(buttons().map((button) => button.disabled)).toEqual([false, false, false]); + corner.destroy(); + }); + + it("maps the left, top and right face buttons to the cut, the swelling and the breath", () => { + expect(CORNER_PAD_BUTTONS).toEqual({ 2: "corner_cut", 3: "corner_swelling", 1: "corner_breath" }); + }); + + it("tells the caption where its top edge is while it is up", () => { + const { panel: corner } = panel(() => true); + expect(corner.top()).toBeNull(); + corner.update(resting(), "one", true); + corner.element.getBoundingClientRect = () => ({ top: 520, height: 130 }) as DOMRect; + corner.element.parentElement!.getBoundingClientRect = () => ({ top: 20, height: 720 }) as DOMRect; + expect(corner.top()).toBe(500); + corner.update(resting(), "one", false); + expect(corner.top()).toBeNull(); + corner.destroy(); + }); + + it("takes the controller's face buttons", () => { + const frames: FrameRequestCallback[] = []; + vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => { frames.push(callback); return frames.length; }); + vi.spyOn(window, "cancelAnimationFrame").mockImplementation(() => undefined); + const button = (kind: CornerKind): number => Number(Object.entries(CORNER_PAD_BUTTONS).find(([, candidate]) => candidate === kind)![0]); + const held = new Set(); + const pad = { connected: true, mapping: "standard", get buttons() { return Array.from({ length: 16 }, (_unused, index) => ({ pressed: held.has(index) })); } } as unknown as Gamepad; + Object.defineProperty(navigator, "getGamepads", { configurable: true, value: () => [pad] }); + const picks: CornerKind[] = []; + const { panel: corner } = panel((kind) => { picks.push(kind); return true; }); + // A punch button still held from the last exchange as the bell rings is not a pick. + held.add(button("corner_cut")); + corner.update(resting(), "one", true); + frames.shift()!(0); + expect(picks).toEqual([]); + held.delete(button("corner_cut")); + frames.shift()!(0); + held.add(button("corner_swelling")); + frames.shift()!(0); + expect(picks).toEqual(["corner_swelling"]); + corner.destroy(); + Object.defineProperty(navigator, "getGamepads", { configurable: true, value: undefined }); + }); +}); + +describe("what each instruction would do", () => { + it("previews the cut, the eye and the health in plain words and percentages", () => { + const hurt = { ...fighter("one"), conditioning: 500, trauma: { ...fighter("one").trauma, left_cut: 600, right_cut: 200, right_eye: EYE_SHUT_TRAUMA + 20, swelling: 300 } }; + const preview = cornerPreview(hurt); + expect(preview.cut).toBe(`Dangerous cut → ${cutWord(600 - CORNER_TREATMENTS.cut.worse_cut)}`); + expect(preview.swelling).toBe(`Eye 100% swollen → ${Math.round(((EYE_SHUT_TRAUMA + 20 - CORNER_TREATMENTS.swelling.eyes) / EYE_SHUT_TRAUMA) * 100)}% · opens the eye`); + expect(preview.breath).toBe(`Health 50% → ${Math.round(((500 + CORNER_TREATMENTS.breath.conditioning) / 1000) * 100)}% · full stamina`); + expect(cornerPreview({ ...hurt, trauma: { ...hurt.trauma, left_cut: 60, right_cut: 0 } }).cut).toBe("Small cut → closed"); + expect(Object.values(preview).some((text) => /\d{3}/u.test(text) && !text.includes("%"))).toBe(false); + }); + + it("will not take an instruction with nothing to work on", () => { + const picks: string[] = []; + const parent = document.createElement("div"); + const corner = new CornerPanel(parent, (kind) => { picks.push(kind); return true; }); + const resting = { ...snapshot(), phase: "rest" as const, fighters: [fighter("one"), fighter("two")] as const }; + corner.update(resting, "one", true); + expect(parent.querySelector('[data-corner-pick="corner_cut"]')!.disabled).toBe(true); + expect(parent.querySelector('[data-corner-pick="corner_swelling"]')!.disabled).toBe(true); + expect(parent.querySelector('[data-corner-pick="corner_breath"]')!.disabled).toBe(false); + window.dispatchEvent(new KeyboardEvent("keydown", { code: "Digit1", bubbles: true })); + expect(picks).toEqual([]); + window.dispatchEvent(new KeyboardEvent("keydown", { code: "Digit3", bubbles: true })); + expect(picks).toEqual(["corner_breath"]); + corner.destroy(); + }); + + it("keeps the picked card's preview as it was when it was picked", () => { + const parent = document.createElement("div"); + const corner = new CornerPanel(parent, () => true); + const cut = { ...fighter("one"), trauma: { ...fighter("one").trauma, left_cut: 300 } }; + corner.update({ ...snapshot(), phase: "rest", fighters: [cut, fighter("two")] }, "one", true); + const card = parent.querySelector('[data-corner-pick="corner_cut"]')!; + const before = card.querySelector("span")!.textContent; + card.click(); + const treated = { ...cut, corner_choice: "cut" as const, trauma: { ...cut.trauma, left_cut: 0 } }; + corner.update({ ...snapshot(), phase: "rest", fighters: [treated, fighter("two")] }, "one", true); + expect(card.querySelector("span")!.textContent).toBe(before); + expect(card.querySelector("span")!.textContent).not.toBe("No cut to close"); + corner.destroy(); + }); + + it("says when there is nothing to treat, and when an eye stays shut", () => { + const fresh = cornerPreview(fighter("one")); + expect(fresh.cut).toBe("No cut to close"); + expect(fresh.swelling).toBe("No swelling"); + const closed = cornerPreview({ ...fighter("one"), trauma: { ...fighter("one").trauma, left_eye: 1000 } }); + expect(closed.swelling).toMatch(/stays shut$/u); + }); +}); diff --git a/web/hands/src/corner.ts b/web/hands/src/corner.ts new file mode 100644 index 00000000..fd7ac96b --- /dev/null +++ b/web/hands/src/corner.ts @@ -0,0 +1,225 @@ +import { CORNER_TREATMENTS, EYE_SHUT_TRAUMA } from "./manifest"; +import type { CornerChoice, CornerKind, EngineSnapshot, FighterSnapshot } from "./types"; + +export const CORNER_KEYS: Readonly> = { + Digit1: "corner_cut", Numpad1: "corner_cut", + Digit2: "corner_swelling", Numpad2: "corner_swelling", + Digit3: "corner_breath", Numpad3: "corner_breath", +}; +/** Standard-mapping face buttons: left (X, square) the cut, top (Y, triangle) the swelling, right (B, circle) the breath. */ +export const CORNER_PAD_BUTTONS: Readonly> = { 2: "corner_cut", 3: "corner_swelling", 1: "corner_breath" }; + +const CHOICES: readonly { readonly kind: CornerKind; readonly choice: CornerChoice; readonly key: string; readonly title: string }[] = [ + { kind: "corner_cut", choice: "cut", key: "1", title: "Close the cut" }, + { kind: "corner_swelling", choice: "swelling", key: "2", title: "Bring down the swelling" }, + { kind: "corner_breath", choice: "breath", key: "3", title: "Catch your breath" }, +]; +const WORKING: Readonly> = { + cut: "Your corner is closing the cut.", + swelling: "Your corner is bringing the swelling down.", + breath: "Your corner is getting your breath back.", + balanced: "Your corner did a little of everything.", +}; +/** A pick the server has not confirmed by then is offered again. */ +const PENDING_MS = 1500; +const MAX_CONDITIONING = 1000; + +/** A cut in the words a corner would use; the ringside doctor stops a bout at about 800. */ +export function cutWord(cut: number): string { + return cut < 150 ? "small" : cut < 350 ? "nasty" : cut < 550 ? "deep" : "dangerous"; +} + +const percent = (value: number, of: number): number => Math.round(Math.max(0, Math.min(1, value / of)) * 100); + +/** What each instruction would do for this fighter, in plain words: an eye at 100% is swollen shut. */ +export function cornerPreview(fighter: FighterSnapshot): Readonly> { + const { trauma } = fighter; + const worseCut = Math.max(trauma.left_cut, trauma.right_cut); + const cutAfter = Math.max(0, worseCut - CORNER_TREATMENTS.cut.worse_cut); + const worseEye = Math.max(trauma.left_eye, trauma.right_eye); + const eyeAfter = Math.max(0, worseEye - CORNER_TREATMENTS.swelling.eyes); + const shut = worseEye >= EYE_SHUT_TRAUMA ? (eyeAfter < EYE_SHUT_TRAUMA ? " · opens the eye" : " · stays shut") : ""; + const cutNow = cutWord(worseCut); + return { + cut: worseCut === 0 ? "No cut to close" : `${cutNow[0]!.toUpperCase()}${cutNow.slice(1)} cut → ${cutAfter === 0 ? "closed" : cutWord(cutAfter)}`, + swelling: worseEye === 0 && trauma.swelling === 0 ? "No swelling" : `Eye ${percent(worseEye, EYE_SHUT_TRAUMA)}% swollen → ${percent(eyeAfter, EYE_SHUT_TRAUMA)}%${shut}`, + breath: `Health ${percent(fighter.conditioning, MAX_CONDITIONING)}% → ${percent(fighter.conditioning + CORNER_TREATMENTS.breath.conditioning, MAX_CONDITIONING)}% · full stamina`, + balanced: "", + }; +} + +/** An instruction with nothing to work on cannot be picked. */ +export function cornerAvailable(fighter: FighterSnapshot): Readonly> { + const { trauma } = fighter; + return { + cut: Math.max(trauma.left_cut, trauma.right_cut) > 0, + swelling: Math.max(trauma.left_eye, trauma.right_eye) > 0 || trauma.swelling > 0, + breath: true, + balanced: false, + }; +} + +/** The fighter's instructions to the corner between rounds: three buttons, the number keys and the face buttons. */ +function standardPad(): Gamepad | null { + try { + return [...(navigator.getGamepads?.() ?? [])].find((item): item is Gamepad => item !== null && item.connected && item.mapping === "standard") ?? null; + } catch { + return null; + } +} + +export class CornerPanel { + readonly element: HTMLElement; + private readonly buttons = new Map(); + private readonly details = new Map(); + private readonly status: HTMLElement; + private visible = false; + private choice: CornerChoice | null = null; + private pending: { readonly kind: CornerKind; readonly at: number } | null = null; + /** What the cards said when the pick was made: the treatment changes the fighter, not what was chosen. */ + private shown: Readonly> | null = null; + private frozen: Readonly> | null = null; + private available: Readonly> | null = null; + private padFrame = 0; + private readonly padPrevious = new Set(); + private measuredTop: number | null = null; + private readonly resize = (): void => { + this.measuredTop = null; + }; + + private readonly keydown = (event: KeyboardEvent): void => { + const kind = CORNER_KEYS[event.code]; + if (kind === undefined || !this.visible || event.repeat) return; + event.preventDefault(); + this.choose(kind); + }; + + constructor(parent: HTMLElement, private readonly pick: (kind: CornerKind) => boolean, private readonly now: () => number = () => performance.now()) { + const section = document.createElement("section"); + section.className = "corner"; + section.dataset.corner = ""; + section.hidden = true; + section.setAttribute("aria-label", "Instructions for your corner"); + const title = document.createElement("p"); + title.className = "corner-title"; + title.textContent = "YOUR CORNER"; + const row = document.createElement("div"); + row.className = "corner-choices"; + for (const { kind, key, title: label } of CHOICES) { + const button = document.createElement("button"); + button.type = "button"; + button.dataset.cornerPick = kind; + const keyLabel = document.createElement("kbd"); + keyLabel.textContent = key; + const name = document.createElement("strong"); + name.textContent = label; + const detail = document.createElement("span"); + button.append(keyLabel, name, detail); + button.addEventListener("click", () => this.choose(kind)); + row.append(button); + this.buttons.set(kind, button); + this.details.set(kind, detail); + } + this.status = document.createElement("p"); + this.status.className = "corner-status"; + this.status.setAttribute("role", "status"); + section.append(title, row, this.status); + parent.append(section); + this.element = section; + window.addEventListener("keydown", this.keydown); + window.addEventListener("resize", this.resize); + } + + /** Shows the panel to a fighter during the rest and reflects the corner's work from the latest snapshot. */ + update(snapshot: EngineSnapshot | null, viewerId: string | null, fighter: boolean): void { + const viewer = snapshot?.fighters.find((candidate) => candidate.player_id === viewerId); + const show = fighter && snapshot !== null && snapshot.phase === "rest" && viewer !== undefined; + if (!show || viewer === undefined) { + this.setVisible(false); + this.choice = null; + this.pending = null; + this.shown = null; + this.frozen = null; + return; + } + this.setVisible(true); + this.choice = viewer.corner_choice; + if (this.choice !== null || (this.pending !== null && this.now() - this.pending.at > PENDING_MS)) this.pending = null; + const picked = this.choice !== null || this.pending !== null; + if (!picked) this.frozen = null; + const preview = picked && this.frozen !== null ? this.frozen : cornerPreview(viewer); + this.shown = preview; + const available = cornerAvailable(viewer); + this.available = available; + for (const { kind, choice } of CHOICES) { + const button = this.buttons.get(kind)!; + const detail = this.details.get(kind)!; + if (detail.textContent !== preview[choice]) detail.textContent = preview[choice]; + button.disabled = picked || !available[choice]; + button.toggleAttribute("data-picked", this.choice === choice || this.pending?.kind === kind); + } + const text = this.choice !== null ? WORKING[this.choice] : this.pending !== null ? "Calling it to your corner…" : "Tell your corner what to work on before the bell."; + if (this.status.textContent !== text) this.status.textContent = text; + } + + private choose(kind: CornerKind): void { + if (!this.visible || this.choice !== null || this.pending !== null) return; + const choice = CHOICES.find((candidate) => candidate.kind === kind)?.choice; + if (choice === undefined || this.available?.[choice] === false) return; + if (!this.pick(kind)) return; + this.frozen = this.shown; + this.pending = { kind, at: this.now() }; + for (const [candidate, button] of this.buttons) { + button.disabled = true; + button.toggleAttribute("data-picked", candidate === kind); + } + this.status.textContent = "Calling it to your corner…"; + } + + private setVisible(visible: boolean): void { + if (visible === this.visible) return; + this.visible = visible; + this.element.hidden = !visible; + this.measuredTop = null; + this.padPrevious.clear(); + if (visible) { + // A button already held when the panel opens (a punch thrown at the bell) is not a pick. + const pad = standardPad(); + for (const index of Object.keys(CORNER_PAD_BUTTONS)) if (pad?.buttons[Number(index)]?.pressed === true) this.padPrevious.add(Number(index)); + this.padFrame = requestAnimationFrame(() => this.pollPad()); + } else cancelAnimationFrame(this.padFrame); + } + + /** How far down the activity the panel's top edge is while it is up, so a caption can keep above it; null while hidden. */ + top(): number | null { + if (!this.visible) return null; + if (this.measuredTop === null) { + const rect = this.element.getBoundingClientRect(); + if (rect.height <= 0) return null; + this.measuredTop = rect.top - (this.element.parentElement?.getBoundingClientRect().top ?? 0); + } + return this.measuredTop; + } + + private pollPad(): void { + if (!this.visible) return; + const pad = standardPad(); + if (pad !== null) { + for (const [index, kind] of Object.entries(CORNER_PAD_BUTTONS)) { + const button = Number(index); + const down = pad.buttons[button]?.pressed === true; + if (down && !this.padPrevious.has(button)) this.choose(kind); + if (down) this.padPrevious.add(button); + else this.padPrevious.delete(button); + } + } + this.padFrame = requestAnimationFrame(() => this.pollPad()); + } + + destroy(): void { + this.setVisible(false); + window.removeEventListener("keydown", this.keydown); + window.removeEventListener("resize", this.resize); + this.element.remove(); + } +} diff --git a/web/hands/src/dev-fixtures.ts b/web/hands/src/dev-fixtures.ts index 995e160c..1b431726 100644 --- a/web/hands/src/dev-fixtures.ts +++ b/web/hands/src/dev-fixtures.ts @@ -3,20 +3,28 @@ import * as THREE from "three"; import { SharedActionIntent } from "./input/action-buffer"; import { TouchInput, coarsePointer } from "./input/touch"; import { FightRenderer } from "./render/renderer"; -import type { CombatEvent, EngineSnapshot, FighterSnapshot, PublicPlayer } from "./types"; +import { CAMERA_MODES } from "./settings"; +import { FIGHTER_STYLES } from "./protocol"; +import { PROTOCOL_VERSION, type CombatEvent, type EngineSnapshot, type FighterSnapshot, type FighterStyle, type PublicPlayer } from "./types"; type Draft = { -readonly [K in keyof FighterSnapshot]: FighterSnapshot[K] }; +// `styles=,` boxes the fixture fighters in those styles, for the plates and the introduction. +const fixtureStyles = (new URLSearchParams(window.location.search).get("styles") ?? "").split(","); +const fixtureStyle = (id: string): FighterStyle => { + const wanted = fixtureStyles[id === "fixture-one" ? 0 : 1]; + return FIGHTER_STYLES.find((style) => style === wanted) ?? "balanced"; +}; const base = (id: string): Draft => ({ player_id: id, x: 0, y: 0, facing: id === "fixture-one" ? 1 : -1, facing_x: id === "fixture-one" ? 1000 : -1000, facing_y: 0, velocity_x: 0, velocity_y: 0, - stance: id === "fixture-one" ? "orthodox" : "southpaw", defense: "guard_high", + stance: id === "fixture-one" ? "orthodox" : "southpaw", style: fixtureStyle(id), defense: "guard_high", stamina: 760, maximum_stamina: 1000, conditioning: 820, guard: 670, poise: 520, trauma: id === "fixture-two" ? { head: 420, body: 640, left_eye: 430, right_eye: 210, left_cut: 330, right_cut: 170, swelling: 280, bleeding: 380 } : { head: 150, body: 260, left_eye: 120, right_eye: 60, left_cut: 90, right_cut: 20, swelling: 100, bleeding: 70 }, knockdowns: 0, warnings: 0, deductions: 0, stunned_ticks: 0, is_downed: false, action: null, action_hand: null, action_target: null, action_power: null, action_id: null, action_key: null, action_start_tick: 0, action_startup_ticks: 0, action_active_ticks: 0, action_recovery_ticks: 0, action_contact_tick: null, queued_actions: 0, - clinch_startup_ticks: 0, clinch_ticks: 0, is_foul_recovery_target: false, taunt_ticks: 0, + clinch_startup_ticks: 0, clinch_ticks: 0, is_foul_recovery_target: false, taunt_ticks: 0, corner_choice: null, get_up_prompt: null, get_up_meter: 0, get_up_required: 0, get_up_count: 0, get_up_window_start_tick: 0, get_up_window_end_tick: 0, last_input_sequence: -1, }); @@ -27,15 +35,19 @@ export function runDevelopmentFixture(root: HTMLElement): () => void { root.innerHTML = `
HANDS · DEV FIXTUREProduction never enters this harness
`; const bloodParam = new URLSearchParams(window.location.search).get("blood"); const blood = bloodParam === "reduced" || bloodParam === "off" ? bloodParam : "full"; + const cameraParam = new URLSearchParams(window.location.search).get("camera"); + const camera = CAMERA_MODES.find((mode) => mode === cameraParam) ?? "broadcast"; const renderer = new FightRenderer( root.querySelector("canvas")!, { tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }, - () => ({ volume: 0, haptics: false, reducedMotion: false, blood }), + () => ({ volume: 0, haptics: false, reducedMotion: false, blood, commentary: true, announcer: false, camera }), ); renderer.setBloodLevel(blood); + // `avatars=:,:` draws real pictures on the plates. + const pictured = (new URLSearchParams(window.location.search).get("avatars") ?? "").split(",").map((entry) => entry.split(":")); const players: Record = { - "fixture-one": { id: "fixture-one", name: "Azure Vector", avatar: null, rating: 1512, connected: true }, - "fixture-two": { id: "fixture-two", name: "Crimson Geometry", avatar: null, rating: 1494, connected: true }, + "fixture-one": { id: pictured[0]?.[0] || "fixture-one", name: "Azure Vector", avatar: pictured[0]?.[1] || null, rating: 1512, connected: true }, + "fixture-two": { id: pictured[1]?.[0] || "fixture-two", name: "Crimson Geometry", avatar: pictured[1]?.[1] || null, rating: 1494, connected: true }, }; renderer.setPlayers(players, "fixture-one"); const touch = coarsePointer() ? new TouchInput(root.querySelector(".activity")!, new SharedActionIntent()) : null; @@ -43,6 +55,9 @@ export function runDevelopmentFixture(root: HTMLElement): () => void { let tick = 0; let eventId = 0; let finalSent = false; + let bellRung = false; + let thrownWindow = -1; + let landedWindow = -1; const interval = window.setInterval(() => { tick += 3; const t = tick / 30; @@ -78,6 +93,8 @@ export function runDevelopmentFixture(root: HTMLElement): () => void { one.velocity_x = two.velocity_x = 0; if (tauntCycle > 3.6) one.clinch_ticks = two.clinch_ticks = Math.round((5.1 - tauntCycle) * 30); } + // `mouthpiece=1` puts the gum shields back before every exchange so each big head shot knocks one out. + if (new URLSearchParams(window.location.search).get("mouthpiece") === "1" && cycle < 0.1) renderer.labEffects.clearMouthpieces(); if (cycle < 0.55 && one.taunt_ticks === 0 && two.taunt_ticks === 0 && !closing) { const punch = PUNCHES[Math.floor(t / 3.2) % PUNCHES.length]!; attacker.action = punch; @@ -85,7 +102,14 @@ export function runDevelopmentFixture(root: HTMLElement): () => void { attacker.action_target = Math.floor(t / 3.2) % 4 === 1 ? "body" : "head"; attacker.action_power = Math.floor(t / 3.2) % 5 === 2 ? "power" : "normal"; defender.defense = Math.floor(t / 6.4) % 2 === 0 ? "guard_high" : "none"; - if (cycle > 0.2 && cycle < 0.3) { + const punchWindow = Math.floor(t / 3.2); + if (thrownWindow !== punchWindow) { + thrownWindow = punchWindow; + eventId += 1; + events.push({ event_id: eventId, tick, kind: "punch_start", actor_id: attacker.player_id, target_id: null, amount: 0, detail: `${attacker.action_hand}:${punch}:${attacker.action_target}`, blood: 0, direction: 0, action_id: null }); + } + if (cycle >= 0.2 && landedWindow !== punchWindow) { + landedWindow = punchWindow; eventId += 1; events.push({ event_id: eventId, tick, kind: "hit", actor_id: attacker.player_id, target_id: defender.player_id, amount: 210, detail: `${attacker.action_hand}:${punch}:${attacker.action_target}`, blood: 100, direction: attacker.facing, action_id: null }); defender.stunned_ticks = 12; @@ -93,31 +117,89 @@ export function runDevelopmentFixture(root: HTMLElement): () => void { } const search = new URLSearchParams(window.location.search); const finisher = search.get("finisher"); - const forcedRest = search.get("phase") === "rest"; + // `phase=bell` fights for ten seconds and then rings the bell for the rest; `intro=1` opens with the countdown. + const bellAt = search.get("phase") === "bell" ? 10.5 : null; + const forcedRest = search.get("phase") === "rest" || (bellAt !== null && t >= bellAt); + const introducing = search.get("intro") === "1" && t < 3; + // `ko=` chooses the finishing punch; `finisher=flash` ends the bout on it without a count. + const ko = search.get("ko"); + const flash = finisher === "flash"; const pinned = search.get("pin") === "1"; const knockdownCycle = finisher === null ? t % 14 : (t < 2.5 ? 0 : 12); - if (knockdownCycle > 11 && knockdownCycle < 13.4) { + // A body shot that puts him on a knee a third of a second after it lands. + const bodyCollapse = finisher === "body" && t >= 2.5 && t < 2.85; + // The sparring stops for the body shot; a punch on a fighter who is down would be an arcade finisher. + if (finisher === "body" && t >= 2.5) events.length = 0; + if (bodyCollapse) { + two.stunned_ticks = 11; + two.x = 140; + two.y = 40; + one.x = 60; + one.y = 30; + if (t < 2.65) { + eventId += 1; + events.push({ event_id: eventId, tick, kind: "hit", actor_id: one.player_id, target_id: two.player_id, amount: 90, detail: "hook:body", blood: 30, direction: 1, action_id: "fixture-body" }); + eventId += 1; + events.push({ event_id: eventId, tick, kind: "body_collapse", actor_id: one.player_id, target_id: two.player_id, amount: 10, detail: "", blood: 0, direction: 0, action_id: "fixture-body" }); + } + } else if (finisher === "body" && t >= 2.85) { two.is_downed = true; two.action = null; two.x = 140; two.y = 40; one.x = -60; one.y = -30; + if (t < 3.0) { + eventId += 1; + events.push({ event_id: eventId, tick, kind: "knockdown", actor_id: one.player_id, target_id: two.player_id, amount: 1, detail: "body", blood: 0, direction: 0, action_id: "fixture-body" }); + } + } else if (knockdownCycle > 11 && knockdownCycle < 13.4) { + two.is_downed = !flash; + two.action = null; + two.x = 140; + two.y = 40; + one.x = -60; + one.y = -30; const trigger = finisher === null ? knockdownCycle > 11 && knockdownCycle < 11.15 : t >= 2.5 && t < 2.65; if (trigger) { eventId += 1; - if (finisher !== null && eventId % 2 === 1) eventId += 1; - events.push({ event_id: eventId, tick, kind: "counter_hit", actor_id: one.player_id, target_id: two.player_id, amount: 500, detail: finisher === "hand" ? "left:hook:body" : "right:uppercut:head", blood: 100, direction: 1, action_id: null }); + // `finisher=head` severs the head, `burst` bursts it with a big counter, `eye` forces an eye out + // with a hook, `hand` takes a hand. Finishers are picked by the event id, so it is made even or odd. + const burst = finisher === "burst"; + const eye = finisher === "eye"; + if (finisher !== null && eventId % 2 === (eye ? 0 : 1)) eventId += 1; + let detail = finisher === "hand" ? "left:hook:body" : eye ? "left:hook:head" : "right:uppercut:head"; + if (ko !== null) { + const target = ko === "body" ? "body" : "head"; + const punch = ko === "body" ? "hook" : ko === "jab" || ko === "straight" || ko === "hook" ? ko : "uppercut"; + const hand = ko === "jab" ? "left" : "right"; + Object.assign(one, { action: punch, action_hand: hand, action_target: target, action_power: "power", action_key: `${punch}:${hand}:${target}:power` }); + detail = `${punch}:${target}`; + } + events.push({ event_id: eventId, tick, kind: finisher === "head" || eye ? "hit" : "counter_hit", actor_id: one.player_id, target_id: two.player_id, amount: burst ? 130 : finisher === "head" || eye ? 90 : 500, detail, blood: 100, direction: 1, action_id: null }); eventId += 1; - events.push({ event_id: eventId, tick, kind: "knockdown", actor_id: one.player_id, target_id: two.player_id, amount: 420, detail: "knockdown", blood: 60, direction: 1, action_id: null }); + if (flash) events.push({ event_id: eventId, tick, kind: "result", actor_id: one.player_id, target_id: null, amount: 0, detail: "flash_ko", blood: 0, direction: 0, action_id: null }); + else events.push({ event_id: eventId, tick, kind: "knockdown", actor_id: one.player_id, target_id: two.player_id, amount: 420, detail: "knockdown", blood: 60, direction: 1, action_id: null }); } } + // `rocked=1` keeps the viewer's fighter stunned, for the hurt vision. + if (search.get("rocked") === "1") one.stunned_ticks = 60; if (pinned) { two.x = 460; two.y = 40; one.x = 330; one.y = 20; } + const fixedGap = Number(search.get("gap")); + if (Number.isFinite(fixedGap) && fixedGap > 0) { + one.x = -Math.round(fixedGap / 2); + two.x = one.x + Math.round(fixedGap); + one.y = two.y = 0; + one.velocity_x = two.velocity_x = 0; + one.facing_x = 1000; + two.facing_x = -1000; + one.facing_y = two.facing_y = 0; + } if (search.get("pin") === "corner") { two.x = 366; two.y = 366; @@ -125,7 +207,9 @@ export function runDevelopmentFixture(root: HTMLElement): () => void { one.y = 284; } if (forcedRest) { + const corner = search.get("corner"); for (const [fighter, sign] of [[one, -1], [two, 1]] as const) { + if (corner === "cut" || corner === "swelling" || corner === "breath") fighter.corner_choice = corner; fighter.x = sign * REST_CORNER_OFFSET; fighter.y = sign * REST_CORNER_OFFSET; fighter.velocity_x = fighter.velocity_y = 0; @@ -137,22 +221,74 @@ export function runDevelopmentFixture(root: HTMLElement): () => void { fighter.defense = "none"; } events.length = 0; + if (bellAt !== null && !bellRung) { + bellRung = true; + eventId += 1; + events.push({ event_id: eventId, tick, kind: "bell", actor_id: null, target_id: null, amount: 0, detail: "round_end", blood: 0, direction: 0, action_id: null }); + } } + if (introducing) { + for (const [fighter, sign] of [[one, -1], [two, 1]] as const) { + fighter.x = sign * 150; + fighter.y = 0; + fighter.velocity_x = fighter.velocity_y = 0; + fighter.facing_x = -sign * 1000; + fighter.facing_y = 0; + fighter.facing = -sign; + fighter.action = null; + fighter.taunt_ticks = fighter.stunned_ticks = fighter.clinch_ticks = 0; + fighter.is_downed = false; + } + events.length = 0; + } + if (search.get("eye") === "shut") two.trauma = { ...two.trauma, right_eye: 760, swelling: Math.max(two.trauma.swelling, 420) }; + const decided = search.get("finish") === "decision" && t >= 3.4; + if (decided) { + for (const fighter of [one, two]) { + fighter.action = null; + fighter.velocity_x = fighter.velocity_y = 0; + fighter.taunt_ticks = fighter.stunned_ticks = fighter.clinch_ticks = 0; + fighter.is_downed = false; + fighter.defense = "none"; + } + events.length = 0; + } + const flashOver = flash && t >= 2.5; const snapshot: EngineSnapshot = { - tick, phase: forcedRest ? "rest" : "fight", round_number: 3, phase_ticks_remaining: Math.max(0, 5400 - tick), - fighters: [{ ...one }, { ...two }], events, result: null, checksum: "a".repeat(64), + tick, phase: decided || flashOver ? "complete" : introducing ? "countdown" : forcedRest ? "rest" : "fight", round_number: search.get("intro") === "1" ? 1 : 3, phase_ticks_remaining: decided ? 0 : introducing ? Math.max(0, 90 - tick) : forcedRest ? Math.max(0, 450 - tick) : Math.max(0, 5400 - tick), + fighters: [{ ...one }, { ...two }], events, + result: flashOver ? { + match_id: "fixture", activity_instance_id: "fixture", guild_id: "fixture", player_one_id: one.player_id, player_two_id: two.player_id, winner_id: one.player_id, + finish_method: "flash_ko", round_number: 3, tick, scorecards: [], player_one_knockdowns: 0, player_two_knockdowns: 0, player_one_damage: 900, player_two_damage: 400, + } : null, + checksum: "a".repeat(64), }; renderer.push(snapshot); + if (decided && !finalSent) { + finalSent = true; + const winner = search.get("winner") === "two" ? two : one; + const cards = ["Impact", "Craft", "Generalship"].map((judge) => ({ judge, player_one: winner === one ? [10, 10, 10] : [9, 9, 10], player_two: winner === one ? [9, 9, 10] : [10, 10, 10] })); + renderer.setFinal({ + version: PROTOCOL_VERSION, type: "final", match_id: "fixture", winner_id: search.get("winner") === "none" ? null : winner.player_id, method: search.get("winner") === "none" ? "draw" : "decision", round: 3, + scorecards: search.get("winner") === "none" ? cards.map((card) => ({ ...card, player_one: [10, 10, 10], player_two: [10, 10, 10] })) : cards, + ratings: { [one.player_id]: { before: 1512, after: winner === one ? 1528 : 1496 }, [two.player_id]: { before: 1494, after: winner === one ? 1478 : 1510 } }, + }); + } if (finisher !== null && t >= 3.4 && !finalSent) { finalSent = true; - renderer.setFinal({ version: 3, type: "final", match_id: "fixture", winner_id: one.player_id, method: "ko", round: 3, scorecards: [], ratings: {} }); + renderer.setFinal({ version: PROTOCOL_VERSION, type: "final", match_id: "fixture", winner_id: one.player_id, method: flash ? "flash_ko" : "ko", round: 3, scorecards: [], ratings: {} }); } + (window as unknown as Record).__fixtureRenderer = renderer; (window as unknown as Record).__fixtureDebug = { tick, t: Number(t.toFixed(2)), downed: two.is_downed, severedHeads: renderer.labEffects.activeHeads, severedHands: renderer.labEffects.activeHands, + mouthpieces: [renderer.labEffects.mouthpieceOut(0), renderer.labEffects.mouthpieceOut(1)], + bursts: [renderer.labEffects.headBurst(0), renderer.labEffects.headBurst(1)], + eyes: [renderer.labEffects.eyeOut(0), renderer.labEffects.eyeOut(1)], + eyeAt: (() => { const at = new THREE.Vector3(); return renderer.labEffects.eyePosition(1, at) ? at.toArray().map((value) => Number(value.toFixed(3))) : null; })(), rigs: renderer.labRigs.length, resolutionScale: renderer.resolutionScale, heads: renderer.labRigs.map((root) => { diff --git a/web/hands/src/discord.test.ts b/web/hands/src/discord.test.ts index 8b6f4f31..9bbbc477 100644 --- a/web/hands/src/discord.test.ts +++ b/web/hands/src/discord.test.ts @@ -1,3 +1,4 @@ +import { PROTOCOL_VERSION } from "./types"; import type { IDiscordSDK } from "@discord/embedded-app-sdk"; import { ClientError } from "./api"; import { DiscordActivity, OAUTH_SCOPES } from "./discord"; @@ -6,11 +7,11 @@ const launchUrl = "/?instance_id=launch-1&frame_id=frame-1&platform=desktop"; function sdk(instanceId: string, order: string[]): IDiscordSDK { return { instanceId, ready: vi.fn(async () => { order.push("ready"); }), close: vi.fn(() => { order.push("close"); }), commands: { authorize: vi.fn(async () => { order.push("authorize"); return { code: "oauth-code" }; }), authenticate: vi.fn(async () => { order.push("authenticate"); return {}; }) } } as unknown as IDiscordSDK; } -const backend = (order: string[] = []) => vi.fn(async (input: URL | RequestInfo) => { if (String(input).includes("bootstrap")) { order.push("bootstrap"); return response({ client_id: "123", state: "state", protocol: 3, simulation: { tick_rate: 30, ring_half_width: 6000, ring_half_height: 4000 } }); } order.push("token"); return response({ access_token: "access", ticket: "ticket", player: { id: "one", name: "One", avatar: null, rating: 1000 } }); }); +const backend = (order: string[] = []) => vi.fn(async (input: URL | RequestInfo) => { if (String(input).includes("bootstrap")) { order.push("bootstrap"); return response({ client_id: "123", state: "state", protocol: PROTOCOL_VERSION, simulation: { tick_rate: 30, ring_half_width: 6000, ring_half_height: 4000 } }); } order.push("token"); return response({ access_token: "access", ticket: "ticket", player: { id: "one", name: "One", avatar: null, rating: 1000 } }); }); describe("Discord SDK OAuth", () => { it("uses exact ready-authorize-token-authenticate order/scopes and memory-only ticket", async () => { history.replaceState({}, "", launchUrl); const order: string[] = []; const mockSdk = sdk("launch-1", order); - vi.stubGlobal("fetch", vi.fn(async (input: URL | RequestInfo, init?: RequestInit) => { const url = String(input); if (url.includes("bootstrap")) { order.push("bootstrap"); expect(init?.body).toBe(JSON.stringify({ instance_id: "launch-1" })); return response({ client_id: "123", state: "oauth-state", protocol: 3, simulation: { tick_rate: 30, ring_half_width: 6000, ring_half_height: 4000 } }); } order.push("token"); expect(init?.body).toBe(JSON.stringify({ code: "oauth-code", state: "oauth-state" })); return response({ access_token: "access-secret", ticket: "ticket-secret", player: { id: "one", name: "One", avatar: null, rating: 1500 } }); })); + vi.stubGlobal("fetch", vi.fn(async (input: URL | RequestInfo, init?: RequestInit) => { const url = String(input); if (url.includes("bootstrap")) { order.push("bootstrap"); expect(init?.body).toBe(JSON.stringify({ instance_id: "launch-1" })); return response({ client_id: "123", state: "oauth-state", protocol: PROTOCOL_VERSION, simulation: { tick_rate: 30, ring_half_width: 6000, ring_half_height: 4000 } }); } order.push("token"); expect(init?.body).toBe(JSON.stringify({ code: "oauth-code", state: "oauth-state" })); return response({ access_token: "access-secret", ticket: "ticket-secret", player: { id: "one", name: "One", avatar: null, rating: 1500 } }); })); const activity = new DiscordActivity(() => mockSdk); const session = await activity.authorize(); expect(order).toEqual(["bootstrap", "ready", "authorize", "token", "authenticate"]); expect(OAUTH_SCOPES).toEqual(["identify", "guilds.members.read"]); expect(mockSdk.commands.authorize).toHaveBeenCalledWith({ client_id: "123", response_type: "code", state: "oauth-state", prompt: "none", scope: [...OAUTH_SCOPES] }); expect(mockSdk.commands.authenticate).toHaveBeenCalledWith({ access_token: "access-secret" }); @@ -46,7 +47,7 @@ describe("Discord SDK OAuth", () => { await expect(new DiscordActivity(() => mockSdk).authorize()).rejects.toEqual(new ClientError("authorize_failed", true)); expect(mockSdk.close).not.toHaveBeenCalled(); }); it("keeps the Activity open and preserves a safe token exchange failure", async () => { - history.replaceState({}, "", launchUrl); const order: string[] = []; const mockSdk = sdk("launch-1", order); vi.stubGlobal("fetch", vi.fn(async (input: URL | RequestInfo) => String(input).includes("bootstrap") ? response({ client_id: "123", state: "state", protocol: 3, simulation: { tick_rate: 30, ring_half_width: 6000, ring_half_height: 4000 } }) : new Response("{}", { status: 401, headers: { "Content-Type": "application/json" } }))); + history.replaceState({}, "", launchUrl); const order: string[] = []; const mockSdk = sdk("launch-1", order); vi.stubGlobal("fetch", vi.fn(async (input: URL | RequestInfo) => String(input).includes("bootstrap") ? response({ client_id: "123", state: "state", protocol: PROTOCOL_VERSION, simulation: { tick_rate: 30, ring_half_width: 6000, ring_half_height: 4000 } }) : new Response("{}", { status: 401, headers: { "Content-Type": "application/json" } }))); await expect(new DiscordActivity(() => mockSdk).authorize()).rejects.toEqual(new ClientError("token_failed", true)); expect(mockSdk.close).not.toHaveBeenCalled(); }); it("keeps the Activity open and identifies an authenticate RPC failure", async () => { diff --git a/web/hands/src/feedback.test.ts b/web/hands/src/feedback.test.ts index 556f2585..620afb22 100644 --- a/web/hands/src/feedback.test.ts +++ b/web/hands/src/feedback.test.ts @@ -1,9 +1,17 @@ -import { AudioFeedback } from "./audio"; +import * as THREE from "three"; +import { PROTOCOL_VERSION } from "./types"; +import { AudioFeedback, rockedBeatTicks } from "./audio"; import { INJURY_SOUNDS } from "./assets/injury-sounds"; import { HapticFeedback } from "./haptics"; +import { EventDeduplicator, SnapshotBuffer } from "./interpolation"; +import { RoundStatsTracker } from "./render/hud"; +import { contactParticipants, contactPresentationPlan, FightRenderer } from "./render/renderer"; +import { worldMapping } from "./render/world"; +import { fighter, snapshot } from "./test/fixtures"; +import type { CombatEvent, EngineSnapshot } from "./types"; -const settings = { volume: 1, haptics: true, reducedMotion: false, blood: "full" as const }; -const audioParam = (): AudioParam => ({ value: 0, setValueAtTime: vi.fn(), exponentialRampToValueAtTime: vi.fn() } as unknown as AudioParam); +const settings = { volume: 1, haptics: true, reducedMotion: false, blood: "full" as const, camera: "broadcast" as const, commentary: true, announcer: true }; +const audioParam = (): AudioParam => ({ value: 0, setValueAtTime: vi.fn(), exponentialRampToValueAtTime: vi.fn(), cancelScheduledValues: vi.fn(), setTargetAtTime: vi.fn() } as unknown as AudioParam); class MockAudioContext { static created = 0; static failResume = false; @@ -15,6 +23,9 @@ class MockAudioContext { static operations: string[] = []; static last: MockAudioContext | null = null; static playedBuffers: Array = []; + static filters: BiquadFilterNode[] = []; + static gains: GainNode[] = []; + static sourceStarts: Array<[number | undefined, number | undefined]> = []; /** The next resume() is outside a user activation: it stays pending until a later resume() starts the context. */ static holdResume = false; private readonly pendingResumes: (() => void)[] = []; @@ -39,9 +50,17 @@ class MockAudioContext { MockAudioContext.created += 1; MockAudioContext.last = this; } - createGain(): GainNode { return { gain: audioParam(), connect: vi.fn((target) => target) } as unknown as GainNode; } + createGain(): GainNode { + const gain = { gain: audioParam(), connect: vi.fn((target) => target) } as unknown as GainNode; + MockAudioContext.gains.push(gain); + return gain; + } createOscillator(): OscillatorNode { return { type: "sine", frequency: audioParam(), connect: vi.fn((target) => target), start: vi.fn(() => { MockAudioContext.oscillatorStarts += 1; }), stop: vi.fn() } as unknown as OscillatorNode; } - createBiquadFilter(): BiquadFilterNode { return { type: "lowpass", frequency: audioParam(), Q: audioParam(), connect: vi.fn((target) => target) } as unknown as BiquadFilterNode; } + createBiquadFilter(): BiquadFilterNode { + const filter = { type: "lowpass", frequency: audioParam(), Q: audioParam(), connect: vi.fn((target) => target) } as unknown as BiquadFilterNode; + MockAudioContext.filters.push(filter); + return filter; + } createBuffer(_channels: number, length: number): AudioBuffer { return { getChannelData: () => new Float32Array(length) } as unknown as AudioBuffer; } decodeAudioData = vi.fn(async (_data: ArrayBuffer): Promise => { MockAudioContext.operations.push("decode"); @@ -51,9 +70,10 @@ class MockAudioContext { return { decodeIndex: MockAudioContext.decodeCalls - 1 } as unknown as AudioBuffer; }); createBufferSource(): AudioBufferSourceNode { - const source = { buffer: null as AudioBuffer | null, loop: false, connect: vi.fn((target) => target), start: vi.fn(() => { + const source = { buffer: null as AudioBuffer | null, loop: false, connect: vi.fn((target) => target), start: vi.fn((when?: number, offset?: number) => { MockAudioContext.bufferStarts += 1; MockAudioContext.playedBuffers.push(source.buffer); + MockAudioContext.sourceStarts.push([when, offset]); }), stop: vi.fn() }; return source as unknown as AudioBufferSourceNode; } @@ -71,6 +91,9 @@ describe("authoritative audio and haptics", () => { MockAudioContext.operations = []; MockAudioContext.last = null; MockAudioContext.playedBuffers = []; + MockAudioContext.filters = []; + MockAudioContext.gains = []; + MockAudioContext.sourceStarts = []; MockAudioContext.holdResume = false; vi.stubGlobal("AudioContext", MockAudioContext); }); @@ -124,6 +147,10 @@ describe("authoritative audio and haptics", () => { expect(MockAudioContext.bufferStarts).toBe(before + 2); feedback.roundClock("fight", 2, 250, 30); expect(MockAudioContext.bufferStarts).toBe(before + 4); + // The next bout starts at round one again. + feedback.reset(); + feedback.roundClock("fight", 1, 299, 30); + expect(MockAudioContext.bufferStarts).toBe(before + 6); feedback.destroy(); }); @@ -203,6 +230,15 @@ describe("authoritative audio and haptics", () => { feedback.destroy(); }); + it("gives a finisher without a voice of its own the nearest one", async () => { + const feedback = new AudioFeedback(() => settings); + await feedback.unlock(); + feedback.injury("head_burst"); + const played = MockAudioContext.playedBuffers.at(-1) as unknown as { decodeIndex: number }; + expect(played.decodeIndex).toBe(0); + feedback.destroy(); + }); + it("suppresses gore voices for accessibility settings and tolerates an undecodable WAV", async () => { let blood: "off" | "reduced" | "full" = "full"; let reducedMotion = false; @@ -279,3 +315,217 @@ describe("authoritative audio and haptics", () => { expect(() => haptics.event({ event_id: 2, tick: 1, kind: "hit", actor_id: null, target_id: null, amount: 1, detail: "", blood: 0, direction: 0, action_id: null })).not.toThrow(); }); }); + +describe("the crowd", () => { + const formants = (): number => MockAudioContext.filters.filter((filter) => (filter.Q as unknown as { value: number }).value === 5).length; + const crowdEvent = (kind: string, amount = 0, detail = ""): Parameters[0] => ({ event_id: 1, tick: 1, kind, actor_id: "one", target_id: "two", amount, detail, blood: 0, direction: 0, action_id: null }); + + beforeEach(() => { + MockAudioContext.filters = []; + MockAudioContext.gains = []; + MockAudioContext.sourceStarts = []; + vi.stubGlobal("AudioContext", MockAudioContext); + }); + + it("goes ooh at a big counter but not at every jab, and not twice at once", async () => { + const feedback = new AudioFeedback(() => settings); + await feedback.unlock(); + feedback.event(crowdEvent("hit", 30, "jab:head")); + expect(formants()).toBe(0); + feedback.event(crowdEvent("counter_hit", 70, "hook:head")); + expect(formants()).toBe(2); + feedback.event(crowdEvent("counter_hit", 70, "hook:head")); + expect(formants()).toBe(2); + MockAudioContext.last!.currentTime = 3; + feedback.event(crowdEvent("hit", 95, "uppercut:head")); + expect(formants()).toBe(4); + feedback.destroy(); + }); + + it("groans and boos at a low blow and boos a headbutt", async () => { + const feedback = new AudioFeedback(() => settings); + await feedback.unlock(); + feedback.event(crowdEvent("foul", 0, "low_blow")); + expect(formants()).toBe(4); + feedback.event(crowdEvent("foul", 0, "headbutt")); + expect(formants()).toBe(6); + feedback.destroy(); + }); + + it("claps in rhythm behind a fighter who takes over, and not again straight away", async () => { + const feedback = new AudioFeedback(() => settings); + await feedback.unlock(); + const before = MockAudioContext.sourceStarts.length; + feedback.chant(); + const claps = MockAudioContext.sourceStarts.slice(before); + expect(claps.length).toBe(8 * 3 + 1); + const beats = claps.slice(0, 24).map(([when]) => when!); + expect(beats[3]! - beats[0]!).toBeCloseTo(0.42); + expect(new Set(claps.slice(0, 3).map(([, offset]) => offset)).size).toBe(3); + feedback.chant(); + expect(MockAudioContext.sourceStarts.length).toBe(before + claps.length); + feedback.destroy(); + }); + + it("murmurs with the tension and settles when it passes", async () => { + const feedback = new AudioFeedback(() => settings); + await feedback.unlock(); + const murmur = MockAudioContext.gains.find((gain) => gain.gain.value === 0 && MockAudioContext.gains.indexOf(gain) > 0)!; + const target = murmur.gain.setTargetAtTime as unknown as ReturnType; + feedback.tension(0.8); + expect(target).toHaveBeenLastCalledWith(0.8 * 0.05, 0, 0.3); + feedback.tension(0.81); + expect(target).toHaveBeenCalledTimes(1); + feedback.tension(0); + expect(target).toHaveBeenLastCalledWith(0, 0, 0.8); + feedback.destroy(); + }); + + it("goes quiet through a knockdown count and roars when the fighter gets up", async () => { + const feedback = new AudioFeedback(() => settings); + await feedback.unlock(); + const crowd = MockAudioContext.gains.find((gain) => gain.gain.value === 0.022)!; + const ramp = crowd.gain.exponentialRampToValueAtTime as unknown as ReturnType; + feedback.event(crowdEvent("knockdown", 1)); + expect(ramp).toHaveBeenLastCalledWith(0.004, 3); + feedback.event(crowdEvent("get_up", 6)); + expect((crowd.gain.setValueAtTime as unknown as ReturnType).mock.lastCall![0]).toBeCloseTo(0.15); + expect(ramp).toHaveBeenLastCalledWith(0.022, 4.5); + feedback.destroy(); + }); +}); + +describe("rocked", () => { + beforeEach(() => { + MockAudioContext.filters = []; + MockAudioContext.oscillatorStarts = 0; + vi.stubGlobal("AudioContext", MockAudioContext); + }); + + it("muffles the arena while the player's fighter is rocked and opens it up again after", async () => { + const feedback = new AudioFeedback(() => settings); + await feedback.unlock(); + const muffle = MockAudioContext.filters.find((filter) => filter.type === "lowpass" && (filter.frequency as unknown as { value: number }).value === 20_000)!; + const target = muffle.frequency.setTargetAtTime as unknown as ReturnType; + feedback.rocked(1, 100); + expect(target.mock.lastCall![0]).toBeCloseTo(650); + expect(target.mock.lastCall![2]).toBe(0.05); + feedback.rocked(0, 200); + expect(target.mock.lastCall![0]).toBeCloseTo(20_000); + expect(target.mock.lastCall![2]).toBe(0.9); + feedback.destroy(); + }); + + it("pounds a heartbeat that slows as the fighter recovers", async () => { + const feedback = new AudioFeedback(() => settings); + await feedback.unlock(); + const beatsOver = (level: number, from: number): number => { + const before = MockAudioContext.oscillatorStarts; + for (let tick = from; tick < from + 60; tick += 1) feedback.rocked(level, tick); + return (MockAudioContext.oscillatorStarts - before) / 2; + }; + expect(rockedBeatTicks(1)).toBe(14); + expect(rockedBeatTicks(0.3)).toBe(29); + expect(beatsOver(1, 1000)).toBe(5); + expect(beatsOver(0.3, 2000)).toBe(3); + expect(beatsOver(0.1, 3000)).toBe(0); + feedback.destroy(); + }); + + it("beats again in a rematch, whose clock starts over at zero", async () => { + const feedback = new AudioFeedback(() => settings); + await feedback.unlock(); + const beatsOver = (from: number): number => { + const before = MockAudioContext.oscillatorStarts; + for (let tick = from; tick < from + 60; tick += 1) feedback.rocked(1, tick); + return (MockAudioContext.oscillatorStarts - before) / 2; + }; + expect(beatsOver(15_000)).toBe(5); + expect(beatsOver(400)).toBe(5); + feedback.destroy(); + }); + + it("opens the arena up again when the bout ends, even without a last snapshot", async () => { + const feedback = new AudioFeedback(() => settings); + await feedback.unlock(); + const muffle = MockAudioContext.filters.find((filter) => filter.type === "lowpass" && (filter.frequency as unknown as { value: number }).value === 20_000)!; + const target = muffle.frequency.setTargetAtTime as unknown as ReturnType; + feedback.rocked(1, 100); + feedback.result({ version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "two", method: "forfeit", round: 1, scorecards: [], ratings: {} }); + expect(target.mock.lastCall![0]).toBeCloseTo(20_000); + feedback.destroy(); + }); +}); + +describe("a punch the guard took", () => { + const SIMULATION = { tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }; + const renderer = FightRenderer.prototype as unknown as { + fireContacts(this: unknown, sampledTick: number): void; + push(this: unknown, snapshot: EngineSnapshot): void; + recordedHit(this: unknown, knockdown: CombatEvent): CombatEvent | null; + }; + // The engine reports the defender's block, then the hit that leaked through it, under the punch's action id. + const block: CombatEvent = { event_id: 11, tick: 40, kind: "block", actor_id: "two", target_id: "one", amount: 30, detail: "", blood: 0, direction: 0, action_id: "one:7" }; + const leaked: CombatEvent = { event_id: 12, tick: 40, kind: "counter_hit", actor_id: "one", target_id: "two", amount: 60, detail: "uppercut:head", blood: 12, direction: 1, action_id: "one:7" }; + const clean: CombatEvent = { ...leaked, event_id: 21, amount: 120, blood: 30, action_id: "one:8" }; + + beforeEach(() => { + MockAudioContext.filters = []; + MockAudioContext.gains = []; + vi.stubGlobal("AudioContext", MockAudioContext); + }); + + it("is heard and felt as the block, not as a punch that landed", async () => { + const audio = new AudioFeedback(() => settings); + await audio.unlock(); + const rumbles: number[] = []; + Object.defineProperty(navigator, "getGamepads", { configurable: true, value: () => [{ connected: true, vibrationActuator: { playEffect: async (_type: string, pattern: { duration: number }) => { rumbles.push(pattern.duration); } } }] }); + const haptics = new HapticFeedback(() => settings); + const sounds = Object.fromEntries((["impact", "crowdSwell", "ooh"] as const).map((name) => [name, vi.spyOn(AudioFeedback.prototype as unknown as Record void>, name)])); + // Through the renderer's real presentation, as push() queues contacts and the clock reaches them. + const present = (events: CombatEvent[]): string[] => { + const frame = snapshot(40); + const heard: string[] = []; + const stub = Object.assign(Object.create(FightRenderer.prototype) as Record, { + pendingContacts: contactPresentationPlan(events, frame).map((entry) => ({ ...entry, contactTick: 40, ...contactParticipants(entry.event, frame), injury: null })), + buffer: { latest: () => frame }, presentFightEvent: vi.fn(), mapping: worldMapping(SIMULATION), contactPoint: new THREE.Vector3(), mouthPoint: new THREE.Vector3(), tmpA: new THREE.Vector3(), tmpB: new THREE.Vector3(), + effects: { addEvent: vi.fn(), spawnTeeth: vi.fn() }, settings: () => ({ reducedMotion: false, blood: "full" }), arena: { excite: vi.fn() }, viewerId: null, + arcadeInjuries: [null, null], graphs: null, headWorldPose: () => null, knockOutMouthpiece: vi.fn(), viewerHitFlash: 0, + onContact: (event: CombatEvent) => { + heard.push(event.kind); + audio.event(event); + haptics.event(event); + }, + }); + renderer.fireContacts.call(stub, 40); + return heard; + }; + expect(present([block, leaked])).toEqual(["block"]); + for (const spy of Object.values(sounds)) expect(spy).not.toHaveBeenCalled(); + expect(rumbles).toEqual([55]); + // The same counter landing clean is a punch, a swell and an ooh, and the counter's rumble. + expect(present([clean])).toEqual(["counter_hit"]); + for (const spy of Object.values(sounds)) expect(spy).toHaveBeenCalledOnce(); + expect(rumbles).toEqual([55, 105]); + audio.destroy(); + }); + + it("earns no finisher for the knockdown it causes, though the replay still shows it", () => { + const knockdown: CombatEvent = { event_id: 13, tick: 40, kind: "knockdown", actor_id: "one", target_id: "two", amount: 1, detail: "", blood: 0, direction: 0, action_id: null }; + const pushed = (events: CombatEvent[]): { hit: CombatEvent | null; finisher: string | null } => { + const stub: Record = { + buffer: new SnapshotBuffer(64, 30), dedupe: new EventDeduplicator(), history: [], roundStats: new RoundStatsTracker(), referee: null, graphs: null, + acknowledgeActions: vi.fn(), movement: { observe: vi.fn() }, mapping: worldMapping(SIMULATION), contactPoint: new THREE.Vector3(), pendingContacts: [], effects: { addEvent: vi.fn() }, + manualClock: true, lastManualTime: 0, lastKnockdown: null, recordedHit: renderer.recordedHit, settings: () => ({ reducedMotion: false, blood: "full" }), + commentary: { observe: vi.fn() }, simulation: SIMULATION, players: {}, frameSeconds: 0, + }; + const floored = { ...snapshot(40), phase: "knockdown" as const, fighters: [{ ...fighter("one", -100), action_key: "uppercut:right:head:power" }, { ...fighter("two", 100), is_downed: true }] as const, events }; + renderer.push.call(stub, floored); + return stub.lastKnockdown as { hit: CombatEvent | null; finisher: string | null }; + }; + expect(pushed([clean, { ...knockdown, event_id: 22 }]).finisher).not.toBeNull(); + const blocked = pushed([block, leaked, knockdown]); + expect(blocked.finisher).toBeNull(); + expect(blocked.hit).toEqual(leaked); + }); +}); diff --git a/web/hands/src/haptics.ts b/web/hands/src/haptics.ts index 1b3bc2b2..f44fa7d0 100644 --- a/web/hands/src/haptics.ts +++ b/web/hands/src/haptics.ts @@ -10,6 +10,7 @@ type VibratingPad = Gamepad & { const patterns: Readonly> = { block: [55, 0.12, 0.22], hit: [85, 0.3, 0.42], counter_hit: [105, 0.38, 0.5], stun: [125, 0.42, 0.55], knockdown: [180, 0.62, 0.65], + parry: [60, 0.18, 0.4], body_collapse: [170, 0.55, 0.3], }; export class HapticFeedback { diff --git a/web/hands/src/input/bindings.test.ts b/web/hands/src/input/bindings.test.ts new file mode 100644 index 00000000..a03a7594 --- /dev/null +++ b/web/hands/src/input/bindings.test.ts @@ -0,0 +1,43 @@ +import { CONTROL_HELP, controlHint, SHORT_HINT_WIDTH } from "./bindings"; + +/** The lines a hint wraps to in a box this wide: 11 px type at about 0.55 em a character, 12 px of padding a side and a 1 px border. */ +function wrappedLines(text: string, box: number): number { + const room = box - 26; + const character = 11 * 0.55; + let lines = 1; + let used = 0; + for (const word of text.split(" ")) { + const width = word.length * character; + if (used === 0 || used + character + width <= room) used += (used === 0 ? 0 : character) + width; + else { + lines += 1; + used = width; + } + } + return lines; +} + +describe("the controls hint", () => { + it("gives a phone held upright the essentials, short enough for the column beside the pads", () => { + // The hint's box beside the pads, as Edge lays out style.css: min(44vw, 300px, 100vw - 226px) held upright. + for (const [width, box] of [[320, 94], [360, 134], [375, 149], [390, 164], [412, 181]] as const) { + const hint = controlHint(true, width); + for (const essential of [/drag/iu, /move/iu, /tap/iu, /punch/iu, /hold guard/iu]) expect(hint, `${width} px`).toMatch(essential); + // Five lines at 320 px (88 px in Edge, where the whole hint took eleven, 177 px), and four at most from 360 px + // (Edge sets three). + expect(wrappedLines(hint, box), `${width} px`).toBeLessThanOrEqual(width === 320 ? 5 : 4); + expect(wrappedLines(controlHint(true, SHORT_HINT_WIDTH), box)).toBeGreaterThan(wrappedLines(hint, box)); + } + }); + + it("keeps the whole touch hint for wider screens, and the keys for a keyboard", () => { + expect(controlHint(true, SHORT_HINT_WIDTH - 1)).not.toContain("SLIP"); + for (const width of [SHORT_HINT_WIDTH, 568, 844, 1280]) { + const hint = controlHint(true, width); + for (const button of ["BODY", "POWER", "GUARD", "SLIP", "WEAVE", "PULL", "CLINCH"]) expect(hint).toContain(button); + } + for (const width of [320, 1280]) expect(controlHint(false, width)).toContain("Jab F/J"); + // The Controls panel has the rest. + expect(CONTROL_HELP.some((item) => item.startsWith("Touch:") && item.includes("left or right half"))).toBe(true); + }); +}); diff --git a/web/hands/src/input/bindings.ts b/web/hands/src/input/bindings.ts index 0c9f4d84..d33f21b3 100644 --- a/web/hands/src/input/bindings.ts +++ b/web/hands/src/input/bindings.ts @@ -16,13 +16,35 @@ export const ACTIVE_CODES = new Set([...Object.keys(PUNCH_KEYS), ...Object.keys( export const CONTROL_HELP = [ "Move: W up · S down · A left · D right", "High / low guard: Q / E", "Left/right jab: F / J", "Left/right straight: R / U", "Left/right hook: G / H", "Left/right uppercut: T / Y", "Body: Shift · Power: Alt", - "Slip: Z / X · Weave: C · Pull: V", "Clinch: B · Stance: N · Taunt: M · Fouls: 1 / 2", "Get-up rhythm: ← / →", + "Slip: Z / X · Weave: C · Pull: V", "Clinch: B · Stance: N · Taunt: M · Fouls (on purpose): Shift+1 low blow / Shift+2 headbutt", "Get-up rhythm: ← / →", + "Camera: K cycles broadcast, close and over the shoulder (behind your fighter, W walks at the opponent and A / D circle him).", + "Style pick before the bout: arrow keys choose · Enter settles · 1–5 settle on that card at once.", "Controller move: left stick. High / low guard: left / right shoulder (independent of punches).", "Controller face classes: bottom jab · right straight · left hook · top uppercut.", "Controller face hand: hold D-pad left for left hand or D-pad right for right hand, then press a face punch; otherwise punches use the right hand. A direction used for a punch is consumed and does not evade.", "Controller modifiers: left trigger body · right trigger power.", "Controller actions: left stick press clinch · right stick press switch stance · hold right trigger + right stick press to taunt · D-pad up weave · D-pad down pull · tap and release D-pad left/right to slip; while down, D-pad left/right performs the private get-up rhythm immediately.", "Controller fouls: View/Back low blow · Menu/Start headbutt.", + "Controller style pick: D-pad or left stick to choose · bottom face button settles.", "Right-stick gesture: horizontal 0–22.5° hook · 22.5–45° jab · 45–70° straight · 70–90° uppercut; left/right direction selects hand.", + "Between rounds, tell your corner what to work on: 1 close the cut · 2 bring down the swelling · 3 catch your breath (controller: left, top or right face button; or tap).", "Touch: drag the left side to move · tap a punch pad's left or right half for that hand · hold BODY, POWER, GUARD or LOW · tap ◀ SLIP, SLIP ▶, WEAVE, PULL or CLINCH.", ] as const; + +const CONTROL_HINT_KEYBOARD = "Move WASD · Jab F/J · Straight R/U · Hook G/H · Uppercut T/Y · Guard Q/E · Body Shift · Power Alt"; +const CONTROL_HINT_TOUCH = "Drag on the left to move · Tap the pads to punch, L or R hand · Hold BODY, POWER or GUARD · Tap SLIP, WEAVE, PULL or CLINCH"; +/** A phone held upright has only a narrow column beside the pads for the hint: the essentials, which the pads' own labels fill in. */ +const CONTROL_HINT_TOUCH_SHORT = "Drag left to move · Tap pads to punch · Hold GUARD"; +/** Screens narrower than this, phones held upright, get the short touch hint. */ +export const SHORT_HINT_WIDTH = 480; + +/** The controls hint shown before and during the countdown, for a keyboard or for the touch pads on a screen this wide. */ +export const controlHint = (touch: boolean, width: number): string => (!touch ? CONTROL_HINT_KEYBOARD : width < SHORT_HINT_WIDTH ? CONTROL_HINT_TOUCH_SHORT : CONTROL_HINT_TOUCH); + +/** The Controls panel in sections instead of one list. */ +export const CONTROL_SECTIONS: readonly { readonly title: string; readonly items: readonly string[] }[] = [ + { title: "Keyboard", items: CONTROL_HELP.filter((item) => !item.startsWith("Controller") && !item.startsWith("Right-stick") && !item.startsWith("Between rounds") && !item.startsWith("Touch")) }, + { title: "Between rounds", items: CONTROL_HELP.filter((item) => item.startsWith("Between rounds")) }, + { title: "Controller", items: CONTROL_HELP.filter((item) => item.startsWith("Controller") || item.startsWith("Right-stick")) }, + { title: "Touch", items: CONTROL_HELP.filter((item) => item.startsWith("Touch")) }, +]; diff --git a/web/hands/src/input/input.test.ts b/web/hands/src/input/input.test.ts index 02bbebbd..4e5c7804 100644 --- a/web/hands/src/input/input.test.ts +++ b/web/hands/src/input/input.test.ts @@ -1,5 +1,5 @@ import { SharedActionIntent } from "./action-buffer"; -import { InputController } from "./input"; +import { InputController, viewRelativeMove } from "./input"; const button = (pressed = false): GamepadButton => ({ pressed, touched: pressed, value: pressed ? 1 : 0 }); function pad(buttons: number[] = []): Gamepad { @@ -98,3 +98,44 @@ describe("action instance ids", () => { expect(seen).toEqual(["c1", "c2"]); }); }); + +describe("controls that turn with the camera", () => { + it("leaves a move alone for the broadcast camera", () => { + expect(viewRelativeMove(707, 707, 0, -1)).toEqual({ moveX: 707, moveY: 707 }); + expect(viewRelativeMove(-1000, 0, 0, -1)).toEqual({ moveX: -1000, moveY: 0 }); + }); + + it("walks the way the camera faces on up and circles on left and right", () => { + expect(viewRelativeMove(0, 1000, 1, 0)).toEqual({ moveX: 1000, moveY: 0 }); + expect(viewRelativeMove(0, 1000, 0, 1)).toEqual({ moveX: 0, moveY: -1000 }); + expect(viewRelativeMove(1000, 0, 1, 0)).toEqual({ moveX: 0, moveY: -1000 }); + expect(viewRelativeMove(-1000, 0, 1, 0)).toEqual({ moveX: 0, moveY: 1000 }); + }); + + it("keeps the strength of the move and stays in range", () => { + for (let degrees = 0; degrees < 360; degrees += 7) { + const angle = (degrees * Math.PI) / 180; + for (const [x, y] of [[0, 1000], [707, -707], [-300, 0]] as const) { + const turned = viewRelativeMove(x, y, Math.sin(angle), Math.cos(angle)); + expect(Math.abs(Math.hypot(turned.moveX, turned.moveY) - Math.hypot(x, y))).toBeLessThan(2); + expect(Math.abs(turned.moveX)).toBeLessThanOrEqual(1000); + expect(Math.abs(turned.moveY)).toBeLessThanOrEqual(1000); + } + } + }); + + it("turns the held move, and asks the camera only while a direction is held", () => { + const input = new InputController(); + const forward = vi.fn(() => ({ x: 1, z: 0 })); + input.setViewForward(forward); + expect(input.held()).toMatchObject({ moveX: 0, moveY: 0 }); + expect(forward).not.toHaveBeenCalled(); + window.dispatchEvent(key("keydown", "KeyW")); + expect(input.held()).toMatchObject({ moveX: 1000, moveY: 0 }); + expect(input.frame()).toMatchObject({ moveX: 1000, moveY: 0 }); + forward.mockReturnValue(null as unknown as { x: number; z: number }); + expect(input.held()).toMatchObject({ moveX: 0, moveY: 1000 }); + window.dispatchEvent(key("keyup", "KeyW")); + input.destroy(); + }); +}); diff --git a/web/hands/src/input/input.ts b/web/hands/src/input/input.ts index f17e3782..9695a1a6 100644 --- a/web/hands/src/input/input.ts +++ b/web/hands/src/input/input.ts @@ -5,6 +5,16 @@ import { TouchInput, coarsePointer } from "./touch"; import type { HeldInput } from "../prediction"; import type { InputFrame, SemanticAction } from "../types"; +/** + * Turns a stick or key direction given relative to the camera into the ring's own axes. `forwardX` + * and `forwardZ` are the unit direction the camera faces along the canvas in world space; the + * broadcast camera faces world -z, which leaves a move unchanged. Ring +y is world -z. + */ +export function viewRelativeMove(moveX: number, moveY: number, forwardX: number, forwardZ: number): { moveX: number; moveY: number } { + const clamp = (value: number): number => Math.max(-1000, Math.min(1000, Math.round(value))) || 0; + return { moveX: clamp(-forwardZ * moveX + forwardX * moveY), moveY: clamp(-forwardX * moveX - forwardZ * moveY) }; +} + export class InputController { private readonly actions = new SharedActionIntent(1); readonly keyboard: KeyboardInput; readonly gamepad: GamepadInput; touch: TouchInput | null = null; @@ -16,12 +26,18 @@ export class InputController { setActive(active: boolean, touchShown = active): void { this.keyboard.setEnabled(active); this.gamepad.setEnabled(active); this.touch?.setEnabled(active, touchShown); } setKnockdown(value: boolean): void { this.gamepad.setKnockdown(value); this.touch?.setKnockdown(value); } onAction(listener: ((action: SemanticAction) => void) | null): void { this.actions.setListener(listener); } + /** While the camera turns with the player's fighter, up on the stick or W walks the way it faces. */ + setViewForward(provider: (() => { readonly x: number; readonly z: number } | null) | null): void { this.viewForward = provider; } + private viewForward: (() => { readonly x: number; readonly z: number } | null) | null = null; held(): HeldInput { const keyboard = this.keyboard.frame(0); const gamepad = this.gamepad.frame(0); const touch = this.touch?.frame() ?? null; const moveX = gamepad.moveX !== 0 ? gamepad.moveX : touch !== null && touch.moveX !== 0 ? touch.moveX : keyboard.moveX; const moveY = gamepad.moveY !== 0 ? gamepad.moveY : touch !== null && touch.moveY !== 0 ? touch.moveY : keyboard.moveY; const defense = gamepad.defense !== "none" ? gamepad.defense : touch !== null && touch.defense !== "none" ? touch.defense : keyboard.defense; - return { moveX, moveY, defense }; + const forward = (moveX !== 0 || moveY !== 0) ? this.viewForward?.() ?? null : null; + if (forward === null) return { moveX, moveY, defense }; + const turned = viewRelativeMove(moveX, moveY, forward.x, forward.z); + return { moveX: turned.moveX, moveY: turned.moveY, defense }; } frame(): InputFrame { return { ...this.held(), actions: this.actions.drain(4) }; diff --git a/web/hands/src/input/keyboard.test.ts b/web/hands/src/input/keyboard.test.ts index 1e3e0d55..819dc1cf 100644 --- a/web/hands/src/input/keyboard.test.ts +++ b/web/hands/src/input/keyboard.test.ts @@ -2,7 +2,8 @@ import { KeyboardInput } from "./keyboard"; const key = (type: "keydown" | "keyup", code: string, repeat = false): KeyboardEvent => new KeyboardEvent(type, { code, repeat, cancelable: true }); describe("semantic keyboard parity", () => { it("emits all punch fields with latched modifiers and held movement/guard", () => { const input = new KeyboardInput(); window.dispatchEvent(key("keydown", "ShiftLeft")); window.dispatchEvent(key("keydown", "AltLeft")); window.dispatchEvent(key("keydown", "KeyA")); window.dispatchEvent(key("keydown", "KeyQ")); window.dispatchEvent(key("keydown", "KeyF")); expect(input.frame()).toEqual({ moveX: -1000, moveY: 0, defense: "guard_high", actions: [{ kind: "punch", hand: "left", class: "jab", target: "body", power: "power" }] }); input.destroy(); }); - it("covers evasions, clinch, stance, fouls and get-up without repeat", () => { const input = new KeyboardInput(window, 20); for (const code of ["KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "Digit1", "Digit2", "ArrowLeft", "ArrowRight"]) window.dispatchEvent(key("keydown", code)); window.dispatchEvent(key("keydown", "KeyZ", true)); const actions = [...input.frame(20).actions]; expect(actions.map((action) => action.kind)).toEqual(["slip_left", "slip_right", "weave", "pull", "clinch", "switch_stance", "foul", "foul", "get_up_left", "get_up_right"]); window.dispatchEvent(new Event("blur")); expect(input.frame().actions).toEqual([]); input.destroy(); }); + it("covers evasions, clinch, stance, fouls and get-up without repeat", () => { const input = new KeyboardInput(window, 20); for (const code of ["KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "ShiftLeft", "Digit1", "Digit2", "ArrowLeft", "ArrowRight"]) window.dispatchEvent(key("keydown", code)); window.dispatchEvent(key("keydown", "KeyZ", true)); const actions = [...input.frame(20).actions]; expect(actions.map((action) => action.kind)).toEqual(["slip_left", "slip_right", "weave", "pull", "clinch", "switch_stance", "foul", "foul", "get_up_left", "get_up_right"]); window.dispatchEvent(new Event("blur")); expect(input.frame().actions).toEqual([]); input.destroy(); }); + it("throws a foul only with Shift held, so a number pressed for the corner at the bell is not a low blow", () => { const input = new KeyboardInput(window, 20); window.dispatchEvent(key("keydown", "Digit1")); window.dispatchEvent(key("keyup", "Digit1")); window.dispatchEvent(key("keydown", "Digit2")); expect(input.frame(20).actions).toEqual([]); window.dispatchEvent(key("keyup", "Digit2")); window.dispatchEvent(key("keydown", "ShiftLeft")); window.dispatchEvent(key("keydown", "Digit1")); expect(input.frame(20).actions.map((action) => action.kind)).toEqual(["foul"]); window.dispatchEvent(new Event("blur")); input.destroy(); }); it("maps W up and S down, normalizes diagonals and becomes neutral on blur", () => { const input = new KeyboardInput(); window.dispatchEvent(key("keydown", "KeyA")); window.dispatchEvent(key("keydown", "KeyW")); expect(input.frame()).toMatchObject({ moveX: -707, moveY: 707 }); window.dispatchEvent(key("keyup", "KeyW")); window.dispatchEvent(key("keydown", "KeyS")); expect(input.frame()).toMatchObject({ moveX: -707, moveY: -707 }); window.dispatchEvent(new Event("blur")); expect(input.frame()).toMatchObject({ moveX: 0, moveY: 0, defense: "none", actions: [] }); input.destroy(); }); it("coalesces rapid repeats of the same discrete key", () => { const input = new KeyboardInput(window, 3); for (let index = 0; index < 100; index += 1) { window.dispatchEvent(key("keydown", "KeyF")); window.dispatchEvent(key("keyup", "KeyF")); } const actions = input.frame(20).actions; expect(actions).toEqual([{ kind: "punch", hand: "left", class: "jab", target: "head", power: "normal" }]); input.destroy(); }); it("retains only the newest changed intent", () => { const input = new KeyboardInput(window, 1); for (const code of ["KeyF", "KeyJ", "KeyG"]) { window.dispatchEvent(key("keydown", code)); window.dispatchEvent(key("keyup", code)); } expect(input.frame(20).actions).toEqual([{ kind: "punch", hand: "left", class: "hook", target: "head", power: "normal" }]); input.destroy(); }); diff --git a/web/hands/src/input/keyboard.ts b/web/hands/src/input/keyboard.ts index 845a03a2..e5b62738 100644 --- a/web/hands/src/input/keyboard.ts +++ b/web/hands/src/input/keyboard.ts @@ -14,7 +14,8 @@ export class KeyboardInput { if (event.code === "KeyQ" || event.code === "KeyE") this.clearActions(); const punch = PUNCH_KEYS[event.code]; if (punch !== undefined) this.push({ kind: "punch", hand: punch[0], class: punch[1], target: this.hasShift() ? "body" : "head", power: this.hasAlt() ? "power" : "normal" }); - else { const action = ACTION_KEYS[event.code]; if (action !== undefined) this.push(action); } + // A foul needs Shift held, so a number pressed for the corner as the bell goes cannot become one. + else { const action = ACTION_KEYS[event.code]; if (action !== undefined && (action.kind !== "foul" || this.hasShift())) this.push(action); } }; private readonly keyup = (event: KeyboardEvent): void => { if (ACTIVE_CODES.has(event.code)) { if (this.enabled) event.preventDefault(); this.held.delete(event.code); } }; private readonly visibility = (): void => { if (document.hidden) this.reset(); }; diff --git a/web/hands/src/lab/lab.ts b/web/hands/src/lab/lab.ts index 6cf18915..060caf63 100644 --- a/web/hands/src/lab/lab.ts +++ b/web/hands/src/lab/lab.ts @@ -105,7 +105,7 @@ export class LabApp { this.renderer = new FightRenderer( this.canvas, { tick_rate: this.replay.tick_rate, ring_half_width: 500, ring_half_height: 500 }, - () => ({ volume: 0, haptics: false, reducedMotion: false, blood: "full" as BloodLevel }), + () => ({ volume: 0, haptics: false, reducedMotion: false, blood: "full" as BloodLevel, commentary: true, announcer: false, camera: "broadcast" as const }), { manualClock: true }, ); const players = Object.fromEntries(this.replay.ticks[0]!.snapshot.payload.fighters.map((fighter) => [ diff --git a/web/hands/src/lab/model-lab.ts b/web/hands/src/lab/model-lab.ts index 678a7b2a..ececa7fa 100644 --- a/web/hands/src/lab/model-lab.ts +++ b/web/hands/src/lab/model-lab.ts @@ -1,5 +1,9 @@ import * as THREE from "three"; +import { Effects3D } from "../render/effects"; +import { eyeSocket, measureBurstStump } from "../render/renderer"; import { BoxingGraph, SkinnedBoxer, loadBoxerGlb } from "../render/graph"; +import { OFFICIAL_LOOKS, lookFor, type FighterLook } from "../render/looks"; +import { BLUE_CORNER_OUTFIT, CUTMAN_OUTFIT, RED_CORNER_OUTFIT, REFEREE_OUTFIT, type OfficialOutfit } from "../render/outfit"; import { GloveTrail } from "../render/trails"; import { worldMapping } from "../render/world"; import type { FighterSnapshot, Hand, PunchClass, Target } from "../types"; @@ -9,33 +13,53 @@ import type { FighterSnapshot, Hand, PunchClass, Target } from "../types"; * every authored pose can be inspected from any angle without a match. * Query parameters: pose (idle, guard_high, guard_low, slip_left, slip_right, * weave, pull, jab_left, straight_right, hook_left, uppercut_right, ..., - * hit_head, hit_body, block, knockdown, getup, stunned, taunt, clinch, seated, celebrate, wave_off, break, touch_gloves, walk), + * hit_head, hit_body, block, knockdown, getup, stunned, taunt, clinch, seated, celebrate, wave_off, break, touch_gloves, walk, + * ragdoll: a knockout fall under physics from `hit` (jab|straight|hook|uppercut|body) thrown with + * `hand` (left|right) for `amount` damage, getting up after `getup` seconds when given), * t (seconds into the pose), stance (orthodox|southpaw), cam (front|side| - * three-quarter|top|back), skeleton (1). + * three-quarter|top|back), skeleton (1), outfit (referee|corner_blue|corner_red|cutman) to + * show a ring official, whose poses are idle, count, attend, treat, wave_off and break, and + * player (any id) to show the fighter that player gets. */ +const OUTFITS: Readonly> = { + referee: { outfit: REFEREE_OUTFIT, look: OFFICIAL_LOOKS.referee }, + corner_blue: { outfit: BLUE_CORNER_OUTFIT, look: OFFICIAL_LOOKS.blueCorner }, + corner_red: { outfit: RED_CORNER_OUTFIT, look: OFFICIAL_LOOKS.redCorner }, + cutman: { outfit: CUTMAN_OUTFIT, look: OFFICIAL_LOOKS.blueCutman }, +}; + type Draft = { -readonly [K in keyof FighterSnapshot]: FighterSnapshot[K] }; const base = (): Draft => ({ player_id: "lab", x: 0, y: 0, facing: 1, facing_x: 0, facing_y: -1000, velocity_x: 0, velocity_y: 0, - stance: "orthodox", defense: "none", stamina: 1000, maximum_stamina: 1000, conditioning: 1000, guard: 700, poise: 600, + stance: "orthodox", style: "balanced", defense: "none", stamina: 1000, maximum_stamina: 1000, conditioning: 1000, guard: 700, poise: 600, trauma: { head: 0, body: 0, left_eye: 0, right_eye: 0, left_cut: 0, right_cut: 0, swelling: 0, bleeding: 0 }, knockdowns: 0, warnings: 0, deductions: 0, stunned_ticks: 0, is_downed: false, action: null, action_hand: null, action_target: null, action_power: null, action_id: null, action_key: null, action_start_tick: 0, action_startup_ticks: 0, action_active_ticks: 0, action_recovery_ticks: 0, action_contact_tick: null, - queued_actions: 0, clinch_startup_ticks: 0, clinch_ticks: 0, is_foul_recovery_target: false, taunt_ticks: 0, + queued_actions: 0, clinch_startup_ticks: 0, clinch_ticks: 0, is_foul_recovery_target: false, taunt_ticks: 0, corner_choice: null, get_up_prompt: null, get_up_meter: 0, get_up_required: 0, get_up_count: 0, get_up_window_start_tick: 0, get_up_window_end_tick: 0, last_input_sequence: -1, }); +const KNEEL_WINDED_SECONDS = 0.45; +/** Seconds into the lab at which the ragdoll pose takes its blow and goes down. */ +const RAGDOLL_FALL_AT = 0.3; + const CAMERAS: Record = { face: [0.25, 1.55, 1.0], + portrait: [0.12, 1.62, 0.62], + eyes: [-0.2, 1.65, 0.44], + floor: [1.62, 0.3, 0.18], + hand: [0.55, 1.05, 0.75], front: [0, 1.35, 3.4], side: [3.4, 1.3, 0.2], "three-quarter": [2.4, 1.5, 2.6], top: [0.01, 4.5, 0.6], back: [0.2, 1.4, -3.4], low: [1.8, 0.5, 2.6], + wide: [3.4, 2.7, 3.6], }; export class ModelLab { @@ -46,7 +70,19 @@ export class ModelLab { private boxer: SkinnedBoxer | null = null; private trails: GloveTrail[] = []; private readonly trailGlove = new THREE.Vector3(); + private readonly treatEye = new THREE.Vector3(0, 1.22, 0.78); + private readonly treatFacing = new THREE.Vector3(0, 0, -1); + private readonly treatTarget = new THREE.Vector3(); private skeletonHelper: THREE.SkeletonHelper | null = null; + /** `mouthpiece=1` knocks the gum shield out of the mouth every few seconds. */ + private effects: Effects3D | null = null; + private shieldClock = Infinity; + private shieldEvent = 0; + private readonly mouth = new THREE.Vector3(); + private readonly headTurn = new THREE.Quaternion(); + /** `burst=1` shows what a burst leaves on the neck. */ + private burstRim = new Float32Array(0); + private readonly burstStump = { position: new THREE.Vector3(), quaternion: new THREE.Quaternion(), across: new THREE.Vector3(), scratch: new THREE.Vector3() }; private raf = 0; private previous = performance.now(); private elapsed = 0; @@ -68,7 +104,12 @@ export class ModelLab { this.camera = new THREE.PerspectiveCamera(38, 1, 0.1, 60); const cam = CAMERAS[this.params.get("cam") ?? "three-quarter"] ?? CAMERAS["three-quarter"]!; this.camera.position.set(...cam); - this.camera.lookAt(0, this.params.get("cam") === "face" ? 1.5 : 1.0, 0); + if (this.params.get("cam") === "hand") this.camera.lookAt(0.27, 0.84, 0.12); + else if (this.params.get("cam") === "portrait") this.camera.lookAt(0, 1.6, 0.05); + else if (this.params.get("cam") === "eyes") this.camera.lookAt(0.01, 1.6, 0.06); + else if (this.params.get("cam") === "floor") this.camera.lookAt(1.3, 0.0, -0.24); + else if (this.params.get("cam") === "wide") this.camera.lookAt(0, 0.45, -0.2); + else this.camera.lookAt(0, this.params.get("cam") === "face" ? 1.5 : 1.0, 0); this.setupLighting(); } @@ -100,6 +141,12 @@ export class ModelLab { if (trauma === "light") fighter.trauma = { head: 220, body: 260, left_eye: 190, right_eye: 60, left_cut: 40, right_cut: 0, swelling: 120, bleeding: 60 }; if (trauma === "heavy") fighter.trauma = { head: 900, body: 700, left_eye: 720, right_eye: 380, left_cut: 520, right_cut: 190, swelling: 620, bleeding: 520 }; if (trauma === "cut") fighter.trauma = { head: 420, body: 120, left_eye: 380, right_eye: 120, left_cut: 300, right_cut: 0, swelling: 260, bleeding: 380 }; + // `trauma=head,body,left_eye,right_eye,left_cut,right_cut,swelling,bleeding` sets every value. + const values = trauma?.split(",").map(Number) ?? []; + if (values.length === 8 && values.every(Number.isFinite)) { + const [head, body, leftEye, rightEye, leftCut, rightCut, swelling, bleeding] = values as [number, number, number, number, number, number, number, number]; + fighter.trauma = { head, body, left_eye: leftEye, right_eye: rightEye, left_cut: leftCut, right_cut: rightCut, swelling, bleeding }; + } const opponent = { ...base(), player_id: "other", x: 0, y: -150, facing_x: 0, facing_y: 1000 } as FighterSnapshot; const tick = Math.floor(seconds * 30); const punch = /^(jab|straight|hook|uppercut)_(left|right)(_body)?(_power)?$/.exec(pose); @@ -127,6 +174,16 @@ export class ModelLab { fighter.defense = pose as FighterSnapshot["defense"]; } else if (pose === "knockdown") { fighter.is_downed = seconds % 6 < 3.2; + } else if (pose === "kneel") { + // Folded over a body shot for a moment, then down on one knee, then back up. + const phase = seconds % 6; + fighter.is_downed = phase >= KNEEL_WINDED_SECONDS && phase < 3.4; + fighter.stunned_ticks = phase < KNEEL_WINDED_SECONDS ? 11 : 0; + } else if (pose === "winded" || pose === "stagger") { + fighter.stunned_ticks = 18; + } else if (pose === "ragdoll") { + const getUp = this.params.get("getup"); + fighter.is_downed = seconds >= RAGDOLL_FALL_AT && (getUp === null || seconds < Number(getUp)); } else if (pose === "stunned") { fighter.stunned_ticks = 40; } else if (pose === "taunt") { @@ -156,8 +213,12 @@ export class ModelLab { async start(): Promise { try { const gltf = await loadBoxerGlb(); - this.boxer = new SkinnedBoxer(gltf, { skin: 0xa9744f, gear: 0x1d4ed8 }); - this.graph = new BoxingGraph(this.boxer, worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 })); + const official = OUTFITS[this.params.get("outfit") ?? ""]; + const player = this.params.get("player"); + this.boxer = new SkinnedBoxer(gltf, official === undefined + ? { skin: 0xa9744f, gear: 0x1d4ed8, ...(player === null ? {} : { look: lookFor(player) }) } + : { skin: 0xa9744f, gear: 0x1b2230, ...official }); + this.graph = new BoxingGraph(this.boxer, worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }), { referee: official !== undefined }); this.scene.add(this.boxer.root); this.trails = [new GloveTrail(new THREE.Color(0xdbe4ff)), new GloveTrail(new THREE.Color(0xdbe4ff))]; for (const trail of this.trails) this.scene.add(trail.mesh); @@ -165,6 +226,8 @@ export class ModelLab { this.skeletonHelper = new THREE.SkeletonHelper(this.boxer.root); this.scene.add(this.skeletonHelper); } + if (this.params.get("mouthpiece") === "1" || this.params.get("burst") === "1" || this.params.has("eye")) this.effects = new Effects3D(this.scene); + if (this.params.get("burst") === "1") this.boxer.setHeadBurst(true); const dislocation = this.params.get("dislocation"); if (dislocation === "jaw" || dislocation === "shoulder_left" || dislocation === "shoulder_right") this.graph.setArcadeDislocation(dislocation); this.statusEl.textContent = `pose ${this.params.get("pose") ?? "idle"}`; @@ -218,16 +281,64 @@ export class ModelLab { graph.react(kind === "block" ? "block" : "hit", (target as Target) ?? "head", 1, (punchClass as PunchClass) ?? "straight", (hand as Hand) ?? "right", 320); } } + if (reaction === "ragdoll" && !this.reactionFired && this.elapsed >= RAGDOLL_FALL_AT - 0.02) { + this.reactionFired = true; + const hit = this.params.get("hit") ?? "straight"; + const punchClass = (hit === "body" ? "hook" : hit) as PunchClass; + graph.react("hit", hit === "body" ? "body" : "head", 1, punchClass, this.params.get("hand") === "left" ? "left" : "right", Number(this.params.get("amount") ?? 420)); + } if (this.params.get("pose") === "knockdown" && this.params.get("fall") === "prone" && this.elapsed % 6 < dt * 1.5) { graph.react("hit", "head", 1, "hook", "left", 420); } + const struck = this.params.get("side") === "left" ? 1 : -1; + if (this.params.get("pose") === "kneel") { + graph.fallToKnee(true); + const phase = this.elapsed % 6; + if (phase < KNEEL_WINDED_SECONDS) graph.windedFor(KNEEL_WINDED_SECONDS - phase, struck); + } + if (this.params.get("pose") === "winded") graph.windedFor(1, struck); + if (this.params.get("pose") === "stagger" && (this.params.has("freeze") ? !this.reactionFired && this.elapsed >= 0.02 : this.elapsed % 1.6 < dt * 1.5)) { + this.reactionFired = true; + graph.stagger(); + } graph.debugHoldImpact = this.params.get("dent") === "hold"; graph.setResting(this.params.get("pose") === "seated"); if (this.params.get("pose") === "celebrate") graph.celebrate(60); if (this.params.get("pose") === "wave_off") graph.waveOff(60); if (this.params.get("pose") === "break") graph.breakClinch(60); graph.setCountdown(this.params.get("pose") === "touch_gloves" ? 30 : null); + graph.setRefereeCount(this.params.get("pose") === "count", 3); + graph.attend(this.params.get("pose") === "attend"); + const bottle = this.params.get("prop") === "bottle"; + graph.treat(this.params.get("pose") === "treat" ? this.treatTarget.copy(this.treatEye).setY(this.treatEye.y - (bottle ? 0.075 : 0)) : null, this.treatFacing, 1, bottle ? "bottle" : "enswell"); graph.update(fighter, opponent, dt, this.elapsed, false, "full", sampledTick, head); + if (this.effects !== null && this.params.get("burst") === "1") { + const rim = measureBurstStump(this.boxer!, this.burstRim, this.burstStump); + if (rim !== null) { + this.burstRim = rim; + if (!this.effects.headBurst(0)) this.effects.burstHead(0, this.burstStump.position, 1, 7); + this.effects.anchorStump(0, this.burstStump.position, this.burstStump.quaternion, rim, this.burstStump.across); + } + } + // `eye=left|right` forces that eye out and lets it hang. + const eye = this.params.get("eye"); + if (this.effects !== null && (eye === "left" || eye === "right") && eyeSocket(this.boxer!, eye, this.mouth, this.burstStump.scratch, this.headTurn)) { + this.boxer!.headInjury.setEyeOut(eye); + if (!this.effects.eyeOut(0)) this.effects.gougeEye(0, this.mouth, this.burstStump.scratch, 1, 9); + this.effects.anchorEye(0, this.mouth, this.burstStump.scratch, this.boxer!.bone("head")!.getWorldPosition(this.burstStump.position).add(new THREE.Vector3(0, 0.12, 0.02).applyQuaternion(this.headTurn)), this.headTurn); + } + if (this.effects !== null && this.params.get("mouthpiece") === "1") { + this.shieldClock += dt; + if (this.shieldClock > 3) { + this.shieldClock = 0; + this.shieldEvent += 1; + const head = this.boxer!.bone("head")!; + head.getWorldQuaternion(this.headTurn); + head.getWorldPosition(this.mouth).add(new THREE.Vector3(0, 0.045, 0.12).applyQuaternion(this.headTurn)); + this.effects.ejectMouthpiece(0, this.mouth, this.headTurn, 1, this.shieldEvent, 0x1d4ed8, true); + } + } + this.effects?.update(dt); for (const [index, bone] of (["gloveL", "gloveR"] as const).entries()) { this.boxer!.rig.bones[bone].getWorldPosition(this.trailGlove); this.trails[index]?.update(this.trailGlove, dt, this.camera.position, dt > 0); @@ -238,11 +349,16 @@ export class ModelLab { gloveR: this.boxer!.bone("gloveR")!.getWorldPosition(new THREE.Vector3()).toArray().map((v) => Number(v.toFixed(3))), ankleL: this.boxer!.bone("ankleL")!.getWorldPosition(new THREE.Vector3()).toArray().map((v) => Number(v.toFixed(3))), ankleR: this.boxer!.bone("ankleR")!.getWorldPosition(new THREE.Vector3()).toArray().map((v) => Number(v.toFixed(3))), + kneeL: this.boxer!.bone("kneeL")!.getWorldPosition(new THREE.Vector3()).toArray().map((v) => Number(v.toFixed(3))), + kneeR: this.boxer!.bone("kneeR")!.getWorldPosition(new THREE.Vector3()).toArray().map((v) => Number(v.toFixed(3))), + metrics: this.boxer!.rig.metrics, + mouthpiece: this.effects !== null && this.effects.mouthpiecePosition(0, this.mouth) ? this.mouth.toArray().map((v) => Number(v.toFixed(3))) : null, }; } destroy(): void { cancelAnimationFrame(this.raf); + this.effects?.dispose(); this.graph?.dispose(); this.renderer.dispose(); this.root.replaceChildren(); diff --git a/web/hands/src/manifest.ts b/web/hands/src/manifest.ts index 84387146..762ae00f 100644 --- a/web/hands/src/manifest.ts +++ b/web/hands/src/manifest.ts @@ -1,5 +1,5 @@ import manifestJson from "../../../src/intelstream/hands/combat-manifest.json"; -import type { Hand, Power, PunchClass, Target } from "./types"; +import type { FighterStyle, Hand, Power, PunchClass, Target } from "./types"; export interface PunchTiming { readonly startup: number; @@ -45,6 +45,11 @@ export const REST_CORNER_OFFSET = manifestJson.rest.corner_offset; export const RING_HALF_WIDTH = manifestJson.ring.half_width; export const RING_HALF_HEIGHT = manifestJson.ring.half_height; export const FIGHTER_RADIUS = manifestJson.ring.fighter_radius; +/** Eye trauma at which the eye is swollen shut and punches from that side go unseen. */ +export const EYE_SHUT_TRAUMA = manifestJson.blind_side.eye_threshold; +/** A guard worn below this stops nothing: a block that takes it under breaks it, until it recovers past it. */ +export const GUARD_BLOCK_MINIMUM = manifestJson.guard.block_minimum; +export const CORNER_TREATMENTS = manifestJson.corner; export const RING_CORNER_REACH = manifestJson.corners.reach; /** Percent of a punch's recovery that must pass before a chained follow-up may cut the rest of it short. */ export const RECOVERY_CANCEL_PERCENT = manifestJson.combos.recovery_cancel_percent; @@ -63,8 +68,9 @@ export function recoveryCancelAge(timing: Pick= punchStaminaCost(followUp.class, followUp.target, followUp.power); + && stamina >= styledStaminaCost(style, followUp.class, followUp.target, followUp.power); } /** Ticks after a punch ends during which a compatible follow-up still counts as a combination. */ @@ -84,6 +91,25 @@ export function comboWindow(punchClass: PunchClass): number { return punches[punchClass].combo_window; } +/** A stun this long or longer is a fighter rocked by a big shot rather than a flinch. */ +export const ROCKED_BASE_TICKS = manifestJson.stun.rocked_base_ticks; +export const ROCKED_MAX_TICKS = manifestJson.stun.rocked_max_ticks; +const knockdownRules = manifestJson.knockdown; +/** The engine caps head trauma here and stops a bout at the third knockdown. */ +const HEAD_TRAUMA_LIMIT = 1400; +const KNOCKDOWN_LIMIT = 3; +/** The fewest get-up presses the engine asks for, and the most: three knockdowns on a head beaten to the cap. */ +export const GET_UP_REQUIRED_MIN = knockdownRules.get_up_base; +export const GET_UP_REQUIRED_MAX = knockdownRules.get_up_base + KNOCKDOWN_LIMIT * knockdownRules.get_up_per_knockdown + Math.floor(HEAD_TRAUMA_LIMIT / knockdownRules.get_up_trauma_divisor); +/** A stunned fighter's footwork, as a share of his speed. */ +export const STUNNED_SPEED_PERCENT = manifestJson.stun.moving_speed_percent; +/** A punch the fighter cannot pay for in full is thrown tired: this much slower to land and to recover. */ +export const TIRED_STARTUP_TICKS = manifestJson.tired.startup_ticks; +export const TIRED_RECOVERY_TICKS = manifestJson.tired.recovery_ticks; +/** The cut and the swelling at which the ringside doctor stops a bout (the engine's default). */ +export const DOCTOR_CUT = manifestJson.doctor.cut; +export const DOCTOR_SWELLING = manifestJson.doctor.swelling; + export function punchTiming(punchClass: PunchClass, target: Target, power: Power): PunchTiming { const base = punches[punchClass]; let startup = base.startup; @@ -116,6 +142,51 @@ export function punchStaminaCost(punchClass: PunchClass, target: Target, power: return cost; } +interface ManifestStyle { + readonly startup_ticks?: Partial>; + readonly recovery_ticks?: Partial>; + readonly stamina_cost_percent?: number; + readonly conditioning_loss_percent?: number; + readonly move_speed_percent?: number; + readonly evasion_ticks?: number; +} + +/** What the client needs of a style to predict the player's own punches, evasions and footwork as the engine will. */ +export interface StyleTiming { + readonly startupTicks: Readonly>>; + readonly recoveryTicks: Readonly>>; + readonly staminaCostPercent: number; + readonly conditioningLossPercent: number; + readonly moveSpeedPercent: number; + /** Ticks a slip, weave or pull lasts beyond the engine's EVASION_TICKS. */ + readonly evasionTicks: number; +} + +const styles = manifestJson.styles as unknown as Record; +/** Built once per style: the prediction asks for it many times a frame. */ +const styleTimings = new Map(); + +export function styleTiming(style: FighterStyle): StyleTiming { + const known = styleTimings.get(style); + if (known !== undefined) return known; + const raw = styles[style] ?? {}; + const timing: StyleTiming = { + startupTicks: raw.startup_ticks ?? {}, + recoveryTicks: raw.recovery_ticks ?? {}, + staminaCostPercent: raw.stamina_cost_percent ?? 100, + conditioningLossPercent: raw.conditioning_loss_percent ?? 100, + moveSpeedPercent: raw.move_speed_percent ?? 100, + evasionTicks: raw.evasion_ticks ?? 0, + }; + styleTimings.set(style, timing); + return timing; +} + +/** The stamina a fighter of `style` is charged for the punch, before any combo discount. */ +export function styledStaminaCost(style: FighterStyle, punchClass: PunchClass, target: Target, power: Power): number { + return Math.floor((punchStaminaCost(punchClass, target, power) * styleTiming(style).staminaCostPercent) / 100); +} + export function actionKey(punchClass: PunchClass, hand: Hand, target: Target, power: Power): string { return `${punchClass}:${hand}:${target}:${power}`; } diff --git a/web/hands/src/network.test.ts b/web/hands/src/network.test.ts index a1a4c9af..e5742e9d 100644 --- a/web/hands/src/network.test.ts +++ b/web/hands/src/network.test.ts @@ -1,6 +1,7 @@ import { snapshot } from "./test/fixtures"; +import { SharedActionIntent } from "./input/action-buffer"; import { NetworkController, websocketUrl } from "./network"; -import type { ServerMessage } from "./types"; +import { PROTOCOL_VERSION, type ServerMessage } from "./types"; class FakeSocket { readyState = 1; @@ -32,18 +33,18 @@ const callbacks = (overrides: Partial<{ }); const welcome = (ticket = "ticket-b") => ({ - version: 3, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, + version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [{ id: "one", name: "One", avatar: null, rating: 1500, connected: true }], server_tick: 40, next_sequence: 5, reconnect_ticket: ticket, }); const ready = { - version: 3, type: "ready", players: [ + version: PROTOCOL_VERSION, type: "ready", players: [ { id: "one", name: "One", avatar: null, rating: 1500, connected: true }, { id: "two", name: "Two", avatar: null, rating: 1500, connected: true }, ], }; const final = { - version: 3, type: "final", match_id: "match", winner_id: "one", method: "decision", round: 1, + version: PROTOCOL_VERSION, type: "final", match_id: "match", winner_id: "one", method: "decision", round: 1, scorecards: ["A", "B", "C"].map((judge) => ({ judge, player_one: [10], player_two: [9] })), ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } }, }; @@ -102,13 +103,13 @@ describe("same-origin WebSocket controller", () => { vi.advanceTimersByTime(70); const base = snapshot(); const acknowledged = { ...base, tick: 44, fighters: [{ ...base.fighters[0], player_id: welcome().player_id, last_input_sequence: sent.sequence }, base.fighters[1]] as const }; - socket.message({ version: 3, type: "snapshot", payload: acknowledged }); + socket.message({ version: PROTOCOL_VERSION, type: "snapshot", payload: acknowledged }); const measured = controller.inputLatencyMs; expect(measured).not.toBeNull(); expect(measured!).toBeGreaterThanOrEqual(70); expect(measured!).toBeLessThan(120); vi.advanceTimersByTime(40); - socket.message({ version: 3, type: "snapshot", payload: { ...acknowledged, tick: 45 } }); + socket.message({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...acknowledged, tick: 45 } }); expect(controller.inputLatencyMs).toBe(measured); controller.dispose(); }); @@ -121,7 +122,7 @@ describe("same-origin WebSocket controller", () => { controller.start(); socket.open(); socket.message({ - version: 3, + version: PROTOCOL_VERSION, type: "welcome", role: "spectator", player_id: "viewer", @@ -139,6 +140,29 @@ describe("same-origin WebSocket controller", () => { controller.dispose(); }); + it("starts sending input once a spectator is seated as a fighter before the bell", () => { + vi.useFakeTimers(); + const socket = new FakeSocket(); + const getInput = vi.fn(() => ({ moveX: 1000, moveY: 0, defense: "none" as const, actions: [] })); + const controller = new NetworkController("ticket", getInput, callbacks(), () => socket); + controller.start(); + socket.open(); + const watching = [{ ...ready.players[1] }, { id: "three", name: "Three", avatar: null, rating: 1500, connected: false }]; + socket.message({ version: PROTOCOL_VERSION, type: "welcome", role: "spectator", player_id: "one", players: watching, server_tick: 0, reconnect_ticket: "spectator-ticket" }); + vi.advanceTimersByTime(200); + expect(getInput).not.toHaveBeenCalled(); + socket.message({ ...welcome("seated-ticket"), server_tick: 0, next_sequence: 0 }); + socket.message(ready); + vi.advanceTimersByTime(100); + const frames = socket.sent.map((frame) => JSON.parse(frame) as { type: string; sequence?: number }); + expect(frames[0]?.type).toBe("authenticate"); + expect(frames.slice(1).length).toBeGreaterThan(0); + expect(frames.slice(1).every((frame) => frame.type === "input")).toBe(true); + expect(frames[1]?.sequence).toBe(0); + controller.dispose(); + expect(vi.getTimerCount()).toBe(0); + }); + it("uses the latest in-memory ticket refresh without exposing it to app callbacks", () => { vi.useFakeTimers(); const sockets: FakeSocket[] = []; @@ -153,9 +177,9 @@ describe("same-origin WebSocket controller", () => { controller.start(); sockets[0]!.open(); sockets[0]!.message(welcome("ticket-b")); - sockets[0]!.message({ version: 3, type: "ticket", reconnect_ticket: "ticket-c", refresh_id: "refresh-identifier" }); + sockets[0]!.message({ version: PROTOCOL_VERSION, type: "ticket", reconnect_ticket: "ticket-c", refresh_id: "refresh-identifier" }); expect(messages.map((message) => message.type)).toEqual(["welcome"]); - expect(JSON.parse(sockets[0]!.sent[1]!)).toEqual({ version: 3, type: "ticket_ack", refresh_id: "refresh-identifier" }); + expect(JSON.parse(sockets[0]!.sent[1]!)).toEqual({ version: PROTOCOL_VERSION, type: "ticket_ack", refresh_id: "refresh-identifier" }); sockets[0]!.disconnect(); vi.advanceTimersByTime(250); sockets[1]!.open(); @@ -195,11 +219,11 @@ describe("same-origin WebSocket controller", () => { socket.open(); socket.message(welcome()); socket.message(ready); - socket.message({ version: 3, type: "snapshot", payload: { ...snapshot(41), phase: "fight" } }); + socket.message({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...snapshot(41), phase: "fight" } }); vi.advanceTimersByTime(40); expect(inputs().at(-1)).toMatchObject({ move: { x: 0, y: 1000 }, defense: "guard_high" }); const held = inputs().length; - socket.message({ version: 3, type: "snapshot", payload: { ...snapshot(42), phase: "rest" } }); + socket.message({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...snapshot(42), phase: "rest" } }); expect(inputs()).toHaveLength(held + 1); expect(inputs().at(-1)).toMatchObject({ move: { x: 0, y: 0 }, defense: "none", actions: [] }); controller.setActive(false); @@ -259,7 +283,7 @@ describe("same-origin WebSocket controller", () => { controller.start(); socket.open(); socket.message(welcome()); - socket.message({ version: 3, type: "waiting", open_seats: 1 }); + socket.message({ version: PROTOCOL_VERSION, type: "waiting", open_seats: 1 }); controller.setActive(false); window.dispatchEvent(new Event("blur")); vi.advanceTimersByTime(200); @@ -277,7 +301,7 @@ describe("same-origin WebSocket controller", () => { controller.start(); socket.open(); socket.message(welcome()); - socket.message({ version: 3, type: "error", code }); + socket.message({ version: PROTOCOL_VERSION, type: "error", code }); expect(fatal).toHaveBeenCalledOnce(); expect(fatal).toHaveBeenCalledWith(code); expect(socket.closed).toBe(true); @@ -295,7 +319,7 @@ describe("same-origin WebSocket controller", () => { controller.start(); outdated.open(); // A server that moved on answers authenticate with its own version, so the frame itself does not decode. - outdated.message({ version: 4, type: "error", code: "client_outdated" }); + outdated.message({ version: PROTOCOL_VERSION + 1, type: "error", code: "client_outdated" }); expect(fatal).toHaveBeenCalledWith("client_outdated"); controller.dispose(); @@ -304,7 +328,7 @@ describe("same-origin WebSocket controller", () => { const other = new NetworkController("ticket", () => ({ moveX: 0, moveY: 0, defense: "none", actions: [] }), callbacks({ onFatal: protocolFatal }), () => malformed); other.start(); malformed.open(); - malformed.message({ version: 3, type: "waiting", open_seats: 2 }); + malformed.message({ version: PROTOCOL_VERSION, type: "waiting", open_seats: 3 }); expect(protocolFatal).toHaveBeenCalledWith("protocol_error"); other.dispose(); }); @@ -315,7 +339,7 @@ describe("same-origin WebSocket controller", () => { const socket = new FakeSocket(); const controller = new NetworkController("ticket", () => ({ moveX: 0, moveY: 0, defense: "none", actions: [] }), callbacks({ onReconnect: reconnect }), () => socket, () => Date.now()); controller.start(); socket.open(); socket.message(welcome()); - socket.message({ version: 3, type: "paused", player_id: "two", grace_ms: 1_000 }); + socket.message({ version: PROTOCOL_VERSION, type: "paused", player_id: "two", grace_ms: 1_000 }); expect(reconnect).toHaveBeenLastCalledWith(1_000); vi.advanceTimersByTime(250); expect(reconnect).toHaveBeenLastCalledWith(750); vi.advanceTimersByTime(750); expect(reconnect).toHaveBeenLastCalledWith(0); @@ -329,9 +353,9 @@ describe("same-origin WebSocket controller", () => { const socket = new FakeSocket(); const controller = new NetworkController("ticket", () => ({ moveX: 0, moveY: 0, defense: "none", actions: [] }), callbacks({ onReconnect: reconnect }), () => socket, () => Date.now()); controller.start(); socket.open(); socket.message(welcome()); - socket.message({ version: 3, type: "paused", player_id: "two", grace_ms: 2_000 }); + socket.message({ version: PROTOCOL_VERSION, type: "paused", player_id: "two", grace_ms: 2_000 }); vi.advanceTimersByTime(250); - socket.message({ version: 3, type: "resumed", player_id: "two" }); + socket.message({ version: PROTOCOL_VERSION, type: "resumed", player_id: "two" }); expect(reconnect).toHaveBeenLastCalledWith(0); const calls = reconnect.mock.calls.length; vi.advanceTimersByTime(3_000); @@ -339,13 +363,34 @@ describe("same-origin WebSocket controller", () => { controller.dispose(); }); + it("lets the pick and the bell end an opponent pause, so no countdown runs over the picker or the bout", () => { + vi.useFakeTimers(); + const reconnect = vi.fn(); + const socket = new FakeSocket(); + const controller = new NetworkController("ticket", () => ({ moveX: 0, moveY: 0, defense: "none", actions: [] }), callbacks({ onReconnect: reconnect }), () => socket, () => Date.now()); + controller.start(); socket.open(); socket.message(welcome()); + const select = { version: PROTOCOL_VERSION, type: "select", deadline_ms: 9_000, players: ready.players, ready: [] }; + for (const ends of [select, ready, { version: PROTOCOL_VERSION, type: "waiting", open_seats: 1 }]) { + socket.message({ version: PROTOCOL_VERSION, type: "paused", player_id: "two", grace_ms: 8_500 }); + vi.advanceTimersByTime(250); + expect(reconnect).toHaveBeenLastCalledWith(8_250); + socket.message(ends); + expect(reconnect).toHaveBeenLastCalledWith(0); + const calls = reconnect.mock.calls.length; + vi.advanceTimersByTime(9_000); + expect(reconnect).toHaveBeenCalledTimes(calls); + } + controller.dispose(); + expect(vi.getTimerCount()).toBe(0); + }); + it("gives its own dropped transport a fresh grace while the opponent is paused", () => { vi.useFakeTimers(); const reconnect = vi.fn(); const socket = new FakeSocket(); const controller = new NetworkController("ticket", () => ({ moveX: 0, moveY: 0, defense: "none", actions: [] }), callbacks({ onReconnect: reconnect }), () => socket, () => Date.now()); controller.start(); socket.open(); socket.message(welcome()); - socket.message({ version: 3, type: "paused", player_id: "two", grace_ms: 5_000 }); + socket.message({ version: PROTOCOL_VERSION, type: "paused", player_id: "two", grace_ms: 5_000 }); vi.advanceTimersByTime(4_000); socket.disconnect(); expect(reconnect).toHaveBeenLastCalledWith(20_000); @@ -359,7 +404,7 @@ describe("same-origin WebSocket controller", () => { const finalSocket = new FakeSocket(); const finalController = new NetworkController("ticket", () => ({ moveX: 0, moveY: 0, defense: "none", actions: [] }), callbacks({ onReconnect: finalReconnect }), () => finalSocket, () => Date.now()); finalController.start(); finalSocket.open(); finalSocket.message(welcome()); - finalSocket.message({ version: 3, type: "paused", player_id: "two", grace_ms: 2_000 }); + finalSocket.message({ version: PROTOCOL_VERSION, type: "paused", player_id: "two", grace_ms: 2_000 }); finalSocket.message(final); expect(finalReconnect).toHaveBeenLastCalledWith(0); expect(vi.getTimerCount()).toBe(0); @@ -368,7 +413,7 @@ describe("same-origin WebSocket controller", () => { const disposeSocket = new FakeSocket(); const disposeController = new NetworkController("ticket", () => ({ moveX: 0, moveY: 0, defense: "none", actions: [] }), callbacks({ onReconnect: disposeReconnect }), () => disposeSocket, () => Date.now()); disposeController.start(); disposeSocket.open(); disposeSocket.message(welcome()); - disposeSocket.message({ version: 3, type: "paused", player_id: "two", grace_ms: 2_000 }); + disposeSocket.message({ version: PROTOCOL_VERSION, type: "paused", player_id: "two", grace_ms: 2_000 }); disposeController.dispose(); expect(disposeReconnect).toHaveBeenLastCalledWith(0); expect(vi.getTimerCount()).toBe(0); @@ -388,9 +433,9 @@ describe("same-origin WebSocket controller", () => { controller.start(); sockets[0]!.open(); sockets[0]!.message(welcome()); - sockets[0]!.message({ version: 3, type: "paused", player_id: "two", grace_ms: 5_000 }); + sockets[0]!.message({ version: PROTOCOL_VERSION, type: "paused", player_id: "two", grace_ms: 5_000 }); now = 5_500; - sockets[0]!.message({ version: 3, type: "resumed", player_id: "two" }); + sockets[0]!.message({ version: PROTOCOL_VERSION, type: "resumed", player_id: "two" }); now = 8_000; sockets[0]!.disconnect(); expect(reconnect).toHaveBeenLastCalledWith(20_000); @@ -409,6 +454,37 @@ describe("same-origin WebSocket controller", () => { }); }); +describe("calling in the computer", () => { + const neutral = () => ({ moveX: 0, moveY: 0, defense: "none" as const, actions: [] }); + + it("asks on the open connection as a fighter, and not once that connection is gone", () => { + const socket = new FakeSocket(); + const controller = new NetworkController("ticket", neutral, callbacks(), () => socket); + expect(controller.requestCpu("rookie")).toBe(false); + controller.start(); + socket.open(); + socket.message(welcome()); + socket.message({ version: PROTOCOL_VERSION, type: "waiting", open_seats: 1 }); + expect(controller.requestCpu("champion")).toBe(true); + expect(JSON.parse(socket.sent.at(-1)!)).toEqual({ version: PROTOCOL_VERSION, type: "cpu", level: "champion" }); + socket.readyState = 3; + expect(controller.requestCpu("champion")).toBe(false); + expect(socket.sent.filter((frame) => JSON.parse(frame).type === "cpu")).toHaveLength(1); + controller.dispose(); + }); + + it("never asks for a spectator", () => { + const socket = new FakeSocket(); + const controller = new NetworkController("ticket", neutral, callbacks(), () => socket); + controller.start(); + socket.open(); + socket.message({ version: PROTOCOL_VERSION, type: "welcome", role: "spectator", player_id: "viewer", players: ready.players, server_tick: 40, reconnect_ticket: "spectator-ticket" }); + expect(controller.requestCpu("rookie")).toBe(false); + expect(socket.sent.map((frame) => JSON.parse(frame).type)).toEqual(["authenticate"]); + controller.dispose(); + }); +}); + describe("edge-triggered action sends", () => { it("flushes immediately on notifyAction and coalesces bursts", () => { vi.useFakeTimers(); @@ -600,19 +676,21 @@ describe("edge-triggered action sends", () => { vi.useRealTimers(); }); - it("stays under the server's input limit however fast the player mashes, without losing a press", () => { + it("stays under the server's input limit however fast the player mashes, and always sends the newest press", () => { vi.useFakeTimers(); let now = 1000; const socket = new FakeSocket(); - const queued: string[] = []; - let pressed = 0; + // The client's own press queue, as InputController builds it: it holds one press, the newest, as + // the engine keeps the newer of two presses in one tick, so a press the next one overtakes is dropped. + const presses = new SharedActionIntent(1); const controller = new NetworkController( "ticket", - () => ({ moveX: 0, moveY: 0, defense: "none" as const, actions: queued.splice(0, 4).map((id) => ({ kind: "punch" as const, hand: "left" as const, class: "jab" as const, target: "head" as const, power: "normal" as const, id })) }), + () => ({ moveX: 0, moveY: 0, defense: "none" as const, actions: presses.drain(4) }), callbacks(), () => socket, () => now, ); + presses.setListener(() => controller.notifyAction()); controller.start(); socket.open(); socket.message(welcome()); @@ -620,22 +698,78 @@ describe("edge-triggered action sends", () => { controller.setActive(true); const authenticated = socket.sent.length; const times: number[] = []; - const record = (): void => { while (times.length < socket.sent.length - authenticated) times.push(now); }; + const delivered: string[] = []; + let pressed = 0; + // Each frame that leaves carries the newest press, or none once that has gone out. + const record = (): void => { + for (const frame of socket.sent.slice(authenticated + times.length)) { + times.push(now); + const ids = (JSON.parse(frame).actions as { id: string }[]).map((action) => action.id); + expect(ids.length).toBeLessThanOrEqual(1); + if (ids.length === 1) expect(ids[0]).toBe(`c${pressed}`); + delivered.push(...ids); + } + }; for (let millisecond = 0; millisecond < 3000; millisecond += 1) { now += 1; vi.advanceTimersByTime(1); record(); if (millisecond < 2000 && millisecond % 2 === 0) { - if (queued.length < 4) { pressed += 1; queued.push(`p${pressed}`); } - controller.notifyAction(); + // Alternate hands, as a masher does, so no press repeats the one before it. + pressed += 1; + presses.push("keyboard", { kind: "punch", hand: pressed % 2 === 0 ? "right" : "left", class: "jab", target: "head", power: "normal" }); record(); } } for (const start of times) expect(times.filter((at) => at >= start && at < start + 1000).length).toBeLessThanOrEqual(50); expect(times.length).toBeGreaterThan(90); - const delivered = socket.sent.slice(authenticated).flatMap((frame) => (JSON.parse(frame).actions as { id: string }[]).map((action) => action.id)); - expect(delivered).toEqual(Array.from({ length: pressed }, (_unused, index) => `p${index + 1}`)); + // A thousand presses in two seconds: the ones overtaken before a frame could leave are dropped, never sent late. + expect(pressed).toBe(1000); + expect(delivered.length).toBeGreaterThan(90); + expect(delivered.length).toBeLessThan(pressed); + expect(delivered.map((id) => Number(id.slice(1)))).toEqual([...new Set(delivered.map((id) => Number(id.slice(1))))].sort((a, b) => a - b)); + expect(delivered.at(-1)).toBe(`c${pressed}`); controller.dispose(); vi.useRealTimers(); }); }); + +describe("corner instructions between rounds", () => { + it("sends the instruction on its own only during the rest, on the input sequence", () => { + vi.useFakeTimers(); + const socket = new FakeSocket(); + const getInput = vi.fn(() => ({ moveX: 1000, moveY: 0, defense: "none" as const, actions: [] })); + const controller = new NetworkController("ticket", getInput, callbacks(), () => socket); + controller.start(); + socket.open(); + socket.message(welcome()); + socket.message(ready); + socket.message({ version: PROTOCOL_VERSION, type: "snapshot", payload: snapshot() }); + expect(controller.sendCornerChoice("corner_cut")).toBe(false); + + socket.message({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...snapshot(11), phase: "rest", phase_ticks_remaining: 300 } }); + const before = socket.sent.length; + vi.advanceTimersByTime(200); + expect(socket.sent.length).toBe(before); + expect(controller.sendCornerChoice("corner_breath")).toBe(true); + const frame = JSON.parse(socket.sent.at(-1)!) as { type: string; sequence: number; move: unknown; defense: string; actions: unknown[] }; + expect(frame).toMatchObject({ type: "input", move: { x: 0, y: 0 }, defense: "none", actions: [{ kind: "corner_breath" }] }); + expect(frame.sequence).toBeGreaterThanOrEqual(5); + + socket.message(final); + expect(controller.sendCornerChoice("corner_cut")).toBe(false); + controller.dispose(); + }); + + it("never sends a corner instruction for a spectator", () => { + const socket = new FakeSocket(); + const controller = new NetworkController("ticket", () => ({ moveX: 0, moveY: 0, defense: "none" as const, actions: [] }), callbacks(), () => socket); + controller.start(); + socket.open(); + socket.message({ version: PROTOCOL_VERSION, type: "welcome", role: "spectator", player_id: "viewer", players: ready.players, server_tick: 40, reconnect_ticket: "spectator-ticket" }); + socket.message({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...snapshot(11), phase: "rest", phase_ticks_remaining: 300 } }); + expect(controller.sendCornerChoice("corner_cut")).toBe(false); + expect(socket.sent.map((sent) => JSON.parse(sent).type)).toEqual(["authenticate"]); + controller.dispose(); + }); +}); diff --git a/web/hands/src/network.ts b/web/hands/src/network.ts index 5d197d20..260cf017 100644 --- a/web/hands/src/network.ts +++ b/web/hands/src/network.ts @@ -1,6 +1,6 @@ import { ClientError, safeError } from "./api"; -import { decodeServerFrame, encodeInput, parseStrictJson } from "./protocol"; -import { PROTOCOL_VERSION, type ConnectionRole, type EngineSnapshot, type InputFrame, type ServerMessage } from "./types"; +import { decodeServerFrame, encodeCpuRequest, encodeInput, encodeStyleChoice, parseStrictJson } from "./protocol"; +import { PROTOCOL_VERSION, type ConnectionRole, type CornerKind, type CpuLevel, type EngineSnapshot, type FighterStyle, type InputFrame, type ServerMessage } from "./types"; export function websocketUrl(location: Location = window.location): string { const url = new URL("/api/hands/ws", location.origin); @@ -78,6 +78,7 @@ export class NetworkController { private serverTick = 0; private role: ConnectionRole | null = null; private active = false; + private resting = false; /** Input has been on, so the server's engine exists and accepts a frame in any phase. */ private boutStarted = false; private disposed = false; @@ -102,13 +103,17 @@ export class NetworkController { start(): void { if (this.disposed || this.listenersBound) return; + this.bindInputListeners(); + this.connect(); + if (!this.disposed && !this.terminal) this.inputTimer = window.setInterval(() => this.flushInput(), INPUT_FLUSH_MS); + } + + private bindInputListeners(): void { this.listenersBound = true; window.addEventListener("blur", this.onInputLoss); window.addEventListener("focus", this.onInputRegain); window.addEventListener("pointerdown", this.onPagePointer, true); document.addEventListener("visibilitychange", this.onVisibilityChange); - this.connect(); - if (!this.disposed && !this.terminal) this.inputTimer = window.setInterval(() => this.flushInput(), INPUT_FLUSH_MS); } setActive(active: boolean): void { @@ -252,11 +257,17 @@ export class NetworkController { if (message.reconnect_ticket === undefined) throw new Error("missing_reconnect_ticket"); this.reconnectTicket = message.reconnect_ticket; this.serverTick = message.server_tick; + // A seat that opens before the bell goes to a spectator, who is welcomed again as a fighter. + const seated = this.role === "spectator" && message.role === "fighter"; this.role = message.role; this.playerId = message.role === "fighter" ? message.player_id : null; this.sentAt.clear(); if (message.role === "fighter") this.nextSequence = Math.max(this.nextSequence, message.next_sequence); else this.stopInputLifecycle(); + if (seated) { + if (!this.listenersBound) this.bindInputListeners(); + this.inputTimer ??= window.setInterval(() => this.flushInput(), INPUT_FLUSH_MS); + } this.attempts = 0; this.clearTransportReconnect(true); } else if (message.type === "ticket") { @@ -264,6 +275,7 @@ export class NetworkController { } else if (message.type === "snapshot") { this.serverTick = Math.max(this.serverTick, message.payload.tick); this.setInputActive(["countdown", "fight", "knockdown", "foul_recovery"].includes(message.payload.phase)); + this.resting = message.payload.phase === "rest"; this.observeAcknowledgement(message.payload); } else if (message.type === "paused") { this.setInputActive(false); @@ -272,9 +284,15 @@ export class NetworkController { this.setInputActive(true); this.attempts = 0; this.clearOpponentPause(true); + } else if (message.type === "select") { + // A pause belongs to the bout. Before the bell the pick itself says who is connected, and an + // empty seat at the bell comes with a fresh pause after the ready. + this.clearOpponentPause(true); } else if (message.type === "ready") { + this.clearOpponentPause(true); this.setInputActive(true); } else if (message.type === "waiting") { + this.clearOpponentPause(true); this.setInputActive(false); } else if (message.type === "final" || message.type === "error") { this.active = false; @@ -326,6 +344,32 @@ export class NetworkController { }, delay); } + /** Asks the room to put the computer in the empty seat; false when there is no open connection to ask on. */ + requestCpu(level: CpuLevel): boolean { + const socket = this.socket; + if (this.role !== "fighter" || this.disposed || this.terminal || socket?.readyState !== OPEN) return false; + try { + socket.send(encodeCpuRequest(level)); + return true; + } catch { + this.handleClose(socket); + return false; + } + } + + /** Tells the room which style this fighter is choosing, or has settled on when `ready`. */ + chooseStyle(style: FighterStyle, ready: boolean): boolean { + const socket = this.socket; + if (this.role !== "fighter" || this.disposed || this.terminal || socket?.readyState !== OPEN) return false; + try { + socket.send(encodeStyleChoice(style, ready)); + return true; + } catch { + this.handleClose(socket); + return false; + } + } + /** Smoothed round trip from an input send to the first snapshot acknowledging it, in milliseconds. */ get inputLatencyMs(): number | null { return this.latencyMs; @@ -352,8 +396,18 @@ export class NetworkController { } private sendInput(frame: InputFrame, whileInactive = false): boolean { + return (this.active || whileInactive) && this.transmit(frame); + } + + /** Tells the corner what to work on. The rest is the one phase the input stream is off, so this frame goes on its own. */ + sendCornerChoice(kind: CornerKind): boolean { + if (!this.resting) return false; + return this.transmit({ moveX: 0, moveY: 0, defense: "none", actions: [{ kind }] }); + } + + private transmit(frame: InputFrame): boolean { const socket = this.socket; - if (this.role !== "fighter" || (!this.active && !whileInactive) || this.disposed || this.terminal || socket?.readyState !== OPEN) return false; + if (this.role !== "fighter" || this.disposed || this.terminal || socket?.readyState !== OPEN) return false; try { const actions = frame.actions.slice(0, 4); socket.send(encodeInput(this.nextSequence, this.serverTick, { ...frame, actions })); diff --git a/web/hands/src/prediction.test.ts b/web/hands/src/prediction.test.ts index f5f8aac1..99a99b4d 100644 --- a/web/hands/src/prediction.test.ts +++ b/web/hands/src/prediction.test.ts @@ -1,11 +1,14 @@ -import { cancelsRecovery, punchStaminaCost, punchTiming, recoveryCancelAge } from "./manifest"; +import { cancelsRecovery, punchStaminaCost, punchTiming, recoveryCancelAge, styledStaminaCost, styleTiming } from "./manifest"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { SnapshotBuffer } from "./interpolation"; -import { attackTicksRemaining, canAffordPunch, constrainPrediction, EVASION_STAMINA, EVASION_TICKS, EvasionPrediction, fatigueFactor, MINIMUM_SEPARATION, MovementPrediction, movementLocked, predictedDefense, predictMovement, predictedPunchTiming, type HeldInput } from "./prediction"; +import { attackTicksRemaining, constrainPrediction, EVASION_STAMINA, EVASION_TICKS, EvasionPrediction, fatigueFactor, MINIMUM_SEPARATION, MovementPrediction, movementLocked, parryRootedUntil, predictedDefense, predictMovement, predictedPunchTiming, type FootworkHolds, type HeldInput } from "./prediction"; +import { decodeServerFrame, ProtocolError } from "./protocol"; import { fighter, snapshot } from "./test/fixtures"; +import movementTraces from "./test/movement-traces.json"; import timingTable from "./test/punch-timing-table.json"; -import type { EngineSnapshot, FighterSnapshot, PunchClass } from "./types"; +import styleTimingTable from "./test/style-timing-table.json"; +import { PROTOCOL_VERSION, type EngineSnapshot, type FighterSnapshot, type FighterStyle, type PunchClass } from "./types"; describe("local movement prediction", () => { it("mirrors the authoritative fatigue factor", () => { @@ -25,7 +28,7 @@ describe("local movement prediction", () => { expect(Math.hypot(diagonal.dx, diagonal.dy)).toBeLessThanOrEqual(7 * 4 + 1e-9); expect(diagonal.dx).toBeCloseTo(diagonal.dy, 9); }); - it("slows while guarding and stops entirely while punching, stunned, or down", () => { + it("slows while guarding and stops entirely while punching, slipping, or down", () => { const still = { ...fighter("one"), conditioning: 1000 }; const guarded = predictMovement(still, { moveX: 1000, moveY: 0, defense: "guard_high" }, 4); const open = predictMovement(still, { moveX: 1000, moveY: 0, defense: "none" }, 4); @@ -33,7 +36,6 @@ describe("local movement prediction", () => { expect(guarded.dx).toBeGreaterThan(0); for (const locked of [ { ...still, action: "jab" as const }, - { ...still, stunned_ticks: 5 }, { ...still, is_downed: true }, { ...still, defense: "weave" as const }, ]) { @@ -42,6 +44,21 @@ describe("local movement prediction", () => { } expect(movementLocked(still)).toBe(false); }); + it("lets a stunned fighter stumble at the engine's share of his speed, with his guard down", () => { + const still = { ...fighter("one"), conditioning: 1000, velocity_x: 0, velocity_y: 0 }; + const held = { moveX: 1000, moveY: 0, defense: "none" as const }; + const free = predictMovement(still, held, 6); + const stunned = { ...still, stunned_ticks: 20 }; + const stumbling = predictMovement(stunned, held, 6); + expect(movementLocked(stunned)).toBe(false); + expect(stumbling.dx).toBeGreaterThan(0); + expect(stumbling.dx).toBeLessThan(free.dx * 0.75); + // Engine order: the stun counts down before the footwork, and a stunned fighter's guard is down. + const lastStunnedTick = predictMovement({ ...still, stunned_ticks: 1 }, held, 1); + expect(lastStunnedTick.dx).toBeCloseTo(free.dx === 0 ? 0 : predictMovement(still, held, 1).dx, 9); + const guardDropped = predictMovement(stunned, { ...held, defense: "guard_high" }, 6); + expect(guardDropped.dx).toBeCloseTo(stumbling.dx, 9); + }); it("frees the fighter when the punch ends even though the snapshot still presents it", () => { const still = { ...fighter("one"), conditioning: 1000 }; const hook = { ...still, action: "hook" as const, action_start_tick: 100, action_startup_ticks: 7, action_active_ticks: 3, action_recovery_ticks: 12 }; @@ -53,14 +70,49 @@ describe("local movement prediction", () => { expect(movementLocked(hook, 122)).toBe(false); expect(predictMovement(hook, held, 4, 110)).toEqual({ dx: 0, dy: 0 }); expect(predictMovement(hook, held, 4, 130)).toEqual(predictMovement(still, held, 4)); + // The engine frees him on the tick the punch ends, 122: three of the four ticks after 120. const leaving = predictMovement(hook, held, 4, 120); expect(leaving.dx).toBeGreaterThan(0); - expect(leaving.dx).toBeCloseTo(predictMovement(still, held, 2).dx); - expect(predictMovement({ ...hook, stunned_ticks: 5 }, held, 4, 130)).toEqual({ dx: 0, dy: 0 }); + expect(leaving.dx).toBeCloseTo(predictMovement(still, held, 3).dx); + const stumbling = predictMovement({ ...hook, stunned_ticks: 5 }, held, 4, 130); + expect(stumbling.dx).toBeGreaterThan(0); + expect(stumbling.dx).toBeLessThan(predictMovement(still, held, 4).dx); expect(predictMovement({ ...hook, queued_actions: 1 }, held, 4, 130)).toEqual({ dx: 0, dy: 0 }); expect(movementLocked({ ...still, queued_actions: 1 }, 130)).toBe(true); }); + it("lets a stunned fighter stumble though the punch the stun cut short is still presented", () => { + const held = { moveX: -1000, moveY: 0, defense: "none" as const }; + const still = { ...fighter("one"), conditioning: 1000, stunned_ticks: 20 }; + const hook = { ...still, action: "hook" as const, action_id: "c1", action_start_tick: 197, action_startup_ticks: 9, action_active_ticks: 2, action_recovery_ticks: 14 }; + const stumble = predictMovement(still, held, 4, 200); + expect(stumble.dx).toBeLessThan(-10); + expect(predictMovement(hook, held, 4, 200)).toEqual(stumble); + // Once the stun is over, the punch it cut no longer holds him, for a player who saw him stunned in it. + const after = { ...hook, stunned_ticks: 0 }; + expect(predictMovement(after, held, 4, 205)).toEqual({ dx: 0, dy: 0 }); + expect(predictMovement(after, held, 4, 205, { cutActionId: "c1" })).toEqual(predictMovement({ ...after, action: null, action_id: null }, held, 4, 205)); + const prediction = new MovementPrediction(); + prediction.update(hook, held, false, 0, 4, 200, 1, 30); + expect(prediction.update(after, held, false, 1000, 4, 205, 1, 30).dx).toBeLessThan(-10); + expect(new MovementPrediction().update(after, held, false, 1000, 4, 205, 1, 30)).toEqual({ dx: 0, dy: 0 }); + }); + + it("holds the player's own fighter still for a parry's stagger once it hears of the parry, then walks him off", () => { + const held = { moveX: -1000, moveY: 0, defense: "none" as const }; + const parried = { ...fighter("one"), conditioning: 1000, stunned_ticks: 18 }; + const parry = { event_id: 1, tick: 98, kind: "parry", actor_id: "two", target_id: "one", amount: 20, detail: "", blood: 0, direction: 0, action_id: "c4" }; + const prediction = new MovementPrediction(); + prediction.observe({ ...parry, target_id: "two" }, "one"); + expect(prediction.update(parried, held, false, 0, 4, 100, 1, 30).dx).toBeLessThan(-5); + const rooted = new MovementPrediction(); + rooted.observe(parry, "one"); + expect(rooted.update(parried, held, false, 0, 4, 100, 1, 30)).toEqual({ dx: 0, dy: 0 }); + // With two ticks of the stagger left the engine roots him for one more: three of the four ticks ahead are his. + const leaving = rooted.update({ ...parried, stunned_ticks: 2 }, held, false, 1000, 4, 116, 1, 30); + expect(leaving.dx).toBeCloseTo(predictMovement({ ...parried, stunned_ticks: 0 }, held, 3).dx, 5); + }); + it("predicts the timing and cost the engine will use", () => { const fresh = { ...fighter("one"), conditioning: 1000, stance: "orthodox" as const }; const jab = { class: "jab" as const, hand: "left" as const, target: "head" as const, power: "normal" as const }; @@ -75,8 +127,21 @@ describe("local movement prediction", () => { expect(punchStaminaCost("jab", "body", "normal")).toBe(46); expect(punchStaminaCost("jab", "head", "power")).toBe(65); expect(punchStaminaCost("jab", "body", "power")).toBe(71); - expect(canAffordPunch({ ...fresh, stamina: 42 }, jab)).toBe(true); - expect(canAffordPunch({ ...fresh, stamina: 41 }, jab)).toBe(false); + }); + + it("predicts the slow arm punch the engine throws for a fighter who cannot pay in full", () => { + const fresh = { ...fighter("one"), conditioning: 1000, stance: "orthodox" as const }; + const jab = { class: "jab" as const, hand: "left" as const, target: "head" as const, power: "normal" as const }; + expect(predictedPunchTiming({ ...fresh, stamina: 42 }, jab)).toMatchObject({ startup: 3, recovery: 7 }); + expect(predictedPunchTiming({ ...fresh, stamina: 10 }, jab)).toMatchObject({ startup: 6, recovery: 12 }); + expect(predictedPunchTiming({ ...fresh, conditioning: 640, stamina: 41 }, jab)).toMatchObject({ startup: 7, recovery: 13 }); + expect(predictedPunchTiming({ ...fresh, conditioning: 640, stamina: 119 }, { ...jab, class: "hook", power: "power" })).toMatchObject({ startup: 14, recovery: 23 }); + expect(predictedPunchTiming({ ...fresh, conditioning: 820, stamina: 0, trauma: { ...fresh.trauma, body: 350 } }, { class: "uppercut", hand: "right", target: "body", power: "normal" })).toMatchObject({ startup: 14, recovery: 21 }); + // No combination discount for a tired punch, inside the window or out of it. + const afterJab = { ...fresh, conditioning: 108, stamina: 50, action: "jab" as const, action_start_tick: 100, action_startup_ticks: 3, action_active_ticks: 2, action_recovery_ticks: 7 }; + const straight = { class: "straight" as const, hand: "right" as const, target: "head" as const, power: "power" as const }; + expect(predictedPunchTiming(afterJab, straight, 110)).toMatchObject({ startup: 18, recovery: 30 }); + expect(predictedPunchTiming(afterJab, straight, 125)).toMatchObject({ startup: 18, recovery: 30 }); }); it("predicts the startup and recovery the engine gives every punch, its own cost taken off the conditioning first", () => { @@ -93,6 +158,31 @@ describe("local movement prediction", () => { expect(mismatches).toEqual([]); }); + it("predicts each style's timing and price for every punch exactly as the engine gives them", () => { + const classes: Record = { j: "jab", s: "straight", h: "hook", u: "uppercut" }; + const mismatches: string[] = []; + for (const row of styleTimingTable as string[]) { + const [style, key, conditioning, body, startup, recovery, cost] = row.split(",") as [FighterStyle, string, string, string, string, string, string]; + const base = fighter("one"); + const state: FighterSnapshot = { ...base, style, stance: "orthodox", conditioning: Number(conditioning), trauma: { ...base.trauma, body: Number(body) } }; + const punch = { class: classes[key[0]!]!, target: key[1] === "h" ? "head" as const : "body" as const, power: key[2] === "p" ? "power" as const : "normal" as const, hand: key[3] === "l" ? "left" as const : "right" as const }; + const timing = predictedPunchTiming(state, punch); + if (timing.startup !== Number(startup) || timing.recovery !== Number(recovery)) mismatches.push(`${row} -> ${timing.startup},${timing.recovery}`); + if (styledStaminaCost(style, punch.class, punch.target, punch.power) !== Number(cost)) mismatches.push(`${row} cost`); + } + expect(styleTimingTable.length).toBe(1280); + expect(mismatches).toEqual([]); + }); + + it("moves a swarmer as much quicker, and a slugger as much slower, as the engine does", () => { + const held = { moveX: 1000, moveY: 0, defense: "none" as const }; + const balanced = predictMovement({ ...fighter("one"), conditioning: 1000 }, held, 20); + const swarmer = predictMovement({ ...fighter("one"), conditioning: 1000, style: "swarmer" }, held, 20); + const slugger = predictMovement({ ...fighter("one"), conditioning: 1000, style: "slugger" }, held, 20); + expect(swarmer.dx / balanced.dx).toBeCloseTo(styleTiming("swarmer").moveSpeedPercent / 100, 2); + expect(slugger.dx / balanced.dx).toBeCloseTo(0.96, 2); + }); + it("charges a punch thrown inside the combination window at the engine's discount", () => { const afterJab = { ...fighter("one"), stance: "orthodox" as const, conditioning: 108, action: "jab" as const, action_start_tick: 100, action_startup_ticks: 3, action_active_ticks: 2, action_recovery_ticks: 7 }; const straight = { class: "straight" as const, hand: "right" as const, target: "head" as const, power: "power" as const }; @@ -126,6 +216,21 @@ describe("local movement prediction", () => { expect(coast.dx).toBeCloseTo(3 + 1.5 + 0.75, 5); }); + it("takes a velocity past the walking speed clamped to it rather than ending the bout, and seeds the step from that", () => { + const base = snapshot(); + const decode = (one: FighterSnapshot): FighterSnapshot => { + const message = decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...base, fighters: [one, base.fighters[1]] } })); + if (message.type !== "snapshot") throw new Error(message.type); + return message.payload.fighters[0]; + }; + // A fresh swarmer pushed back onto a corner pad keeps 7.7 units a tick along it. + const swarmer = decode({ ...base.fighters[0], style: "swarmer", conditioning: 1000, velocity_x: 8, velocity_y: -9 }); + expect([swarmer.velocity_x, swarmer.velocity_y]).toEqual([7, -7]); + const coast = predictMovement(swarmer, { moveX: 0, moveY: 0, defense: "none" }, 1); + expect(Math.hypot(coast.dx, coast.dy)).toBeLessThanOrEqual(7 * 1.1); + expect(() => decode({ ...base.fighters[0], velocity_x: 7.5 })).toThrow(ProtocolError); + }); + it("mirrors the engine's recovery cancel: a landed, chained, affordable follow-up on a defender who is not stunned", () => { expect(recoveryCancelAge(punchTiming("straight", "head", "normal"))).toBe(6 + 2 + 5); expect(recoveryCancelAge(punchTiming("hook", "head", "normal"))).toBe(7 + 3 + 6); @@ -137,9 +242,52 @@ describe("local movement prediction", () => { expect(cancelsRecovery("straight", true, hook, punchStaminaCost("hook", "head", "normal") - 1, false)).toBe(false); expect(cancelsRecovery("straight", true, { ...hook, class: "jab" }, 900, false)).toBe(false); expect(cancelsRecovery("hook", true, { ...hook, class: "uppercut" }, 900, false)).toBe(true); + // The engine charges the style's price: a slugger short of it throws a tired follow-up, and no combination. + const sluggerCost = styledStaminaCost("slugger", "hook", "head", "normal"); + expect(sluggerCost).toBeGreaterThan(punchStaminaCost("hook", "head", "normal")); + expect(cancelsRecovery("straight", true, hook, sluggerCost - 1, false, "slugger")).toBe(false); + expect(cancelsRecovery("straight", true, hook, sluggerCost - 1, false)).toBe(true); }); }); +describe("footwork predicted against what the engine did", () => { + interface TraceRecord { readonly tick: number; readonly fighter: unknown; readonly events: unknown[]; readonly held: HeldInput } + const LOOKAHEAD = 4; + + /** Each tick's snapshot as the client decodes it, with what it has learnt so far of parries and cut punches. */ + function replay(records: readonly TraceRecord[]): { fighter: FighterSnapshot; holds: FootworkHolds; held: HeldInput; struck: boolean }[] { + let rootedUntil = 0; + let cutActionId: string | null = null; + return records.map((record) => { + const payload = { ...snapshot(record.tick), fighters: [record.fighter, fighter("two", 400)], events: record.events }; + const message = decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload })); + if (message.type !== "snapshot") throw new Error(message.type); + const one = message.payload.fighters[0]; + for (const event of message.payload.events) rootedUntil = parryRootedUntil(event, "one", rootedUntil); + if (one.stunned_ticks > 0 && one.action_id !== null) cutActionId = one.action_id; + const struck = message.payload.events.some((event) => event.target_id === "one" && ["hit", "counter_hit", "parry"].includes(event.kind)); + return { fighter: one, holds: { rootedUntil, cutActionId }, held: record.held, struck }; + }); + } + + for (const [name, records] of Object.entries(movementTraces as Record)) { + it(`${name}: every tick's ${LOOKAHEAD}-tick step is the engine's to within 2 units`, () => { + const decoded = replay(records); + const misses: string[] = []; + // A punch landing on him inside the look-ahead is news no snapshot before it carries. + for (let index = 0; index + LOOKAHEAD < decoded.length; index += 1) { + if (decoded.slice(index + 1, index + 1 + LOOKAHEAD).some((later) => later.struck)) continue; + const { fighter: now, holds, held } = decoded[index]!; + const later = decoded[index + LOOKAHEAD]!.fighter; + const step = predictMovement(now, held, LOOKAHEAD, records[index]!.tick, holds); + const miss = Math.hypot(step.dx - (later.x - now.x), step.dy - (later.y - now.y)); + if (miss > 2) misses.push(`tick ${records[index]!.tick}: predicted ${step.dx.toFixed(1)}, engine ${later.x - now.x}`); + } + expect(misses).toEqual([]); + }); + } +}); + describe("walking behind a round trip", () => { const PRESS_MS = 500; const RELEASE_MS = 1500; @@ -256,6 +404,19 @@ describe("the player's own defence", () => { expect(evasion.pose(1000 + (6 + EVASION_TICKS) * tick)).toBeNull(); }); + it("holds a style's longer evasion as long as the engine plays it", () => { + const tick = 1000 / 30; + const ticks = EVASION_TICKS + styleTiming("swarmer").evasionTicks; + expect(ticks).toBe(EVASION_TICKS + 1); + expect(styleTiming("balanced").evasionTicks).toBe(0); + const evasion = new EvasionPrediction(); + evasion.press("weave", "c3", 0, 6, 30, ticks); + expect(evasion.holdsFeet((ticks - 1) * tick, 30)).toBe(true); + expect(evasion.holdsFeet(ticks * tick, 30)).toBe(false); + expect(evasion.pose((6 + ticks - 1) * tick)).toBe("weave"); + expect(evasion.pose((6 + ticks) * tick)).toBeNull(); + }); + it("drops a slip the server turned down or a stun cut short", () => { const sequenceOf = (id: string): number | null => (id === "c8" ? 41 : null); const refused = new EvasionPrediction(); @@ -286,5 +447,6 @@ describe("the player's own defence", () => { const engine = readFileSync(resolve(process.cwd(), "../../src/intelstream/hands/engine.py"), "utf8"); expect(engine).toContain(`EVASION_TICKS: Final = ${EVASION_TICKS}`); expect(engine).toContain(`if fighter.stamina < ${EVASION_STAMINA}:`); + expect(engine).toContain("fighter.evasion_ticks = EVASION_TICKS + fighter.style_rule.evasion_ticks"); }); }); diff --git a/web/hands/src/prediction.ts b/web/hands/src/prediction.ts index 77b5c68c..6d778acb 100644 --- a/web/hands/src/prediction.ts +++ b/web/hands/src/prediction.ts @@ -1,5 +1,5 @@ -import { comboChain, comboWindow, FIGHTER_RADIUS, punchStaminaCost, punchTiming, RING_CORNER_REACH, RING_HALF_HEIGHT, RING_HALF_WIDTH, type PunchTiming } from "./manifest"; -import type { DefensivePose, FighterSnapshot, Hand, HeldDefense, MovementKind, Power, PunchClass, Target } from "./types"; +import { comboChain, comboWindow, FIGHTER_RADIUS, punchTiming, RING_CORNER_REACH, RING_HALF_HEIGHT, RING_HALF_WIDTH, STUNNED_SPEED_PERCENT, styledStaminaCost, styleTiming, TIRED_RECOVERY_TICKS, TIRED_STARTUP_TICKS, type PunchTiming } from "./manifest"; +import type { CombatEvent, DefensivePose, FighterSnapshot, Hand, HeldDefense, MovementKind, Power, PunchClass, Target } from "./types"; export interface HeldInput { readonly moveX: number; @@ -38,28 +38,30 @@ export function inComboWindow(fighter: FighterSnapshot, punch: PunchIntent, tick /** * The timing the engine will give this punch if it starts at `tick`: slower for a tired fighter, a * tick quicker for the lead-hand jab. The engine takes the punch's own cost off the conditioning - * before it measures fatigue, at the combination discount inside the window. + * before it measures fatigue, at the combination discount inside the window and at the rate the + * fighter's style tires at. A fighter who cannot pay the full cost throws a tired arm punch on what + * stamina is left, slower still and never a combination. For a style that tires less, the engine + * carries the fraction of a point it saves from punch to punch, which no snapshot shows; this takes + * none, as for a fresh fighter, so it can be a point off, and in about one such punch in a hundred + * that crosses a fatigue step and puts the timing a tick out. */ export function predictedPunchTiming(fighter: FighterSnapshot, punch: PunchIntent, tick?: number): PunchTiming { const base = punchTiming(punch.class, punch.target, punch.power); - const fullCost = punchStaminaCost(punch.class, punch.target, punch.power); - const cost = tick !== undefined && inComboWindow(fighter, punch, tick) ? Math.max(1, Math.floor((fullCost * 90) / 100)) : fullCost; - const conditioning = Math.max(0, fighter.conditioning - Math.max(1, Math.floor(cost / 12))); + const style = styleTiming(fighter.style); + const fullCost = styledStaminaCost(fighter.style, punch.class, punch.target, punch.power); + const tired = fighter.stamina < fullCost; + const cost = tired ? fighter.stamina : tick !== undefined && inComboWindow(fighter, punch, tick) ? Math.max(1, Math.floor((fullCost * 90) / 100)) : fullCost; + const conditioning = Math.max(0, fighter.conditioning - Math.floor((Math.max(1, Math.floor(cost / 12)) * style.conditioningLossPercent) / 100)); const speed = fatigueFactor(conditioning, fighter.trauma.body); const lead = fighter.stance === "orthodox" ? "left" : "right"; const quick = punch.class === "jab" && punch.hand === lead ? 1 : 0; return { ...base, - startup: Math.max(2, Math.floor((base.startup * 100) / speed) - quick), - recovery: Math.max(4, Math.floor((base.recovery * 100) / speed)), + startup: Math.max(2, Math.floor((base.startup * 100) / speed) - quick + (style.startupTicks[punch.class] ?? 0)) + (tired ? TIRED_STARTUP_TICKS : 0), + recovery: Math.max(4, Math.floor((base.recovery * 100) / speed) + (style.recoveryTicks[punch.class] ?? 0)) + (tired ? TIRED_RECOVERY_TICKS : 0), }; } -/** False when the fighter cannot pay the punch's full cost: the engine checks it before any combo discount. */ -export function canAffordPunch(fighter: FighterSnapshot, punch: PunchIntent): boolean { - return fighter.stamina >= punchStaminaCost(punch.class, punch.target, punch.power); -} - /** * Ticks until the fighter's punch ends, as of snapshot `tick`. The engine keeps a finished punch in * snapshots for a while so clients can present it, so `action` alone does not mean the fighter is @@ -80,7 +82,6 @@ function stateLocked(fighter: FighterSnapshot): boolean { // A queued punch starts the tick the current one ends, so the fighter is never free in between. return fighter.queued_actions > 0 || fighter.is_downed - || fighter.stunned_ticks > 0 || fighter.clinch_ticks > 0 || fighter.clinch_startup_ticks > 0 || fighter.taunt_ticks > 0 @@ -95,10 +96,20 @@ export interface MovementIntent { readonly speed: number; } -/** The fatigue- and guard-scaled walking intent of `held`, as the engine's `_move_fighter` works it out, written to `out`. */ -export function movementIntent(fighter: FighterSnapshot, held: HeldInput, out: { x: number; y: number; speed: number } = { x: 0, y: 0, speed: 0 }): MovementIntent { - let speed = Math.max(2, Math.floor((MAX_SPEED * fatigueFactor(fighter.conditioning, fighter.trauma.body)) / 100)); - if (held.defense === "guard_high" || held.defense === "guard_low") speed = Math.max(2, Math.floor((speed * GUARD_SPEED_PERCENT) / 100)); +/** + * The fatigue-, guard-, stun- and style-scaled walking intent of `held` on the tick `step` ticks after + * the snapshot, as the engine's `_move_fighter` works it out, written to `out`. The engine counts a + * stun down before the footwork of each tick, and a stunned fighter's guard is down: he stumbles at a + * share of his plain speed while the stun has ticks left after that. + */ +export function movementIntent(fighter: FighterSnapshot, held: HeldInput, out: { x: number; y: number; speed: number } = { x: 0, y: 0, speed: 0 }, step = 0): MovementIntent { + const base = Math.max(2, Math.floor((MAX_SPEED * fatigueFactor(fighter.conditioning, fighter.trauma.body)) / 100)); + const stunned = fighter.stunned_ticks - step; + let speed = base; + if (stunned > 1) speed = Math.max(2, Math.floor((base * STUNNED_SPEED_PERCENT) / 100)); + else if (stunned <= 0 && (held.defense === "guard_high" || held.defense === "guard_low")) speed = Math.max(2, Math.floor((base * GUARD_SPEED_PERCENT) / 100)); + // The engine moves in thousandths of a unit, so a style's few percent of footspeed are kept. + speed *= styleTiming(fighter.style).moveSpeedPercent / 100; const magnitude = Math.hypot(held.moveX, held.moveY); const scale = magnitude > 1000 ? 1000 / magnitude : 1; out.x = (held.moveX * scale * speed) / 1000; @@ -107,17 +118,49 @@ export function movementIntent(fighter: FighterSnapshot, held: HeldInput, out: { return out; } +/** + * The tick until which `playerId` stands rooted, given an event from the engine and the tick known so + * far: a parry roots the fighter it staggers for the stagger (its `amount`), so the counter it opens + * can land. + */ +export function parryRootedUntil(event: CombatEvent, playerId: string | null, current: number): number { + return event.kind === "parry" && event.target_id === playerId ? Math.max(current, event.tick + event.amount) : current; +} + +/** What holds a fighter's feet that a snapshot does not show: `MovementPrediction` keeps track of it. */ +export interface FootworkHolds { + /** The tick a parry roots him until (`parryRootedUntil`). */ + readonly rootedUntil?: number; + /** A punch he was seen stunned in: the stun cut it short, though snapshots go on presenting it. */ + readonly cutActionId?: string | null; +} + +/** + * Steps of the replay from snapshot `tick` that the engine holds the fighter's feet still. A punch + * holds them through its last tick, and the engine frees him on the tick it ends. A stun cuts a + * punch short on the tick it lands, though snapshots go on presenting the punch with its own timing + * for a while, so a stunned fighter is not held by one, nor afterwards by the punch a stun was seen + * to cut. A parry roots him while its stagger lasts. + */ +function heldSteps(fighter: FighterSnapshot, tick: number, holds: FootworkHolds): number { + const cut = fighter.stunned_ticks > 0 || (fighter.action_id !== null && fighter.action_id === holds.cutActionId); + const punch = cut ? 0 : Math.max(0, attackTicksRemaining(fighter, tick) - 1); + const rooted = fighter.stunned_ticks > 0 ? Math.min((holds.rootedUntil ?? 0) - tick, fighter.stunned_ticks) - 1 : 0; + return Math.max(punch, rooted); +} + /** * Mirrors the authoritative movement integrator (velocity blends halfway to * the held direction each tick, capped at the fatigue-scaled speed) so the * viewer's own fighter can be shown `ticks` ahead of the delayed snapshot, * walking toward `intent(step)` on each tick. With the snapshot `tick`, a - * punch only holds the fighter for the ticks it has left, so stepping out of - * a punch is predicted as soon as it ends. + * punch or a parry's stagger only holds the fighter for the ticks it has + * left (see `heldSteps`), so stepping out of it is predicted as soon as it + * ends. */ -export function replayMovement(fighter: FighterSnapshot, intent: (step: number) => MovementIntent, ticks: number, tick?: number): PredictedOffset { +export function replayMovement(fighter: FighterSnapshot, intent: (step: number) => MovementIntent, ticks: number, tick?: number, holds: FootworkHolds = {}): PredictedOffset { if (ticks <= 0 || stateLocked(fighter)) return { dx: 0, dy: 0 }; - const committed = tick === undefined ? (fighter.action !== null ? Infinity : 0) : attackTicksRemaining(fighter, tick); + const committed = tick === undefined ? (fighter.action !== null ? Infinity : 0) : heldSteps(fighter, tick, holds); if (committed >= ticks) return { dx: 0, dy: 0 }; let vx = fighter.velocity_x; let vy = fighter.velocity_y; @@ -148,9 +191,9 @@ export function replayMovement(fighter: FighterSnapshot, intent: (step: number) } /** `replayMovement` with the same input held on every tick. */ -export function predictMovement(fighter: FighterSnapshot, held: HeldInput, ticks: number, tick?: number): PredictedOffset { - const intent = movementIntent(fighter, held); - return replayMovement(fighter, () => intent, ticks, tick); +export function predictMovement(fighter: FighterSnapshot, held: HeldInput, ticks: number, tick?: number, holds: FootworkHolds = {}): PredictedOffset { + const intent = { x: 0, y: 0, speed: 0 }; + return replayMovement(fighter, (step) => movementIntent(fighter, held, intent, step), ticks, tick, holds); } /** Held input kept for replay, longer than any lead the renderer works with. */ @@ -173,6 +216,12 @@ export class MovementPrediction { private readonly mean = { x: 0, y: 0, speed: 0 }; private readonly sample = { x: 0, y: 0, speed: 0 }; private horizon: number | null = null; + private readonly holds: { rootedUntil: number; cutActionId: string | null } = { rootedUntil: 0, cutActionId: null }; + + /** Takes note of an event from the engine for the fighter `playerId` (see `parryRootedUntil`). */ + observe(event: CombatEvent, playerId: string | null): void { + this.holds.rootedUntil = parryRootedUntil(event, playerId, this.holds.rootedUntil); + } /** * The offset to draw `fighter`, as shown, by this frame. `held` is the input held now; with @@ -184,11 +233,12 @@ export class MovementPrediction { while (this.history.length > 2 && this.history[1]!.at < nowMs - HELD_HISTORY_MS) this.history.shift(); this.horizon = this.horizon === null ? horizonTicks : this.horizon + (horizonTicks - this.horizon) * (1 - Math.exp(-HORIZON_EASE_RATE * dt)); let target: PredictedOffset = { dx: 0, dy: 0 }; + if (fighter !== null && fighter.stunned_ticks > 0 && fighter.action_id !== null) this.holds.cutActionId = fighter.action_id; if (fighter !== null) { const tickMs = 1000 / tickRate; const lead = this.horizon; // The input applied `step` ticks ahead left with a flush around `lead - 1 - step` ticks ago. - target = replayMovement(fighter, (step) => this.meanIntent(fighter, nowMs - (lead - 1 - step) * tickMs, tickMs), lead, tick); + target = replayMovement(fighter, (step) => this.meanIntent(fighter, step, nowMs - (lead - 1 - step) * tickMs, tickMs), lead, tick, this.holds); } const limit = OFFSET_SPEED_LIMIT * MAX_SPEED * tickRate * dt; const changeX = target.dx - this.offset.dx; @@ -200,8 +250,11 @@ export class MovementPrediction { return { dx: this.offset.dx, dy: this.offset.dy }; } - /** The walking intent averaged over the `width` ms around `centre`, the input held after now being the current one. */ - private meanIntent(fighter: FighterSnapshot, centre: number, width: number): MovementIntent { + /** + * The walking intent `step` ticks ahead, averaged over the `width` ms around `centre`, the input held + * after now being the current one. + */ + private meanIntent(fighter: FighterSnapshot, step: number, centre: number, width: number): MovementIntent { const from = centre - width / 2; const to = centre + width / 2; const mean = this.mean; @@ -212,7 +265,7 @@ export class MovementPrediction { const start = index === 0 ? from : Math.max(from, this.history[index]!.at); const end = Math.min(to, this.history[index + 1]?.at ?? Infinity); if (end <= start) continue; - const intent = movementIntent(fighter, this.history[index]!.held, this.sample); + const intent = movementIntent(fighter, this.history[index]!.held, this.sample, step); const weight = (end - start) / width; mean.x += intent.x * weight; mean.y += intent.y * weight; @@ -267,7 +320,7 @@ export function constrainPrediction(fighter: { readonly x: number; readonly y: n return { dx: inside.x - fighter.x, dy: inside.y - fighter.y }; } -/** Ticks a slip, weave or pull lasts on the server (the engine's EVASION_TICKS). */ +/** Ticks a slip, weave or pull lasts on the server (the engine's EVASION_TICKS), before a style's own. */ export const EVASION_TICKS = 10; /** Stamina the server asks of a slip, weave or pull before it starts one. */ export const EVASION_STAMINA = 25; @@ -302,14 +355,17 @@ export class EvasionPrediction { private id = ""; private pressedAt = 0; private until = 0; + private ticks = EVASION_TICKS; private sequence: number | null = null; private started = false; - press(kind: EvasionKind, id: string, nowMs: number, leadTicks: number, tickRate: number): void { + /** `evasionTicks` is how long the server plays it: EVASION_TICKS plus the fighter's style's own. */ + press(kind: EvasionKind, id: string, nowMs: number, leadTicks: number, tickRate: number, evasionTicks = EVASION_TICKS): void { this.kind = kind; this.id = id; this.pressedAt = nowMs; - this.until = nowMs + ((Math.max(0, leadTicks) + EVASION_TICKS) * 1000) / tickRate; + this.ticks = evasionTicks; + this.until = nowMs + ((Math.max(0, leadTicks) + evasionTicks) * 1000) / tickRate; this.sequence = null; this.started = false; } @@ -332,6 +388,6 @@ export class EvasionPrediction { /** True while the server, evading, keeps the fighter's feet still for inputs sent now. */ holdsFeet(nowMs: number, tickRate: number): boolean { - return this.kind !== null && nowMs < this.pressedAt + (EVASION_TICKS * 1000) / tickRate; + return this.kind !== null && nowMs < this.pressedAt + (this.ticks * 1000) / tickRate; } } diff --git a/web/hands/src/protocol.test.ts b/web/hands/src/protocol.test.ts index 5a8816fe..0db85a57 100644 --- a/web/hands/src/protocol.test.ts +++ b/web/hands/src/protocol.test.ts @@ -1,37 +1,84 @@ -import { decodeBootstrap, decodeServerFrame, decodeToken, encodeInput, ProtocolError } from "./protocol"; +import manifestJson from "../../../src/intelstream/hands/combat-manifest.json"; +import { GET_UP_REQUIRED_MAX, GET_UP_REQUIRED_MIN } from "./manifest"; +import { decodeBootstrap, decodeServerFrame, decodeToken, encodeCpuRequest, encodeInput, encodeStyleChoice, ProtocolError } from "./protocol"; +import { PROTOCOL_VERSION, type CpuLevel, type FighterStyle } from "./types"; import { envelope, publicPlayers, snapshot } from "./test/fixtures"; -describe("strict protocol v3", () => { +describe(`strict protocol v${PROTOCOL_VERSION}`, () => { it("decodes every control envelope and a populated final", () => { const messages = [ - { version: 3, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [publicPlayers[0]], server_tick: 0, next_sequence: 0, reconnect_ticket: "secret-ticket" }, - { version: 3, type: "ticket", reconnect_ticket: "refreshed-ticket", refresh_id: "refresh-identifier" }, - { version: 3, type: "waiting", open_seats: 1 }, { version: 3, type: "ready", players: publicPlayers }, - { version: 3, type: "paused", player_id: "two", grace_ms: 20000 }, { version: 3, type: "resumed", player_id: "two" }, - { version: 3, type: "error", code: "room_full" }, envelope(), - { version: 3, type: "final", match_id: "m", winner_id: "one", method: "decision", round: 12, scorecards: [{ judge: "A", player_one: [10], player_two: [9] }, { judge: "B", player_one: [9], player_two: [10] }, { judge: "C", player_one: [10], player_two: [9] }], ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }, + { version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [publicPlayers[0]], server_tick: 0, next_sequence: 0, reconnect_ticket: "secret-ticket" }, + { version: PROTOCOL_VERSION, type: "ticket", reconnect_ticket: "refreshed-ticket", refresh_id: "refresh-identifier" }, + { version: PROTOCOL_VERSION, type: "waiting", open_seats: 1 }, { version: PROTOCOL_VERSION, type: "ready", players: publicPlayers }, + { version: PROTOCOL_VERSION, type: "paused", player_id: "two", grace_ms: 20000 }, { version: PROTOCOL_VERSION, type: "resumed", player_id: "two" }, + { version: PROTOCOL_VERSION, type: "error", code: "room_full" }, envelope(), + { version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "one", method: "decision", round: 12, scorecards: [{ judge: "A", player_one: [10], player_two: [9] }, { judge: "B", player_one: [9], player_two: [10] }, { judge: "C", player_one: [10], player_two: [9] }], ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }, ]; expect(messages.map((message) => decodeServerFrame(JSON.stringify(message)).type)).toEqual(["welcome", "ticket", "waiting", "ready", "paused", "resumed", "error", "snapshot", "final"]); }); + it("marks the computer opponent and accepts nothing but true or false for it", () => { + const computer = { id: "cpu:champion", name: "Viktor 'Iron' Volkov", avatar: null, rating: 1400, connected: true, cpu: true }; + const ready = { version: PROTOCOL_VERSION, type: "ready", players: [publicPlayers[0], computer] }; + expect(decodeServerFrame(JSON.stringify(ready))).toEqual(ready); + expect(() => decodeServerFrame(JSON.stringify({ ...ready, players: [publicPlayers[0], { ...computer, cpu: "yes" }] }))).toThrow(ProtocolError); + expect(() => decodeServerFrame(JSON.stringify({ ...ready, players: [publicPlayers[0], { ...computer, level: "champion" }] }))).toThrow(ProtocolError); + }); + it("carries each fighter's record and rejects a malformed one", () => { + const veteran = { ...publicPlayers[0], record: { wins: 12, losses: 3, draws: 1, knockouts: 8 } }; + const ready = { version: PROTOCOL_VERSION, type: "ready", players: [veteran, publicPlayers[1]] }; + expect(decodeServerFrame(JSON.stringify(ready))).toEqual(ready); + for (const record of [{ wins: -1, losses: 0, draws: 0, knockouts: 0 }, { wins: 1, losses: 0, draws: 0 }, { wins: 1, losses: 0, draws: 0, knockouts: 0, titles: 2 }, { wins: "1", losses: 0, draws: 0, knockouts: 0 }, null]) { + expect(() => decodeServerFrame(JSON.stringify({ ...ready, players: [{ ...veteran, record }, publicPlayers[1]] }))).toThrow(ProtocolError); + } + }); + it("asks for a computer opponent by level only", () => { + expect(JSON.parse(encodeCpuRequest("contender"))).toEqual({ version: PROTOCOL_VERSION, type: "cpu", level: "contender" }); + expect(() => encodeCpuRequest("legend" as CpuLevel)).toThrow(ProtocolError); + }); it("decodes spectators without granting fighter fields", () => { - const spectator = { version: 3, type: "welcome", role: "spectator", player_id: "viewer", players: publicPlayers, server_tick: 40, reconnect_ticket: "spectator-ticket" }; + const spectator = { version: PROTOCOL_VERSION, type: "welcome", role: "spectator", player_id: "viewer", players: publicPlayers, server_tick: 40, reconnect_ticket: "spectator-ticket" }; expect(decodeServerFrame(JSON.stringify(spectator))).toEqual(spectator); expect(() => decodeServerFrame(JSON.stringify({ ...spectator, seat: 1 }))).toThrow(ProtocolError); expect(() => decodeServerFrame(JSON.stringify({ ...spectator, player_id: "one" }))).toThrow(/spectator/u); - const fighterWithoutRole = { version: 3, type: "welcome", player_id: "one", seat: 1, rating: 1500, players: [publicPlayers[0]], server_tick: 0, next_sequence: 0 }; + const fighterWithoutRole = { version: PROTOCOL_VERSION, type: "welcome", player_id: "one", seat: 1, rating: 1500, players: [publicPlayers[0]], server_tick: 0, next_sequence: 0 }; expect(() => decodeServerFrame(JSON.stringify(fighterWithoutRole))).toThrow(ProtocolError); }); + it("decodes the engine's punch counts in the final, for both fighters and nobody else", () => { + const final = { version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "one", method: "decision", round: 3, scorecards: ["A", "B", "C"].map((judge) => ({ judge, player_one: [10], player_two: [9] })), ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }; + const punches = { one: { thrown: 40, landed: 12, jabs_thrown: 20, jabs_landed: 5 }, two: { thrown: 31, landed: 9, jabs_thrown: 11, jabs_landed: 4 } }; + expect(decodeServerFrame(JSON.stringify({ ...final, punches }))).toEqual({ ...final, punches }); + expect(decodeServerFrame(JSON.stringify(final))).toEqual(final); + for (const bad of [ + { one: punches.one }, + { one: punches.one, three: punches.two }, + { ...punches, two: { ...punches.two, landed: 32 } }, + { ...punches, two: { ...punches.two, jabs_thrown: 32 } }, + { ...punches, two: { ...punches.two, jabs_landed: 10 } }, + { ...punches, two: { ...punches.two, clean: 1 } }, + { ...punches, two: { ...punches.two, thrown: -1 } }, + ]) expect(() => decodeServerFrame(JSON.stringify({ ...final, punches: bad }))).toThrow(ProtocolError); + }); + it("lets a spectator arrive before the corners are filled, and a waiting room show both seats open", () => { + const early = { version: PROTOCOL_VERSION, type: "welcome", role: "spectator", player_id: "viewer", players: [], server_tick: 0, reconnect_ticket: "spectator-ticket" }; + expect(decodeServerFrame(JSON.stringify(early))).toEqual(early); + expect(decodeServerFrame(JSON.stringify({ ...early, players: [publicPlayers[0]] }))).toEqual({ ...early, players: [publicPlayers[0]] }); + expect(() => decodeServerFrame(JSON.stringify({ ...early, players: [publicPlayers[0], publicPlayers[0]] }))).toThrow(/distinct/u); + expect(decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "waiting", open_seats: 2 }))).toEqual({ version: PROTOCOL_VERSION, type: "waiting", open_seats: 2 }); + for (const open of [0, 3, "1"]) expect(() => decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "waiting", open_seats: open }))).toThrow(/open seats/u); + // A fighter's welcome still names him among the players. + expect(() => decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [], server_tick: 0, next_sequence: 0 }))).toThrow(ProtocolError); + }); it("decodes a fully populated embedded result", () => { const value = snapshot(); value.events as unknown as unknown[]; const populated = { ...value, result: { match_id: "m", activity_instance_id: "i", guild_id: "g", player_one_id: "one", player_two_id: "two", winner_id: null, finish_method: "draw", round_number: 15, tick: 10, scorecards: [{ judge: "A", player_one: [], player_two: [] }, { judge: "B", player_one: [], player_two: [] }, { judge: "C", player_one: [], player_two: [] }], player_one_knockdowns: 1, player_two_knockdowns: 1, player_one_damage: 200, player_two_damage: 210 } }; - expect(decodeServerFrame(JSON.stringify({ version: 3, type: "snapshot", payload: populated })).type).toBe("snapshot"); + expect(decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload: populated })).type).toBe("snapshot"); }); it.each([ - ["missing", { version: 3, type: "waiting" }], ["extra", { version: 3, type: "waiting", open_seats: 1, winner_id: "forged" }], - ["version", { version: 4, type: "waiting", open_seats: 1 }], ["nonfinite", "{\"version\":1,\"type\":\"paused\",\"player_id\":\"one\",\"grace_ms\":NaN}"], + ["missing", { version: PROTOCOL_VERSION, type: "waiting" }], ["extra", { version: PROTOCOL_VERSION, type: "waiting", open_seats: 1, winner_id: "forged" }], + ["version", { version: PROTOCOL_VERSION + 1, type: "waiting", open_seats: 1 }], ["nonfinite", "{\"version\":1,\"type\":\"paused\",\"player_id\":\"one\",\"grace_ms\":NaN}"], ])("rejects %s fields", (_name, value) => expect(() => decodeServerFrame(typeof value === "string" ? value : JSON.stringify(value))).toThrow(ProtocolError)); it("rejects duplicate, oversized, and same-player snapshots", () => { - expect(() => decodeServerFrame('{"version":3,"type":"waiting","type":"ready","open_seats":1}')).toThrow(/duplicate/u); + expect(() => decodeServerFrame(`{"version":${PROTOCOL_VERSION},"type":"waiting","type":"ready","open_seats":1}`)).toThrow(/duplicate/u); expect(() => decodeServerFrame(" ".repeat(65_537))).toThrow(/large/u); const bad = structuredClone(envelope()) as { payload: { fighters: { player_id: string }[] } }; bad.payload.fighters[1]!.player_id = "one"; expect(() => decodeServerFrame(JSON.stringify(bad))).toThrow(/distinct/u); @@ -39,56 +86,61 @@ describe("strict protocol v3", () => { it("encodes only authorized fields, clamps movement, and caps actions", () => { const forged = { kind: "punch", hand: "left", class: "jab", target: "head", power: "normal", winner_id: "one" } as const; const encoded = JSON.parse(encodeInput(3, 9, { moveX: Infinity, moveY: -9000, defense: "guard_high", actions: [forged, forged, forged, forged, forged] })) as Record; - expect(encoded).toMatchObject({ version: 3, type: "input", sequence: 3, client_tick: 9, move: { x: 0, y: -1000 } }); expect(encoded).not.toHaveProperty("winner_id"); expect((encoded.actions as unknown[])).toHaveLength(4); expect((encoded.actions as Record[])[0]).not.toHaveProperty("winner_id"); + expect(encoded).toMatchObject({ version: PROTOCOL_VERSION, type: "input", sequence: 3, client_tick: 9, move: { x: 0, y: -1000 } }); expect(encoded).not.toHaveProperty("winner_id"); expect((encoded.actions as unknown[])).toHaveLength(4); expect((encoded.actions as Record[])[0]).not.toHaveProperty("winner_id"); }); it("accepts authoritative rounds 13–15 with exactly three complete judge cards", () => { const rounds = Array.from({ length: 15 }, () => 10); const cards = ["A", "B", "C"].map((judge) => ({ judge, player_one: rounds, player_two: rounds.map(() => 9) })); - const final = { version: 3, type: "final", match_id: "m", winner_id: "one", method: "decision", round: 15, scorecards: cards, ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }; + const final = { version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "one", method: "decision", round: 15, scorecards: cards, ratings: { one: { before: 1500, after: 1516 }, two: { before: 1500, after: 1484 } } }; expect(decodeServerFrame(JSON.stringify(final))).toMatchObject({ type: "final", round: 15 }); const atThirteen = { ...snapshot(), round_number: 13 }; - expect(decodeServerFrame(JSON.stringify({ version: 3, type: "snapshot", payload: atThirteen }))).toMatchObject({ type: "snapshot", payload: { round_number: 13 } }); + expect(decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload: atThirteen }))).toMatchObject({ type: "snapshot", payload: { round_number: 13 } }); }); it.each([ { winner_id: null, method: "decision" }, { winner_id: "one", method: "draw" }, ])("rejects incoherent final winner/method combinations", ({ winner_id, method }) => { const cards = ["A", "B", "C"].map((judge) => ({ judge, player_one: [], player_two: [] })); - expect(() => decodeServerFrame(JSON.stringify({ version: 3, type: "final", match_id: "m", winner_id, method, round: 1, scorecards: cards, ratings: { one: { before: 1, after: 1 }, two: { before: 1, after: 1 } } }))).toThrow(/coherent/u); + expect(() => decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id, method, round: 1, scorecards: cards, ratings: { one: { before: 1, after: 1 }, two: { before: 1, after: 1 } } }))).toThrow(/coherent/u); + }); + it("allows the most get-up presses the engine can ask for: three knockdowns on a head beaten to the cap", () => { + expect(GET_UP_REQUIRED_MIN).toBe(manifestJson.knockdown.get_up_base); + expect(GET_UP_REQUIRED_MAX).toBe(manifestJson.knockdown.get_up_base + 3 * manifestJson.knockdown.get_up_per_knockdown + Math.floor(1400 / manifestJson.knockdown.get_up_trauma_divisor)); + expect(GET_UP_REQUIRED_MAX).toBeGreaterThanOrEqual(171); }); it("accepts exact fighter-domain boundaries and the private get-up sentinel", () => { const base = snapshot(); const lower = { ...base.fighters[0], x: -462, y: -462, facing: -1, velocity_x: -7, velocity_y: -7, maximum_stamina: 330, stamina: 0, poise: 0, get_up_required: 45 }; - const upper = { ...base.fighters[1], x: 462, y: 462, facing: 1, velocity_x: 7, velocity_y: 7, maximum_stamina: 1000, stamina: 1000, poise: 600, get_up_required: 169 }; - expect(decodeServerFrame(JSON.stringify({ version: 3, type: "snapshot", payload: { ...base, fighters: [lower, upper] } }))).toMatchObject({ type: "snapshot", payload: { fighters: [{ get_up_required: 45 }, { get_up_required: 169 }] } }); + const upper = { ...base.fighters[1], x: 462, y: 462, facing: 1, velocity_x: 7, velocity_y: 7, maximum_stamina: 1000, stamina: 1000, poise: 600, get_up_required: GET_UP_REQUIRED_MAX }; + expect(decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...base, fighters: [lower, upper] } }))).toMatchObject({ type: "snapshot", payload: { fighters: [{ get_up_required: 45 }, { get_up_required: GET_UP_REQUIRED_MAX }] } }); const redactedOpponent = { ...upper, get_up_required: 0 }; - expect(decodeServerFrame(JSON.stringify({ version: 3, type: "snapshot", payload: { ...base, fighters: [lower, redactedOpponent] } }))).toMatchObject({ payload: { fighters: [{ get_up_required: 45 }, { get_up_required: 0 }] } }); + expect(decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...base, fighters: [lower, redactedOpponent] } }))).toMatchObject({ payload: { fighters: [{ get_up_required: 45 }, { get_up_required: 0 }] } }); }); it.each([ ["x below", "x", -463], ["x above", "x", 463], ["y below", "y", -463], ["y above", "y", 463], - ["zero facing", "facing", 0], ["velocity x below", "velocity_x", -8], ["velocity x above", "velocity_x", 8], - ["velocity y below", "velocity_y", -8], ["velocity y above", "velocity_y", 8], ["maximum stamina below", "maximum_stamina", 329], + ["zero facing", "facing", 0], ["velocity x fraction", "velocity_x", 6.5], + ["velocity y fraction", "velocity_y", -6.5], ["maximum stamina below", "maximum_stamina", 329], ["maximum stamina above", "maximum_stamina", 1001], ["poise below", "poise", -1], ["poise above", "poise", 601], - ["get-up gap start", "get_up_required", 1], ["get-up gap end", "get_up_required", 33], ["get-up above", "get_up_required", 170], + ["get-up gap start", "get_up_required", 1], ["get-up gap end", "get_up_required", GET_UP_REQUIRED_MIN - 1], ["get-up above", "get_up_required", GET_UP_REQUIRED_MAX + 1], ])("rejects %s fighter output", (_name, field, value) => { const base = snapshot(); const changed = { ...base.fighters[0], [field]: value }; - expect(() => decodeServerFrame(JSON.stringify({ version: 3, type: "snapshot", payload: { ...base, fighters: [changed, base.fighters[1]] } }))).toThrow(ProtocolError); + expect(() => decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...base, fighters: [changed, base.fighters[1]] } }))).toThrow(ProtocolError); }); it("rejects stamina above the decoded maximum", () => { const base = snapshot(); const exhausted = { ...base.fighters[0], maximum_stamina: 330, stamina: 331 }; - expect(() => decodeServerFrame(JSON.stringify({ version: 3, type: "snapshot", payload: { ...base, fighters: [exhausted, base.fighters[1]] } }))).toThrow(ProtocolError); + expect(() => decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...base, fighters: [exhausted, base.fighters[1]] } }))).toThrow(ProtocolError); }); it("rejects out-of-authority event and fighter bounds", () => { const base = snapshot(); - const badEvent = { version: 3, type: "snapshot", payload: { ...base, events: [{ event_id: 1, tick: 1, kind: "x".repeat(33), actor_id: null, target_id: null, amount: 10_001, detail: "", blood: 101, direction: 2 }] } }; + const badEvent = { version: PROTOCOL_VERSION, type: "snapshot", payload: { ...base, events: [{ event_id: 1, tick: 1, kind: "x".repeat(33), actor_id: null, target_id: null, amount: 10_001, detail: "", blood: 101, direction: 2 }] } }; expect(() => decodeServerFrame(JSON.stringify(badEvent))).toThrow(); - const badQueue = { version: 3, type: "snapshot", payload: { ...base, fighters: [{ ...base.fighters[0], queued_actions: 2 }, base.fighters[1]] } }; + const badQueue = { version: PROTOCOL_VERSION, type: "snapshot", payload: { ...base, fighters: [{ ...base.fighters[0], queued_actions: 2 }, base.fighters[1]] } }; expect(() => decodeServerFrame(JSON.stringify(badQueue))).toThrow(); }); it("strictly decodes same-origin API payload schemas", () => { - expect(decodeBootstrap({ client_id: "123", state: "s", protocol: 3, simulation: { tick_rate: 30, ring_half_width: 6000, ring_half_height: 4000 } }).simulation.tick_rate).toBe(30); + expect(decodeBootstrap({ client_id: "123", state: "s", protocol: PROTOCOL_VERSION, simulation: { tick_rate: 30, ring_half_width: 6000, ring_half_height: 4000 } }).simulation.tick_rate).toBe(30); expect(decodeToken({ access_token: "a", ticket: "t", player: { id: "one", name: "One", avatar: null, rating: 1500 } }).player.id).toBe("one"); expect(() => decodeToken({ access_token: "a", ticket: "t", outcome: "forged", player: { id: "one", name: "One", avatar: null, rating: 1500 } })).toThrow(); }); @@ -98,12 +150,66 @@ describe("facing vector and input acknowledgement", () => { it("rejects a zero facing vector and negative acknowledgements below the sentinel", () => { const base = snapshot(); const zero = { ...base.fighters[0], facing_x: 0, facing_y: 0 }; - expect(() => decodeServerFrame(JSON.stringify({ version: 3, type: "snapshot", payload: { ...base, fighters: [zero, base.fighters[1]] } }))).toThrow(/facing vector/u); + expect(() => decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...base, fighters: [zero, base.fighters[1]] } }))).toThrow(/facing vector/u); const oversized = { ...base.fighters[0], facing_x: 1001 }; - expect(() => decodeServerFrame(JSON.stringify({ version: 3, type: "snapshot", payload: { ...base, fighters: [oversized, base.fighters[1]] } }))).toThrow(ProtocolError); + expect(() => decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...base, fighters: [oversized, base.fighters[1]] } }))).toThrow(ProtocolError); const unacknowledged = { ...base.fighters[0], last_input_sequence: -2 }; - expect(() => decodeServerFrame(JSON.stringify({ version: 3, type: "snapshot", payload: { ...base, fighters: [unacknowledged, base.fighters[1]] } }))).toThrow(ProtocolError); + expect(() => decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...base, fighters: [unacknowledged, base.fighters[1]] } }))).toThrow(ProtocolError); const turned = { ...base.fighters[0], facing_x: 0, facing_y: -1000, last_input_sequence: 41 }; - expect(decodeServerFrame(JSON.stringify({ version: 3, type: "snapshot", payload: { ...base, fighters: [turned, base.fighters[1]] } }))).toMatchObject({ payload: { fighters: [{ facing_y: -1000, last_input_sequence: 41 }, { facing_x: -1000 }] } }); + expect(decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...base, fighters: [turned, base.fighters[1]] } }))).toMatchObject({ payload: { fighters: [{ facing_y: -1000, last_input_sequence: 41 }, { facing_x: -1000 }] } }); + }); +}); + +describe("corner instructions", () => { + it("decodes the corner's choice for each fighter and rejects anything else", () => { + const base = snapshot(); + const treated = { ...base, phase: "rest", fighters: [{ ...base.fighters[0], corner_choice: "cut" }, { ...base.fighters[1], corner_choice: "balanced" }] }; + expect(decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload: treated }))).toMatchObject({ payload: { fighters: [{ corner_choice: "cut" }, { corner_choice: "balanced" }] } }); + const unknown = { ...base, fighters: [{ ...base.fighters[0], corner_choice: "towel" }, base.fighters[1]] }; + expect(() => decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload: unknown }))).toThrow(/corner choice/u); + const { corner_choice: _dropped, ...withoutChoice } = base.fighters[1]; + const missing = { ...base, fighters: [base.fighters[0], withoutChoice] }; + expect(() => decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload: missing }))).toThrow(ProtocolError); + }); + it("sends a corner instruction as an ordinary action", () => { + const encoded = JSON.parse(encodeInput(7, 40, { moveX: 0, moveY: 0, defense: "none", actions: [{ kind: "corner_swelling" }] })) as { actions: unknown[] }; + expect(encoded.actions).toEqual([{ kind: "corner_swelling" }]); + }); +}); + +describe("fighter styles", () => { + const select = { version: PROTOCOL_VERSION, type: "select", deadline_ms: 9_000, players: [{ ...publicPlayers[0], style: "slugger" }, publicPlayers[1]], ready: ["one"] }; + it("decodes the pick of styles with a style only for the fighter who has settled", () => { + expect(decodeServerFrame(JSON.stringify(select))).toEqual(select); + expect(decodeServerFrame(JSON.stringify({ ...select, players: [...publicPlayers], ready: [] }))).toMatchObject({ type: "select", ready: [] }); + }); + it.each([ + ["a separate styles map", { ...select, styles: { one: "slugger" } }], + ["one fighter", { ...select, players: [publicPlayers[0]] }], + ["an unknown style", { ...select, players: [{ ...publicPlayers[0], style: "brawler" }, publicPlayers[1]] }], + ["a stranger settled", { ...select, ready: ["three"] }], + ["a fighter settled twice", { ...select, ready: ["one", "one"] }], + ["a deadline past a minute", { ...select, deadline_ms: 60_001 }], + ["a negative deadline", { ...select, deadline_ms: -1 }], + ["no deadline", { version: PROTOCOL_VERSION, type: "select", players: select.players, ready: [] }], + ])("rejects a pick with %s", (_name, value) => expect(() => decodeServerFrame(JSON.stringify(value))).toThrow(ProtocolError)); + it("carries each fighter's style in snapshots and in the players of a bout", () => { + const base = snapshot(); + for (const style of ["balanced", "boxer", "slugger", "swarmer", "counter_puncher"] as const) { + const styled = { ...base, fighters: [{ ...base.fighters[0], style }, base.fighters[1]] }; + expect(decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload: styled }))).toMatchObject({ payload: { fighters: [{ style }, { style: "balanced" }] } }); + } + const unknown = { ...base, fighters: [{ ...base.fighters[0], style: "brawler" }, base.fighters[1]] }; + expect(() => decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload: unknown }))).toThrow(/style/u); + const { style: _dropped, ...unstyled } = base.fighters[1]; + expect(() => decodeServerFrame(JSON.stringify({ version: PROTOCOL_VERSION, type: "snapshot", payload: { ...base, fighters: [base.fighters[0], unstyled] } }))).toThrow(ProtocolError); + const ready = { version: PROTOCOL_VERSION, type: "ready", players: [{ ...publicPlayers[0], style: "swarmer" }, { ...publicPlayers[1], style: "counter_puncher" }] }; + expect(decodeServerFrame(JSON.stringify(ready))).toEqual(ready); + expect(() => decodeServerFrame(JSON.stringify({ ...ready, players: [{ ...publicPlayers[0], style: 3 }, publicPlayers[1]] }))).toThrow(ProtocolError); + }); + it("sends a style with whether the fighter has settled on it, and nothing else", () => { + expect(JSON.parse(encodeStyleChoice("swarmer", true))).toEqual({ version: PROTOCOL_VERSION, type: "style", style: "swarmer", ready: true }); + expect(JSON.parse(encodeStyleChoice("boxer", false))).toEqual({ version: PROTOCOL_VERSION, type: "style", style: "boxer", ready: false }); + expect(() => encodeStyleChoice("brawler" as FighterStyle, true)).toThrow(ProtocolError); }); }); diff --git a/web/hands/src/protocol.ts b/web/hands/src/protocol.ts index a1f73c09..bdc08974 100644 --- a/web/hands/src/protocol.ts +++ b/web/hands/src/protocol.ts @@ -1,11 +1,12 @@ import { MAX_SERVER_FRAME_BYTES, PROTOCOL_VERSION, - type BootstrapResponse, type CombatEvent, type DefensivePose, type EngineSnapshot, - type FighterSnapshot, type FinalMessage, type FinishMethod, type Foul, type Hand, + type BootstrapResponse, type CombatEvent, type CornerChoice, type CpuLevel, type DefensivePose, type EngineSnapshot, + type FighterRecord, type FighterSnapshot, type FighterStyle, type FinalMessage, type FinishMethod, type Foul, type Hand, type HeldDefense, type JudgeCard, type MatchPhase, type MatchResult, type MovementKind, - type Power, type PublicPlayer, type PunchClass, type SemanticAction, type ServerMessage, + type Power, type PublicPlayer, type PunchClass, type PunchTotals, type SemanticAction, type ServerMessage, type Stance, type Target, type TokenResponse, type TraumaSnapshot, } from "./types"; +import { GET_UP_REQUIRED_MAX, GET_UP_REQUIRED_MIN } from "./manifest"; export class ProtocolError extends Error { override name = "ProtocolError" } const MAX_INT = 2_147_483_647; @@ -85,20 +86,30 @@ function array(value: unknown, name: string, max: number): unknown[] { if (!Arra const hands = ["left", "right"] as const; const classes = ["jab", "straight", "hook", "uppercut"] as const; const targets = ["head", "body"] as const; const powers = ["normal", "power"] as const; const stances = ["orthodox", "southpaw"] as const; +export const FIGHTER_STYLES = ["balanced", "boxer", "slugger", "swarmer", "counter_puncher"] as const; const defenses = ["none", "guard_high", "guard_low", "slip_left", "slip_right", "weave", "pull"] as const; const heldDefenses = ["none", "guard_high", "guard_low"] as const; -const movementKinds = ["slip_left", "slip_right", "weave", "pull", "clinch", "switch_stance", "get_up_left", "get_up_right", "taunt"] as const; +const movementKinds = ["slip_left", "slip_right", "weave", "pull", "clinch", "switch_stance", "get_up_left", "get_up_right", "taunt", "corner_cut", "corner_swelling", "corner_breath"] as const; +const cornerChoices = ["cut", "swelling", "breath", "balanced"] as const; const fouls = ["low_blow", "headbutt"] as const; const phases = ["countdown", "fight", "knockdown", "foul_recovery", "rest", "complete"] as const; const methods = ["ko", "flash_ko", "tko", "doctor_stoppage", "disqualification", "decision", "draw", "forfeit"] as const; +function fighterRecord(value: unknown): FighterRecord { + const o = object(value, "record"); exact(o, ["wins", "losses", "draws", "knockouts"]); + return { wins: integer(o.wins, "record.wins"), losses: integer(o.losses, "record.losses"), draws: integer(o.draws, "record.draws"), knockouts: integer(o.knockouts, "record.knockouts") }; +} + function publicPlayer(value: unknown): PublicPlayer { - const o = object(value, "player"); exact(o, ["id", "name", "avatar", "rating", "connected"]); - return { id: string(o.id, "player.id"), name: string(o.name, "player.name", 80), avatar: nullableString(o.avatar, "player.avatar", 128), rating: integer(o.rating, "rating"), connected: bool(o.connected, "connected") }; + const o = object(value, "player"); exact(o, ["id", "name", "avatar", "rating", "connected"], ["cpu", "record", "style"]); + const style = o.style === undefined ? {} : { style: oneOf(o.style, FIGHTER_STYLES, "player style") }; + const cpu = o.cpu === undefined ? {} : { cpu: bool(o.cpu, "cpu") }; + const record = o.record === undefined ? {} : { record: fighterRecord(o.record) }; + return { id: string(o.id, "player.id"), name: string(o.name, "player.name", 80), avatar: nullableString(o.avatar, "player.avatar", 128), rating: integer(o.rating, "rating"), connected: bool(o.connected, "connected"), ...cpu, ...record, ...style }; } -function players(value: unknown, exactLength?: number): PublicPlayer[] { +function players(value: unknown, exactLength?: number, minLength = 1): PublicPlayer[] { const result = array(value, "players", 2).map(publicPlayer); - if (result.length < 1 || (exactLength !== undefined && result.length !== exactLength) || new Set(result.map((p) => p.id)).size !== result.length) throw new ProtocolError("players must be present and distinct"); + if (result.length < minLength || (exactLength !== undefined && result.length !== exactLength) || new Set(result.map((p) => p.id)).size !== result.length) throw new ProtocolError("players must be present and distinct"); return result; } function trauma(value: unknown): TraumaSnapshot { @@ -111,16 +122,22 @@ function trauma(value: unknown): TraumaSnapshot { swelling: integer(o.swelling, "swelling", 0, 1000), bleeding: integer(o.bleeding, "bleeding", 0, 1000), }; } +/** The engine reports a fighter's velocity within his walking speed. */ +const MAX_REPORTED_SPEED = 7; +/** The velocity only seeds the movement prediction, so one past the walking speed is clamped rather than ending the bout for everyone in the room. */ +function velocity(value: unknown, name: string): number { + return Math.max(-MAX_REPORTED_SPEED, Math.min(MAX_REPORTED_SPEED, integer(value, name, -MAX_INT, MAX_INT))); +} function fighter(value: unknown): FighterSnapshot { const o = object(value, "fighter"); exact(o, [ - "player_id", "x", "y", "facing", "facing_x", "facing_y", "velocity_x", "velocity_y", "stance", "defense", + "player_id", "x", "y", "facing", "facing_x", "facing_y", "velocity_x", "velocity_y", "stance", "style", "defense", "stamina", "maximum_stamina", "conditioning", "guard", "poise", "trauma", "knockdowns", "warnings", "deductions", "stunned_ticks", "is_downed", "action", "action_hand", "action_target", "action_power", "action_id", "action_key", "action_start_tick", "action_startup_ticks", "action_active_ticks", "action_recovery_ticks", "action_contact_tick", "queued_actions", "clinch_startup_ticks", "clinch_ticks", - "is_foul_recovery_target", "taunt_ticks", "get_up_prompt", "get_up_meter", "get_up_required", "get_up_count", + "is_foul_recovery_target", "taunt_ticks", "corner_choice", "get_up_prompt", "get_up_meter", "get_up_required", "get_up_count", "get_up_window_start_tick", "get_up_window_end_tick", "last_input_sequence", ]); const action = o.action === null ? null : oneOf(o.action, classes, "action"); @@ -135,17 +152,17 @@ function fighter(value: unknown): FighterSnapshot { const prompt = o.get_up_prompt === null ? null : oneOf(o.get_up_prompt, ["get_up_left", "get_up_right"] as const, "get-up prompt"); const maximumStamina = integer(o.maximum_stamina, "maximum_stamina", 330, 1000); const stamina = integer(o.stamina, "stamina", 0, maximumStamina); - const getUpRequired = integer(o.get_up_required, "get_up_required", 0, 169); + const getUpRequired = integer(o.get_up_required, "get_up_required", 0, GET_UP_REQUIRED_MAX); const facing = integer(o.facing, "facing", -1, 1); if (facing === 0) throw new ProtocolError("invalid facing"); const facingX = integer(o.facing_x, "facing_x", -1000, 1000); const facingY = integer(o.facing_y, "facing_y", -1000, 1000); if (facingX === 0 && facingY === 0) throw new ProtocolError("invalid facing vector"); - if (getUpRequired !== 0 && getUpRequired < 34) throw new ProtocolError("invalid get-up requirement"); + if (getUpRequired !== 0 && getUpRequired < GET_UP_REQUIRED_MIN) throw new ProtocolError("invalid get-up requirement"); return { player_id: string(o.player_id, "player_id"), x: integer(o.x, "x", -462, 462), y: integer(o.y, "y", -462, 462), - facing, facing_x: facingX, facing_y: facingY, velocity_x: integer(o.velocity_x, "velocity_x", -7, 7), velocity_y: integer(o.velocity_y, "velocity_y", -7, 7), - stance: oneOf(o.stance, stances, "stance"), defense: oneOf(o.defense, defenses, "defense"), + facing, facing_x: facingX, facing_y: facingY, velocity_x: velocity(o.velocity_x, "velocity_x"), velocity_y: velocity(o.velocity_y, "velocity_y"), + stance: oneOf(o.stance, stances, "stance"), style: oneOf(o.style, FIGHTER_STYLES, "style"), defense: oneOf(o.defense, defenses, "defense"), stamina, maximum_stamina: maximumStamina, conditioning: integer(o.conditioning, "conditioning", 0, 1000), guard: integer(o.guard, "guard", 0, 700), poise: integer(o.poise, "poise", 0, 600), trauma: trauma(o.trauma), knockdowns: integer(o.knockdowns, "knockdowns", 0, 3), warnings: integer(o.warnings, "warnings", 0, 3), deductions: integer(o.deductions, "deductions", 0, 1), @@ -159,7 +176,8 @@ function fighter(value: unknown): FighterSnapshot { action_recovery_ticks: integer(o.action_recovery_ticks, "action_recovery_ticks", 0, 40), action_contact_tick: o.action_contact_tick === null ? null : integer(o.action_contact_tick, "action_contact_tick", 0), queued_actions: integer(o.queued_actions, "queued_actions", 0, 1), clinch_startup_ticks: integer(o.clinch_startup_ticks, "clinch_startup_ticks", 0, 8), clinch_ticks: integer(o.clinch_ticks, "clinch_ticks", 0, 45), - is_foul_recovery_target: bool(o.is_foul_recovery_target, "is_foul_recovery_target"), taunt_ticks: integer(o.taunt_ticks, "taunt_ticks", 0, 60), get_up_prompt: prompt, + is_foul_recovery_target: bool(o.is_foul_recovery_target, "is_foul_recovery_target"), taunt_ticks: integer(o.taunt_ticks, "taunt_ticks", 0, 60), + corner_choice: o.corner_choice === null ? null : oneOf(o.corner_choice, cornerChoices, "corner choice"), get_up_prompt: prompt, get_up_meter: integer(o.get_up_meter, "get_up_meter", 0, 256), get_up_required: getUpRequired, get_up_count: integer(o.get_up_count, "get_up_count", 0, 10), get_up_window_start_tick: integer(o.get_up_window_start_tick, "get_up_window_start_tick"), get_up_window_end_tick: integer(o.get_up_window_end_tick, "get_up_window_end_tick"), last_input_sequence: integer(o.last_input_sequence, "last_input_sequence", -1), @@ -209,31 +227,51 @@ export function decodeServerFrame(frame: string | ArrayBuffer | Uint8Array): Ser exact(o, ["version", "type", "role", "player_id", "seat", "rating", "players", "server_tick", "next_sequence"], ["reconnect_ticket"]); const decodedPlayers = players(o.players); if (!decodedPlayers.some((player) => player.id === playerId)) throw new ProtocolError("welcome fighter is absent"); - return { version: 3, type, role, player_id: playerId, seat: integer(o.seat, "seat", 1, 2) as 1 | 2, rating: integer(o.rating, "rating"), players: decodedPlayers, server_tick: integer(o.server_tick, "server_tick"), next_sequence: integer(o.next_sequence, "next_sequence"), ...reconnect }; + return { version: PROTOCOL_VERSION, type, role, player_id: playerId, seat: integer(o.seat, "seat", 1, 2) as 1 | 2, rating: integer(o.rating, "rating"), players: decodedPlayers, server_tick: integer(o.server_tick, "server_tick"), next_sequence: integer(o.next_sequence, "next_sequence"), ...reconnect }; } exact(o, ["version", "type", "role", "player_id", "players", "server_tick"], ["reconnect_ticket"]); - const decodedPlayers = players(o.players, 2); + // Before the bell a spectator can arrive while the corners are still filling. + const decodedPlayers = players(o.players, undefined, 0); if (decodedPlayers.some((player) => player.id === playerId)) throw new ProtocolError("spectator cannot be a fighter"); - return { version: 3, type, role, player_id: playerId, players: [decodedPlayers[0]!, decodedPlayers[1]!], server_tick: integer(o.server_tick, "server_tick"), ...reconnect }; + return { version: PROTOCOL_VERSION, type, role, player_id: playerId, players: decodedPlayers, server_tick: integer(o.server_tick, "server_tick"), ...reconnect }; } - if (type === "ticket") { exact(o, ["version", "type", "reconnect_ticket", "refresh_id"]); return { version: 3, type, reconnect_ticket: string(o.reconnect_ticket, "reconnect_ticket", 4096), refresh_id: string(o.refresh_id, "refresh_id", 128, 16) }; } - if (type === "waiting") { exact(o, ["version", "type", "open_seats"]); if (o.open_seats !== 1) throw new ProtocolError("invalid open seats"); return { version: 3, type, open_seats: 1 }; } - if (type === "ready") { exact(o, ["version", "type", "players"]); const ps = players(o.players, 2); return { version: 3, type, players: [ps[0]!, ps[1]!] }; } - if (type === "paused") { exact(o, ["version", "type", "player_id", "grace_ms"]); return { version: 3, type, player_id: string(o.player_id, "player_id"), grace_ms: integer(o.grace_ms, "grace_ms", 0, 60_000) }; } - if (type === "resumed") { exact(o, ["version", "type", "player_id"]); return { version: 3, type, player_id: string(o.player_id, "player_id") }; } - if (type === "snapshot") { exact(o, ["version", "type", "payload"]); return { version: 3, type, payload: snapshot(o.payload) }; } - if (type === "error") { exact(o, ["version", "type", "code"]); return { version: 3, type, code: string(o.code, "error code", 80) }; } + if (type === "ticket") { exact(o, ["version", "type", "reconnect_ticket", "refresh_id"]); return { version: PROTOCOL_VERSION, type, reconnect_ticket: string(o.reconnect_ticket, "reconnect_ticket", 4096), refresh_id: string(o.refresh_id, "refresh_id", 128, 16) }; } + if (type === "waiting") { exact(o, ["version", "type", "open_seats"]); if (o.open_seats !== 1 && o.open_seats !== 2) throw new ProtocolError("invalid open seats"); return { version: PROTOCOL_VERSION, type, open_seats: o.open_seats }; } + if (type === "select") { + exact(o, ["version", "type", "deadline_ms", "players", "ready"]); + const ps = players(o.players, 2); + const ready = array(o.ready, "ready", 2).map((id) => string(id, "ready player id")); + if (new Set(ready).size !== ready.length || ready.some((id) => !ps.some((player) => player.id === id))) throw new ProtocolError("invalid ready fighters"); + return { version: PROTOCOL_VERSION, type, deadline_ms: integer(o.deadline_ms, "deadline_ms", 0, 60_000), players: [ps[0]!, ps[1]!], ready }; + } + if (type === "ready") { exact(o, ["version", "type", "players"]); const ps = players(o.players, 2); return { version: PROTOCOL_VERSION, type, players: [ps[0]!, ps[1]!] }; } + if (type === "paused") { exact(o, ["version", "type", "player_id", "grace_ms"]); return { version: PROTOCOL_VERSION, type, player_id: string(o.player_id, "player_id"), grace_ms: integer(o.grace_ms, "grace_ms", 0, 60_000) }; } + if (type === "resumed") { exact(o, ["version", "type", "player_id"]); return { version: PROTOCOL_VERSION, type, player_id: string(o.player_id, "player_id") }; } + if (type === "snapshot") { exact(o, ["version", "type", "payload"]); return { version: PROTOCOL_VERSION, type, payload: snapshot(o.payload) }; } + if (type === "error") { exact(o, ["version", "type", "code"]); return { version: PROTOCOL_VERSION, type, code: string(o.code, "error code", 80) }; } if (type === "final") return decodeFinal(o, type); throw new ProtocolError("unsupported server message type"); } +function punchTotals(value: unknown, fighters: readonly string[]): Record { + const entries = Object.entries(object(value, "punches")); + if (entries.length !== 2 || entries.some(([id]) => !fighters.includes(id))) throw new ProtocolError("punches must count both fighters"); + const totals = Object.create(null) as Record; + for (const [id, raw] of entries) { + const o = object(raw, "punch totals"); exact(o, ["thrown", "landed", "jabs_thrown", "jabs_landed"]); + const thrown = integer(o.thrown, "thrown"), landed = integer(o.landed, "landed", 0, thrown), jabsThrown = integer(o.jabs_thrown, "jabs_thrown", 0, thrown); + totals[id] = { thrown, landed, jabs_thrown: jabsThrown, jabs_landed: integer(o.jabs_landed, "jabs_landed", 0, Math.min(landed, jabsThrown)) }; + } + return totals; +} function decodeFinal(o: Obj, type: "final"): FinalMessage { - exact(o, ["version", "type", "match_id", "winner_id", "method", "round", "scorecards", "ratings"]); + exact(o, ["version", "type", "match_id", "winner_id", "method", "round", "scorecards", "ratings"], ["punches"]); const ratingsRaw = object(o.ratings, "ratings"), entries = Object.entries(ratingsRaw); if (entries.length !== 2) throw new ProtocolError("final requires two ratings"); const ratings = Object.create(null) as Record; for (const [id, raw] of entries) { string(id, "rating player id"); const r = object(raw, "rating"); exact(r, ["before", "after"]); ratings[id] = { before: integer(r.before, "before"), after: integer(r.after, "after") }; } const winner = nullableString(o.winner_id, "winner_id"); if (winner !== null && !Object.hasOwn(ratings, winner)) throw new ProtocolError("winner absent from ratings"); const method = oneOf(o.method, methods, "finish method"); coherentFinish(winner, method); - return { version: 3, type, match_id: string(o.match_id, "match_id"), winner_id: winner, method, round: integer(o.round, "round", 1, 15), scorecards: scorecards(o.scorecards), ratings }; + const punches = o.punches === undefined ? {} : { punches: punchTotals(o.punches, Object.keys(ratings)) }; + return { version: PROTOCOL_VERSION, type, match_id: string(o.match_id, "match_id"), winner_id: winner, method, round: integer(o.round, "round", 1, 15), scorecards: scorecards(o.scorecards), ratings, ...punches }; } export function encodeInput(sequence: number, clientTick: number, frame: { moveX: number; moveY: number; defense: HeldDefense; actions: readonly SemanticAction[] }): string { @@ -243,12 +281,22 @@ export function encodeInput(sequence: number, clientTick: number, frame: { moveX if (raw.kind === "foul") return { kind: "foul", foul: oneOf(raw.foul, fouls, "foul"), ...id }; return { kind: oneOf(raw.kind, movementKinds, "action"), ...id }; }; - return JSON.stringify({ version: 3, type: "input", sequence: integer(sequence, "sequence"), client_tick: integer(clientTick, "client tick"), move: { x: Math.max(-1000, Math.min(1000, Math.round(Number.isFinite(frame.moveX) ? frame.moveX : 0))), y: Math.max(-1000, Math.min(1000, Math.round(Number.isFinite(frame.moveY) ? frame.moveY : 0))) }, defense: oneOf(frame.defense, heldDefenses, "held defense"), actions: frame.actions.slice(0, 4).map(action) }); + return JSON.stringify({ version: PROTOCOL_VERSION, type: "input", sequence: integer(sequence, "sequence"), client_tick: integer(clientTick, "client tick"), move: { x: Math.max(-1000, Math.min(1000, Math.round(Number.isFinite(frame.moveX) ? frame.moveX : 0))), y: Math.max(-1000, Math.min(1000, Math.round(Number.isFinite(frame.moveY) ? frame.moveY : 0))) }, defense: oneOf(frame.defense, heldDefenses, "held defense"), actions: frame.actions.slice(0, 4).map(action) }); +} + +/** A style the fighter is choosing (`ready` false) or has settled on before the bout. */ +export function encodeStyleChoice(style: FighterStyle, ready: boolean): string { + return JSON.stringify({ version: PROTOCOL_VERSION, type: "style", style: oneOf(style, FIGHTER_STYLES, "fighter style"), ready: ready === true }); +} + +export const CPU_LEVELS = ["rookie", "contender", "champion"] as const; +export function encodeCpuRequest(level: CpuLevel): string { + return JSON.stringify({ version: PROTOCOL_VERSION, type: "cpu", level: oneOf(level, CPU_LEVELS, "computer level") }); } export function decodeBootstrap(value: unknown): BootstrapResponse { - const o = object(value, "bootstrap"); exact(o, ["client_id", "state", "protocol", "simulation"]); if (o.protocol !== 3) throw new ProtocolError("unsupported protocol version"); const s = object(o.simulation, "simulation"); exact(s, ["tick_rate", "ring_half_width", "ring_half_height"]); - return { client_id: string(o.client_id, "client_id", 36), state: string(o.state, "state", 128), protocol: 3, simulation: { tick_rate: integer(s.tick_rate, "tick rate", 1, 120), ring_half_width: integer(s.ring_half_width, "ring width", 1), ring_half_height: integer(s.ring_half_height, "ring height", 1) } }; + const o = object(value, "bootstrap"); exact(o, ["client_id", "state", "protocol", "simulation"]); if (o.protocol !== PROTOCOL_VERSION) throw new ProtocolError("unsupported protocol version"); const s = object(o.simulation, "simulation"); exact(s, ["tick_rate", "ring_half_width", "ring_half_height"]); + return { client_id: string(o.client_id, "client_id", 36), state: string(o.state, "state", 128), protocol: PROTOCOL_VERSION, simulation: { tick_rate: integer(s.tick_rate, "tick rate", 1, 120), ring_half_width: integer(s.ring_half_width, "ring width", 1), ring_half_height: integer(s.ring_half_height, "ring height", 1) } }; } export function decodeToken(value: unknown): TokenResponse { const o = object(value, "token response"); exact(o, ["access_token", "ticket", "player"]); const p = object(o.player, "player"); exact(p, ["id", "name", "avatar", "rating"]); diff --git a/web/hands/src/record.test.ts b/web/hands/src/record.test.ts new file mode 100644 index 00000000..1eb2fb7d --- /dev/null +++ b/web/hands/src/record.test.ts @@ -0,0 +1,15 @@ +import { isDebut, recordCard, recordLine } from "./record"; + +describe("a fighter's record", () => { + it("is written wins-losses, with draws only when there are any", () => { + expect(recordLine({ wins: 12, losses: 3, draws: 1, knockouts: 8 })).toBe("12-3-1"); + expect(recordLine({ wins: 7, losses: 0, draws: 0, knockouts: 2 })).toBe("7-0"); + }); + + it("calls a fighter with no bouts a debut", () => { + expect(isDebut({ wins: 0, losses: 0, draws: 0, knockouts: 0 })).toBe(true); + expect(isDebut({ wins: 0, losses: 0, draws: 1, knockouts: 0 })).toBe(false); + expect(recordCard({ wins: 0, losses: 0, draws: 0, knockouts: 0 })).toBe("PRO DEBUT"); + expect(recordCard({ wins: 36, losses: 1, draws: 0, knockouts: 29 })).toBe("36-1 (29 KO)"); + }); +}); diff --git a/web/hands/src/record.ts b/web/hands/src/record.ts new file mode 100644 index 00000000..f222c01a --- /dev/null +++ b/web/hands/src/record.ts @@ -0,0 +1,13 @@ +import type { FighterRecord } from "./types"; + +/** A record the way boxing writes it: wins and losses, and draws when there are any. */ +export function recordLine(record: FighterRecord): string { + return `${record.wins}-${record.losses}${record.draws > 0 ? `-${record.draws}` : ""}`; +} + +export const isDebut = (record: FighterRecord): boolean => record.wins + record.losses + record.draws === 0; + +/** The ring announcer's card: "12-3-1 (8 KO)", or a fighter's first bout. */ +export function recordCard(record: FighterRecord): string { + return isDebut(record) ? "PRO DEBUT" : `${recordLine(record)} (${record.knockouts} KO)`; +} diff --git a/web/hands/src/render/arena.ts b/web/hands/src/render/arena.ts index bfc0d059..aca14919 100644 --- a/web/hands/src/render/arena.ts +++ b/web/hands/src/render/arena.ts @@ -1,4 +1,6 @@ import * as THREE from "three"; +import { CROWD_TIERS, PARAPET_HEIGHT, PARAPET_SETBACK, buildCrowd } from "./crowd"; +import { ARENA_FLOOR } from "./world"; export interface BuiltArena { readonly group: THREE.Group; @@ -6,13 +8,16 @@ export interface BuiltArena { /** Raises crowd excitement (0..1); it decays over a few seconds. */ readonly excite: (amount: number) => void; readonly excitement: () => number; + /** Sheds the crowd's most expensive work when the client is struggling. */ + readonly setLowTier: (low: boolean) => void; + /** Clears the seats in front of the broadcast camera while it stands among them; called once the frame's camera is placed. */ + readonly makeRoomForCamera: (camera: { readonly x: number; readonly z: number }) => void; readonly dispose: () => void; } const EXCITEMENT_DECAY_PER_SECOND = 0.3; - -const CROWD_BODY_COLORS = [0x2a3140, 0x3a2f2a, 0x26343a, 0x40312e, 0x2e3a2c, 0x38343e, 0x443c30, 0x5a5148, 0x31404a, 0x4a3542]; -const CROWD_SKIN_COLORS = [0xc79b76, 0x8a5a3b, 0x6e4128, 0xe0b48f, 0x54301d, 0xa9744f]; +const BOARD_REPEATS = 6; +const BOARD_SCROLL_PER_SECOND = 0.012; const seededRandom = (seed: number): (() => number) => () => { seed = Math.imul(seed ^ (seed >>> 15), 1 | seed); @@ -33,60 +38,54 @@ export function buildArena(): BuiltArena { geometries.push(floorGeo); const floor = new THREE.Mesh(floorGeo, floorMat); floor.rotation.x = -Math.PI / 2; - floor.position.y = -1.0; + floor.position.y = ARENA_FLOOR; floor.receiveShadow = true; group.add(floor); const rand = seededRandom(20260727); - const tiers = [ - { radius: 8.2, y: -0.55, count: 120, scale: 1 }, - { radius: 10.6, y: 0.35, count: 150, scale: 1.04 }, - { radius: 13.2, y: 1.35, count: 180, scale: 1.08 }, - { radius: 16.0, y: 2.45, count: 210, scale: 1.12 }, - ]; - const total = tiers.reduce((sum, tier) => sum + tier.count, 0); - - const bodyGeo = new THREE.CapsuleGeometry(0.19, 0.5, 4, 8); - const headGeo = new THREE.SphereGeometry(0.115, 8, 7); - geometries.push(bodyGeo, headGeo); - const bodyMat = new THREE.MeshStandardMaterial({ roughness: 0.9, metalness: 0, emissive: 0x141926, emissiveIntensity: 0.7 }); - const headMat = new THREE.MeshStandardMaterial({ roughness: 0.75, metalness: 0, emissive: 0x2a1d16, emissiveIntensity: 0.7 }); - materials.push(bodyMat, headMat); - const bodies = new THREE.InstancedMesh(bodyGeo, bodyMat, total); - const heads = new THREE.InstancedMesh(headGeo, headMat, total); - bodies.instanceMatrix.setUsage(THREE.DynamicDrawUsage); - heads.instanceMatrix.setUsage(THREE.DynamicDrawUsage); - - const matrix = new THREE.Matrix4(); - const color = new THREE.Color(); - const phases = new Float32Array(total); - const bases: Array<{ x: number; y: number; z: number; yaw: number; scale: number }> = []; - let index = 0; - for (const tier of tiers) { - for (let i = 0; i < tier.count; i += 1) { - const angle = (i / tier.count) * Math.PI * 2 + rand() * 0.03; - const jitter = (rand() - 0.5) * 0.7; - const x = Math.sin(angle) * (tier.radius + jitter); - const z = Math.cos(angle) * (tier.radius + jitter); - const y = tier.y + (rand() - 0.5) * 0.12; - const yaw = Math.atan2(-x, -z) + (rand() - 0.5) * 0.5; - const scale = tier.scale * (0.92 + rand() * 0.2); - bases.push({ x, y, z, yaw, scale }); - phases[index] = rand() * Math.PI * 2; - matrix.compose(new THREE.Vector3(x, y + 0.45 * scale, z), new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), yaw), new THREE.Vector3(scale, scale, scale)); - bodies.setMatrixAt(index, matrix); - matrix.compose(new THREE.Vector3(x, y + 0.95 * scale, z), new THREE.Quaternion(), new THREE.Vector3(scale, scale, scale)); - heads.setMatrixAt(index, matrix); - color.setHex(CROWD_BODY_COLORS[Math.floor(rand() * CROWD_BODY_COLORS.length)]!).multiplyScalar(0.85 + rand() * 0.6); - bodies.setColorAt(index, color); - color.setHex(CROWD_SKIN_COLORS[Math.floor(rand() * CROWD_SKIN_COLORS.length)]!).multiplyScalar(0.85 + rand() * 0.35); - heads.setColorAt(index, color); - index += 1; - } + const tiers = CROWD_TIERS; + const crowd = buildCrowd(rand); + group.add(crowd.group); + + const boardCanvas = document.createElement("canvas"); + boardCanvas.width = 1024; + boardCanvas.height = 64; + const boardCtx = boardCanvas.getContext("2d"); + if (boardCtx !== null) { + boardCtx.fillStyle = "#070c1a"; + boardCtx.fillRect(0, 0, 1024, 64); + boardCtx.fillStyle = "#13203f"; + boardCtx.fillRect(0, 0, 1024, 5); + boardCtx.fillRect(0, 59, 1024, 5); + boardCtx.textBaseline = "middle"; + boardCtx.font = "800 38px Inter, system-ui, sans-serif"; + boardCtx.fillStyle = "#e9c46a"; + boardCtx.fillText("H A N D S", 28, 33); + boardCtx.fillStyle = "#c9d6f2"; + boardCtx.font = "600 26px Inter, system-ui, sans-serif"; + boardCtx.fillText("CHAMPIONSHIP BOXING", 300, 34); + boardCtx.fillStyle = "#e9c46a"; + boardCtx.font = "800 30px Inter, system-ui, sans-serif"; + boardCtx.fillText("FIGHT NIGHT", 700, 34); + boardCtx.fillStyle = "#b91c1c"; + boardCtx.fillRect(948, 22, 22, 22); + boardCtx.fillStyle = "#1d4ed8"; + boardCtx.fillRect(978, 22, 22, 22); } - bodies.instanceColor!.needsUpdate = true; - heads.instanceColor!.needsUpdate = true; - group.add(bodies, heads); + const boardTexture = new THREE.CanvasTexture(boardCanvas); + boardTexture.colorSpace = THREE.SRGBColorSpace; + boardTexture.wrapS = THREE.RepeatWrapping; + boardTexture.repeat.set(-BOARD_REPEATS, 1); + disposables.push(boardTexture); + const boardMat = new THREE.MeshBasicMaterial({ map: boardTexture, side: THREE.BackSide }); + materials.push(boardMat); + const boardTop = tiers[0]!.y + PARAPET_HEIGHT; + const boardGeo = new THREE.CylinderGeometry(tiers[0]!.radius - PARAPET_SETBACK - 0.03, tiers[0]!.radius - PARAPET_SETBACK - 0.03, boardTop - ARENA_FLOOR - 0.04, 72, 1, true); + geometries.push(boardGeo); + const boards = new THREE.Mesh(boardGeo, boardMat); + boards.name = "boards"; + boards.position.y = (boardTop + ARENA_FLOOR) / 2 + 0.02; + group.add(boards); const flashCount = 90; const flashPositions = new Float32Array(flashCount * 3); @@ -177,38 +176,15 @@ export function buildArena(): BuiltArena { let flashTimer = 0; let flashOn = 0; let excitement = 0; - const crowdMatrix = new THREE.Matrix4(); - const crowdPosition = new THREE.Vector3(); - const crowdQuaternion = new THREE.Quaternion(); - const crowdScale = new THREE.Vector3(); - const yAxis = new THREE.Vector3(0, 1, 0); + let still = false; const update = (time: number, dt: number, reducedMotion: boolean): void => { excitement = Math.max(0, excitement - dt * EXCITEMENT_DECAY_PER_SECOND); + // Reduced motion seats the crowd once, rather than leaving it in whatever pose it held. + if (reducedMotion && !still) crowd.update(0, 0, true); + still = reducedMotion; if (!reducedMotion) { - const swayAmplitude = 0.05 + excitement * 0.06; - const bounceAmplitude = 0.05 + excitement * 0.2; - const bounceRate = 2.3 + excitement * 3.2; - for (let i = 0; i < total; i += 1) { - const base = bases[i]!; - const sway = Math.sin(time * 1.6 + phases[i]!) * swayAmplitude; - const bounce = Math.abs(Math.sin(time * bounceRate + phases[i]! * 1.7)) * bounceAmplitude; - crowdQuaternion.setFromAxisAngle(yAxis, base.yaw + sway); - crowdMatrix.compose( - crowdPosition.set(base.x, base.y + 0.45 * base.scale + bounce, base.z + sway * 0.4), - crowdQuaternion, - crowdScale.set(base.scale, base.scale, base.scale), - ); - bodies.setMatrixAt(i, crowdMatrix); - crowdQuaternion.setFromAxisAngle(yAxis, base.yaw + sway * 1.2); - crowdMatrix.compose( - crowdPosition.set(base.x, base.y + 0.95 * base.scale + bounce * 1.2, base.z + sway * 0.5), - crowdQuaternion, - crowdScale, - ); - heads.setMatrixAt(i, crowdMatrix); - } - bodies.instanceMatrix.needsUpdate = true; - heads.instanceMatrix.needsUpdate = true; + crowd.update(time, excitement); + boardTexture.offset.x = (time * BOARD_SCROLL_PER_SECOND) % 1; flashTimer -= dt; if (flashTimer <= 0) { flashOn = 0.09 + Math.random() * 0.08; @@ -223,13 +199,12 @@ export function buildArena(): BuiltArena { for (const geometry of geometries) geometry.dispose(); for (const material of materials) material.dispose(); for (const disposable of disposables) disposable.dispose(); - bodies.dispose(); - heads.dispose(); + crowd.dispose(); }; const excite = (amount: number): void => { excitement = Math.min(1, excitement + Math.max(0, amount)); }; - return { group, update, excite, excitement: () => excitement, dispose }; + return { group, update, excite, excitement: () => excitement, setLowTier: crowd.setLowTier, makeRoomForCamera: crowd.makeRoomForCamera, dispose }; } diff --git a/web/hands/src/render/avatars.test.ts b/web/hands/src/render/avatars.test.ts new file mode 100644 index 00000000..0fa08a92 --- /dev/null +++ b/web/hands/src/render/avatars.test.ts @@ -0,0 +1,152 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { Avatars, avatarUrl, monogram } from "./avatars"; + +const ID = "123456789012345678"; +const HASH = "a_0123456789abcdef0123456789abcdef"; + +describe("avatar address", () => { + it("is on Discord's image host, for that player and that picture", () => { + expect(avatarUrl({ id: ID, avatar: HASH })).toBe(`https://cdn.discordapp.com/avatars/${ID}/${HASH}.png?size=128`); + }); + + it("is withheld when there is no picture or either part is not what Discord issues", () => { + expect(avatarUrl({ id: ID, avatar: null })).toBeNull(); + for (const id of ["", "one", "12345", "1".repeat(21), `${ID}/..`, `${ID}?x=1`, ` ${ID}`, "123456789012345678"]) { + expect(avatarUrl({ id, avatar: HASH })).toBeNull(); + } + for (const avatar of ["", "../../attachments/1", "abc.png", "abc/def", "abc?size=4096", "abc#", "a b", "a".repeat(129), "https://example.com/x"]) { + expect(avatarUrl({ id: ID, avatar })).toBeNull(); + } + }); + + it("never leaves the avatars folder of that host", () => { + for (const avatar of [HASH, "abc", "A_b_9", "a".repeat(128)]) { + const url = new URL(avatarUrl({ id: ID, avatar })!); + expect(url.origin).toBe("https://cdn.discordapp.com"); + expect(url.pathname).toBe(`/avatars/${ID}/${avatar}.png`); + expect(url.search).toBe("?size=128"); + } + }); +}); + +describe("monogram", () => { + it("is the first letter or digit of the name, in capitals", () => { + expect(monogram("azure vector")).toBe("A"); + expect(monogram(" ~*crimson*~")).toBe("C"); + expect(monogram("9lives")).toBe("9"); + expect(monogram("élan")).toBe("É"); + expect(monogram("🥊🥊")).toBe("?"); + expect(monogram("")).toBe("?"); + }); +}); + +interface FakeImage { + src: string; + crossOrigin: string | null; + referrerPolicy: string; + decoding: string; + naturalWidth: number; + onload: (() => void) | null; + onerror: (() => void) | null; +} + +function fakeImages(): { made: FakeImage[]; create: () => HTMLImageElement } { + const made: FakeImage[] = []; + return { + made, + create: () => { + const image: FakeImage = { src: "", crossOrigin: null, referrerPolicy: "", decoding: "auto", naturalWidth: 0, onload: null, onerror: null }; + made.push(image); + return image as unknown as HTMLImageElement; + }, + }; +} + +describe("avatars", () => { + const player = { id: ID, avatar: HASH }; + + it("asks for a picture once, without credentials or a referrer, and hands it out once it has arrived", () => { + const { made, create } = fakeImages(); + const avatars = new Avatars(create); + expect(avatars.get(player)).toBeNull(); + expect(avatars.get(player)).toBeNull(); + expect(made).toHaveLength(1); + expect(made[0]!.src).toBe(avatarUrl(player)); + expect(made[0]!.crossOrigin).toBe("anonymous"); + expect(made[0]!.referrerPolicy).toBe("no-referrer"); + made[0]!.naturalWidth = 128; + made[0]!.onload!(); + expect(avatars.get(player)).toBe(made[0]); + expect(made).toHaveLength(1); + }); + + it("asks for nothing when the player has no picture", () => { + const { made, create } = fakeImages(); + const avatars = new Avatars(create); + expect(avatars.get(undefined)).toBeNull(); + expect(avatars.get({ id: ID, avatar: null })).toBeNull(); + expect(avatars.get({ id: "alpha", avatar: HASH })).toBeNull(); + expect(made).toHaveLength(0); + }); + + it("does not ask again for a picture that failed or came back empty", () => { + const { made, create } = fakeImages(); + const avatars = new Avatars(create); + avatars.get(player); + made[0]!.onerror!(); + expect(avatars.get(player)).toBeNull(); + const other = { id: ID, avatar: "empty" }; + avatars.get(other); + made[1]!.onload!(); + expect(avatars.get(other)).toBeNull(); + expect(made).toHaveLength(2); + }); + + it("keeps a handful of pictures and lets the oldest go", () => { + const { made, create } = fakeImages(); + const avatars = new Avatars(create); + for (let index = 0; index < 9; index += 1) avatars.get({ id: ID, avatar: `picture_${index}` }); + expect(made).toHaveLength(9); + expect(made[0]!.onload).toBeNull(); + expect(made[1]!.onload).not.toBeNull(); + avatars.get({ id: ID, avatar: "picture_8" }); + expect(made).toHaveLength(9); + avatars.get({ id: ID, avatar: "picture_0" }); + expect(made).toHaveLength(10); + }); + + it("forgets every picture when it is disposed of", () => { + const { made, create } = fakeImages(); + const avatars = new Avatars(create); + avatars.get(player); + made[0]!.naturalWidth = 128; + made[0]!.onload!(); + avatars.dispose(); + expect(made[0]!.onload).toBeNull(); + expect(avatars.get(player)).toBeNull(); + expect(made).toHaveLength(2); + }); +}); + +describe("avatars asked for on every frame", () => { + const Original = URL; + afterEach(() => { + globalThis.URL = Original; + }); + + it("work the address out once per picture, and again when the picture changes", () => { + let built = 0; + globalThis.URL = class extends Original { + constructor(url: string | URL, base?: string | URL) { + super(url, base); + built += 1; + } + } as typeof URL; + const avatars = new Avatars(() => ({}) as HTMLImageElement); + for (let frame = 0; frame < 120; frame += 1) avatars.get({ id: ID, avatar: HASH }); + expect(built).toBe(1); + avatars.get({ id: ID, avatar: "b_1" }); + expect(built).toBe(2); + avatars.dispose(); + }); +}); diff --git a/web/hands/src/render/avatars.ts b/web/hands/src/render/avatars.ts new file mode 100644 index 00000000..3f2e1514 --- /dev/null +++ b/web/hands/src/render/avatars.ts @@ -0,0 +1,78 @@ +import type { PublicPlayer } from "../types"; + +type Pictured = Pick; + +const DISCORD_ID = /^[0-9]{17,20}$/u; +const AVATAR_HASH = /^[A-Za-z0-9_]{1,128}$/u; +const AVATAR_HOST = "https://cdn.discordapp.com/avatars/"; +const AVATAR_PIXELS = 128; +const MAX_PICTURES = 8; + +/** Where Discord serves the player's picture, or null when they have none. Never anywhere but Discord's image host. */ +export function avatarUrl(player: Pictured): string | null { + if (player.avatar === null || !DISCORD_ID.test(player.id) || !AVATAR_HASH.test(player.avatar)) return null; + return new URL(`${player.id}/${player.avatar}.png?size=${AVATAR_PIXELS}`, AVATAR_HOST).href; +} + +/** The letter that stands in for a picture. */ +export function monogram(name: string): string { + const first = Array.from(name).find((character) => /[\p{L}\p{N}]/u.test(character)); + return first === undefined ? "?" : first.toUpperCase(); +} + +interface Picture { + readonly image: HTMLImageElement; + ready: boolean; +} + +/** Players' pictures, each fetched once and handed out once it has arrived. */ +export class Avatars { + private readonly pictures = new Map(); + /** The address worked out for each player's current picture: the HUD asks for every player on every frame. */ + private readonly addresses = new Map(); + + constructor(private readonly create: () => HTMLImageElement = () => new Image()) {} + + get(player: Pictured | undefined): HTMLImageElement | null { + if (player === undefined) return null; + let address = this.addresses.get(player.id); + if (address === undefined || address.avatar !== player.avatar) { + if (this.addresses.size >= MAX_PICTURES) this.addresses.clear(); + address = { avatar: player.avatar, url: avatarUrl(player) }; + this.addresses.set(player.id, address); + } + const url = address.url; + if (url === null) return null; + const known = this.pictures.get(url); + if (known !== undefined) return known.ready ? known.image : null; + if (this.pictures.size >= MAX_PICTURES) { + const oldest = this.pictures.entries().next().value; + if (oldest !== undefined) this.drop(oldest[0], oldest[1]); + } + const picture: Picture = { image: this.create(), ready: false }; + this.pictures.set(url, picture); + picture.image.crossOrigin = "anonymous"; + picture.image.referrerPolicy = "no-referrer"; + picture.image.decoding = "async"; + picture.image.onload = () => { + picture.ready = picture.image.naturalWidth > 0; + }; + picture.image.onerror = () => { + picture.ready = false; + }; + picture.image.src = url; + return null; + } + + dispose(): void { + for (const [url, picture] of [...this.pictures]) this.drop(url, picture); + this.addresses.clear(); + } + + private drop(url: string, picture: Picture): void { + picture.image.onload = null; + picture.image.onerror = null; + picture.ready = false; + this.pictures.delete(url); + } +} diff --git a/web/hands/src/render/burst.test.ts b/web/hands/src/render/burst.test.ts new file mode 100644 index 00000000..b034ac19 --- /dev/null +++ b/web/hands/src/render/burst.test.ts @@ -0,0 +1,303 @@ +import * as THREE from "three"; +import { describe, expect, it } from "vitest"; +import { fighter } from "../test/fixtures"; +import type { CombatEvent, MatchResult } from "../types"; +import { Effects3D } from "./effects"; +import { BIG_SHOT, HARD_SHOT, closeCut } from "./gore"; +import { SkinnedBoxer, loadBoxerGlb } from "./graph"; +import { BURST_CUT_HEIGHT, HEAD_SITES, InjuryShading, NECK_CUT_HEIGHT, BODY_SITES, BODY_SWELL_CORE, applyBodyTrauma } from "./injury"; +import { aboveBurstCut, arcadeInjuryFor, measureBurstStump, replayReattaches, caveInRibs } from "./renderer"; +import { CANVAS_TOP } from "./world"; + +const gltf = await loadBoxerGlb(); +const hit = (kind: string, amount: number, detail = "uppercut:head", eventId = 2): CombatEvent => ({ event_id: eventId, tick: 10, kind, actor_id: "one", target_id: "two", amount, detail, blood: 30, direction: 1, action_id: null }); +const ending = (finish_method: MatchResult["finish_method"]) => ({ finish_method, winner_id: "one" }); +const downed = { ...fighter("two"), is_downed: true }; + +describe("the finisher a knockout punch earns", () => { + const puncher = (hand: "left" | "right") => ({ ...fighter("one"), action_key: `straight:${hand}:head:normal` }); + // [punch and target, kind, damage, the puncher's hand] and the finisher, by punch, place and weight alone. + const table: readonly (readonly [string, string, number, "left" | "right", string])[] = [ + ["jab:head", "hit", 34, "left", "jaw_dislocation"], + ["jab:head", "hit", HARD_SHOT + 30, "left", "jaw_dislocation"], + ["straight:head", "hit", 79, "right", "jaw_dislocation"], + ["straight:head", "hit", 80, "right", "decapitation"], + ["uppercut:head", "hit", HARD_SHOT - 1, "right", "jaw_dislocation"], + ["uppercut:head", "hit", HARD_SHOT, "right", "decapitation"], + ["hook:head", "hit", HARD_SHOT - 1, "left", "jaw_dislocation"], + ["hook:head", "hit", HARD_SHOT, "left", "eye_right"], + ["hook:head", "hit", HARD_SHOT + 40, "right", "eye_left"], + ["straight:head", "hit", 140, "right", "decapitation"], + ["straight:head", "counter_hit", BIG_SHOT, "right", "head_burst"], + ["jab:body", "hit", 50, "left", "ribs_right"], + ["straight:body", "hit", 140, "right", "ribs_left"], + ["uppercut:body", "counter_hit", 120, "left", "ribs_right"], + ["hook:body", "hit", HARD_SHOT - 1, "left", "ribs_right"], + ["hook:body", "hit", HARD_SHOT, "left", "shoulder_right"], + ["hook:body", "hit", BIG_SHOT, "right", "dismember_left"], + ]; + + it("is chosen by the punch, where it landed and how hard, whatever the event's id", () => { + for (const [detail, kind, amount, hand, injury] of table) { + for (const eventId of [1, 2, 3, 4, 17, 40]) { + expect(arcadeInjuryFor(hit(kind, amount, detail, eventId), downed, ending("ko"), puncher(hand)), `${detail} ${kind} ${amount} ${hand} #${eventId}`).toBe(injury); + } + } + }); + + it("never takes a man's head off with a jab, nor a hand with a punch up the middle of his body", () => { + for (let amount = 1; amount <= 160; amount += 1) { + expect(arcadeInjuryFor(hit("hit", amount, "jab:head", amount), downed, ending("ko"), puncher("left"))).toBe("jaw_dislocation"); + for (const detail of ["jab:body", "straight:body", "uppercut:body"]) expect(arcadeInjuryFor(hit("counter_hit", amount, detail, amount), downed, ending("tko"), puncher("right"))).toBe("ribs_left"); + } + }); + + it("bursts the head of a flash knockout whatever the punch, and lands on his right without the puncher's hand", () => { + expect(arcadeInjuryFor(hit("hit", 40, "jab:head"), fighter("two"), ending("flash_ko"))).toBe("head_burst"); + expect(arcadeInjuryFor(hit("hit", 90, "hook:head"), downed, ending("ko"))).toBe("eye_right"); + expect(arcadeInjuryFor(hit("hit", 60, "straight:body"), downed, ending("ko"))).toBe("ribs_right"); + }); +}); + +describe("ribs caved in by a body shot", () => { + it("dent deep into the side the punch landed on, black with bruising and running with blood", () => { + for (const side of ["left", "right"] as const) { + const shading = new InjuryShading(null, BODY_SITES, { core: BODY_SWELL_CORE, wash: true }); + // The engine's trauma is painted first each frame; the caved-in ribs go over it. + applyBodyTrauma(shading, fighter("two").trauma, "full"); + caveInRibs(shading, side); + const site = BODY_SITES.find((candidate) => candidate.name === `${side}Ribs`)!; + const impact = shading.uniforms.uInjuryImpact.value; + expect([impact.x, impact.y, impact.z]).toEqual([...site.position]); + const push = shading.uniforms.uInjuryImpactPush.value; + // Into the body, toward the spine from the side it is on, deeper than any punch's own dent (3.2 cm). + expect(Math.sign(push.x)).toBe(-Math.sign(site.position[0])); + expect(shading.impactDepth).toBeGreaterThan(5); + const ribs = shading.level(`${side}Ribs`); + expect(ribs.bruise).toBeCloseTo(1.2, 6); + expect(ribs.swell).toBe(0); + expect(ribs.blood).toBeCloseTo(1.4, 6); + // A punch's dent springs back; these are set again every frame and stay in. + shading.update(0.5); + expect(shading.impactDepth).toBeLessThan(0.1); + caveInRibs(shading, side); + expect(shading.impactDepth).toBeGreaterThan(5); + } + expect(replayReattaches("ribs_left")).toBe(true); + }); +}); + +describe("a head that bursts", () => { + it("is what a flash knockout or a big counter to the head earns; other finishers stay as they were", () => { + expect(arcadeInjuryFor(hit("counter_hit", BIG_SHOT), downed, ending("ko"))).toBe("head_burst"); + expect(arcadeInjuryFor(hit("hit", 60, "hook:head"), fighter("two"), ending("flash_ko"))).toBe("head_burst"); + expect(arcadeInjuryFor(hit("counter_hit", BIG_SHOT - 1, "uppercut:head", 2), downed, ending("ko"))).toBe("decapitation"); + expect(arcadeInjuryFor(hit("hit", 140, "uppercut:head", 3), downed, ending("tko"))).toBe("decapitation"); + expect(arcadeInjuryFor(hit("counter_hit", 140, "hook:body", 2), downed, ending("ko"))).not.toBe("head_burst"); + expect(replayReattaches("head_burst")).toBe(true); + }); + + it("takes everything above the mouth and leaves the chin, the jaw and the collar", () => { + expect(aboveBurstCut(new THREE.Vector3(0, 120, 3.4))).toBe(true); + expect(aboveBurstCut(new THREE.Vector3(0, 117, 5.3))).toBe(true); + expect(aboveBurstCut(new THREE.Vector3(0, 126, -6))).toBe(true); + expect(aboveBurstCut(new THREE.Vector3(0, 110.8, 4.2))).toBe(false); + expect(aboveBurstCut(new THREE.Vector3(4.6, 112, 1.4))).toBe(false); + expect(aboveBurstCut(new THREE.Vector3(0, 106, -4))).toBe(false); + }); + + it("cuts the skin and its shadow along a ragged line level with the mouth, and back to the neck cut when severed", () => { + const material = new THREE.MeshStandardMaterial(); + const shading = new InjuryShading(material, HEAD_SITES); + const shadow = shading.shadowMaterial(); + const depth = { uniforms: {} as Record, vertexShader: "#include \n#include ", fragmentShader: "#include \n#include " }; + shadow.onBeforeCompile(depth as unknown as THREE.WebGLProgramParametersWithUniforms, null as unknown as THREE.WebGLRenderer); + expect(depth.uniforms.uInjurySeverShape).toBe(shading.uniforms.uInjurySeverShape); + shading.setBurst(true); + expect(shading.uniforms.uInjurySever.value).toBe(BURST_CUT_HEIGHT); + expect(shading.uniforms.uInjurySeverShape.value.x).toBe(0); + expect(shading.uniforms.uInjurySeverShape.value.y).toBeGreaterThan(1); + shading.setSevered(true); + expect(shading.uniforms.uInjurySever.value).toBe(NECK_CUT_HEIGHT); + expect(shading.uniforms.uInjurySeverShape.value.y).toBe(1); + shading.setBurst(false); + expect(shading.uniforms.uInjurySever.value).toBeGreaterThan(500); + }); + + it("shows raw flesh inside the head where it is open, drawing the head from inside too", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + expect((boxer.headMesh.material as THREE.Material).shadowSide).toBe(THREE.BackSide); + const shader = { uniforms: {}, vertexShader: "#include \n#include ", fragmentShader: "#include \n#include \n#include " }; + boxer.headInjury.material!.onBeforeCompile(shader as unknown as THREE.WebGLProgramParametersWithUniforms, null as unknown as THREE.WebGLRenderer); + expect(shader.fragmentShader).toContain("if (!gl_FrontFacing)"); + boxer.setHeadBurst(true); + expect((boxer.headMesh.material as THREE.Material).side).toBe(THREE.DoubleSide); + expect(boxer.isHeadBurst).toBe(true); + expect(boxer.isDecapitated).toBe(false); + boxer.setDecapitated(true); + expect(boxer.isHeadBurst).toBe(false); + boxer.dispose(); + }); + + it("leaves a wound closing the neck level with the mouth, its teeth turned to the front of the head", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + boxer.root.rotation.y = 1.1; + const out = { position: new THREE.Vector3(), quaternion: new THREE.Quaternion(), across: new THREE.Vector3(), scratch: new THREE.Vector3() }; + const rim = measureBurstStump(boxer, new Float32Array(0), out); + expect(rim).not.toBeNull(); + expect(rim!.length).toBeGreaterThan(60); + const head = boxer.bone("head")!.getWorldPosition(new THREE.Vector3()); + expect(out.position.distanceTo(head)).toBeLessThan(0.12); + const up = new THREE.Vector3(0, 1, 0).applyQuaternion(out.quaternion); + expect(up.y).toBeGreaterThan(0.5); + expect(Math.abs(out.across.dot(up))).toBeLessThan(1e-6); + // The same array is reused from frame to frame. + expect(measureBurstStump(boxer, rim!, out)).toBe(rim); + boxer.dispose(); + }); + + it("throws out skull, brain and teeth with a great spray of blood, once, and only with full blood", () => { + const burst = (level: "full" | "reduced"): Effects3D => { + const effects = new Effects3D(new THREE.Scene(), 256); + effects.setBloodLevel(level); + effects.burstHead(1, new THREE.Vector3(0.4, 1.6, 0), -1, 21); + return effects; + }; + const full = burst("full"); + expect(full.headBurst(1)).toBe(true); + expect(full.headBurst(0)).toBe(false); + expect(full.debrisCount("shard")).toBe(18); + expect(full.debrisCount("brain")).toBe(14); + expect(full.debrisCount("tooth")).toBe(5); + expect(full.liveBloodParticles).toBeGreaterThan(150); + full.burstHead(1, new THREE.Vector3(0.4, 1.6, 0), -1, 21); + expect(full.debrisCount("shard")).toBe(18); + full.restoreFighter(1); + expect(full.headBurst(1)).toBe(false); + full.dispose(); + const reduced = burst("reduced"); + expect(reduced.headBurst(1)).toBe(false); + expect(reduced.debrisCount("shard")).toBe(0); + reduced.dispose(); + }); + + it("leaves skull, brain, teeth and flesh lying where they land for the rest of the bout", () => { + const scene = new THREE.Scene(); + const effects = new Effects3D(scene, 256); + effects.burstHead(0, new THREE.Vector3(0, 1.6, 0), 1, 5); + effects.decapitate(1, new THREE.Vector3(1, 1.6, 0), new THREE.Quaternion(), -1, 6); + // Half a minute on, through the replay and the result card, all of it is still there. + for (let frame = 0; frame < 60 * 30; frame += 1) effects.update(1 / 60); + expect(effects.debrisCount("shard")).toBe(18); + expect(effects.debrisCount("brain")).toBe(14); + expect(effects.debrisCount("tooth")).toBe(5); + expect(effects.debrisCount("flesh")).toBe(24); + const matrix = new THREE.Matrix4(); + const at = new THREE.Vector3(); + for (const mesh of scene.children.filter((child): child is THREE.InstancedMesh => child instanceof THREE.InstancedMesh && child !== effects.dropletMesh)) { + for (let index = 0; index < mesh.count; index += 1) { + mesh.getMatrixAt(index, matrix); + if (at.setFromMatrixPosition(matrix).y > -10) expect(at.y).toBeCloseTo(CANVAS_TOP + 0.015, 6); + } + // Lying still, it is not sent to the graphics card again. + const version = mesh.instanceMatrix.version; + effects.update(1 / 60); + expect(mesh.instanceMatrix.version).toBe(version); + } + // A replay that puts the head back on takes up what its burst threw; the other man's flesh stays. + effects.restoreFighter(0); + expect(effects.debrisCount("shard")).toBe(0); + expect(effects.debrisCount("brain")).toBe(0); + expect(effects.debrisCount("flesh")).toBe(24); + effects.clearDynamic(); + expect(effects.debrisCount("flesh")).toBe(0); + effects.dispose(); + }); + + it("makes room for more by shrinking the oldest piece on the canvas away, never popping one", () => { + const effects = new Effects3D(new THREE.Scene(), 256); + const gibs = (effects as unknown as { gibMesh: THREE.InstancedMesh }).gibMesh; + const matrix = new THREE.Matrix4(); + const position = new THREE.Vector3(); + const turn = new THREE.Quaternion(); + const scale = new THREE.Vector3(); + const read = (): { x: number; y: number; z: number; size: number }[] => Array.from({ length: gibs.count }, (_, index) => { + gibs.getMatrixAt(index, matrix); + matrix.decompose(position, turn, scale); + return { x: position.x, y: position.y, z: position.z, size: scale.y }; + }); + // A big counter tears flesh every two seconds for a minute: 240 pieces for 96 places. + let earlier = read(); + let before = read(); + let popped = 0; + const still = (a: { x: number; y: number; z: number }, b: { x: number; y: number; z: number }): boolean => Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z) < 1e-6; + for (let frame = 0; frame < 60 * 60; frame += 1) { + if (frame % 120 === 0) effects.addEvent({ ...hit("counter_hit", 140, "hook:head", frame + 1), blood: 70 }, new THREE.Vector3(), false); + effects.update(1 / 60); + const after = read(); + for (const [index, was] of before.entries()) { + // A piece that lay still on the canvas, gone the next frame at more than a fraction of its size. + const lying = Math.abs(was.y - (CANVAS_TOP + 0.015)) < 1e-6 && still(was, earlier[index]!); + if (lying && !still(was, after[index]!) && was.size > 0.15) popped += 1; + } + earlier = before; + before = after; + } + expect(effects.liveGibs).toBeGreaterThan(80); + expect(popped).toBe(0); + effects.dispose(); + }); + + it("draws teeth white and brain grey, not in the colour of flesh", () => { + const effects = new Effects3D(new THREE.Scene(), 256); + effects.spawnTeeth(new THREE.Vector3(0, 1.5, 0), 1, 1, 3); + const mesh = (effects as unknown as { gibMesh: THREE.InstancedMesh }).gibMesh; + const color = new THREE.Color(); + mesh.getColorAt(0, color); + expect(color.r).toBeGreaterThan(0.8); + expect(color.g).toBeGreaterThan(0.75); + expect((mesh.material as THREE.MeshStandardMaterial).color.getHex()).toBe(0xffffff); + effects.dispose(); + }); +}); + +describe("flesh over a cut that has a front", () => { + it("turns its texture with the body when told which way is across", () => { + const rim = new Float32Array([0.05, 0, 0, 0, 0, 0.05, 0, 0, 0.05, -0.05, 0, 0, -0.05, 0, 0, 0, 0, -0.05, 0, 0, -0.05, 0.05, 0, 0]); + const uvAt = (across: THREE.Vector3 | undefined): { u: number; v: number } => { + const geometry = new THREE.BufferGeometry(); + closeCut(geometry, rim, new THREE.Vector3(), new THREE.Vector3(0, 1, 0), 0.01, across); + const position = geometry.getAttribute("position"); + const uv = geometry.getAttribute("uv"); + for (let index = 0; index < position.count; index += 1) { + if (Math.abs(position.getZ(index) - 0.05) < 1e-6 && Math.abs(position.getX(index)) < 1e-6) return { u: uv.getX(index), v: uv.getY(index) }; + } + throw new Error("front of the rim not found"); + }; + // The front of the cut (+Z) lies toward the low edge of the texture whatever way across is turned. + expect(uvAt(new THREE.Vector3(1, 0, 0)).v).toBeLessThan(0.25); + expect(uvAt(new THREE.Vector3(0, 0, 1)).u).toBeGreaterThan(0.75); + expect(uvAt(new THREE.Vector3(0, 1, 0)).v).toBeLessThan(0.25); + }); +}); + +describe("what a cut shows", () => { + it("is the inside of the limb or neck it went through: the wrist its two bones, the neck its spine, the jaw its teeth", () => { + const effects = new Effects3D(new THREE.Scene(), 256); + const inner = effects as unknown as { stumps: { mesh: THREE.Mesh }[]; handStumps: { mesh: THREE.Mesh }[]; hands: { cap: THREE.Mesh }[]; heads: { cap: THREE.Mesh }[] }; + const map = (mesh: THREE.Mesh): THREE.Texture | null => (mesh.material as THREE.MeshStandardMaterial).map; + const neck = map(inner.stumps[0]!.mesh); + const wrist = map(inner.handStumps[0]!.mesh); + expect(neck).not.toBeNull(); + expect(wrist).not.toBeNull(); + expect(wrist).not.toBe(neck); + expect(map(inner.hands[0]!.cap)).toBe(wrist); + expect(map(inner.heads[0]!.cap)).toBe(neck); + effects.burstHead(0, new THREE.Vector3(0, 1.6, 0), 1, 3); + const jaw = map(inner.stumps[0]!.mesh); + expect(jaw).not.toBe(neck); + effects.restoreFighter(0); + expect(map(inner.stumps[0]!.mesh)).toBe(neck); + effects.dispose(); + }); +}); diff --git a/web/hands/src/render/camera.test.ts b/web/hands/src/render/camera.test.ts new file mode 100644 index 00000000..e8bc3c2c --- /dev/null +++ b/web/hands/src/render/camera.test.ts @@ -0,0 +1,95 @@ +import * as THREE from "three"; +import { CameraDirector, FIGHTER_CAM_LIMIT, FighterCam, fighterCamFrame } from "./camera"; + +const step = 1 / 60; + +describe("the player's own camera", () => { + it("stands behind the fighter and over his right shoulder, looking at the opponent", () => { + const position = new THREE.Vector3(); + const lookAt = new THREE.Vector3(); + fighterCamFrame({ x: -0.6, z: 0 }, { x: 0.6, z: 0 }, 1, 0, position, lookAt); + expect(position.x).toBeLessThan(-2.4); + expect(position.z).toBeGreaterThan(0.3); + expect(position.y).toBeGreaterThan(1.6); + expect(lookAt.x).toBeCloseTo(0.6); + expect(lookAt.z).toBeCloseTo(0); + }); + + it("stays inside the ropes when the fighter has his back to them", () => { + const position = new THREE.Vector3(); + const lookAt = new THREE.Vector3(); + fighterCamFrame({ x: -2.7, z: 2.7 }, { x: -1.7, z: 1.7 }, Math.SQRT1_2, -Math.SQRT1_2, position, lookAt); + expect(Math.abs(position.x)).toBeLessThanOrEqual(FIGHTER_CAM_LIMIT); + expect(Math.abs(position.z)).toBeLessThanOrEqual(FIGHTER_CAM_LIMIT); + }); + + it("starts behind the fighter at once and then turns with him a beat behind", () => { + const cam = new FighterCam(); + cam.update(step, 0, { x: -0.6, z: 0 }, { x: 0.6, z: 0 }, 0, false); + expect(cam.forwardX).toBeCloseTo(1); + expect(cam.forwardZ).toBeCloseTo(0); + cam.update(step, step, { x: -0.6, z: 0 }, { x: -0.6, z: 1.2 }, 0, false); + expect(cam.forwardZ).toBeGreaterThan(0.02); + expect(cam.forwardZ).toBeLessThan(0.5); + for (let frame = 0; frame < 120; frame += 1) cam.update(step, step * frame, { x: -0.6, z: 0 }, { x: -0.6, z: 1.2 }, 0, false); + expect(cam.forwardZ).toBeCloseTo(1, 2); + cam.reset(); + cam.update(step, 0, { x: 0, z: 0 }, { x: 0, z: -1 }, 0, false); + expect(cam.forwardZ).toBeCloseTo(-1); + }); + + it("turns the short way round", () => { + const cam = new FighterCam(); + cam.update(step, 0, { x: 0, z: 0 }, { x: 0.05, z: -1 }, 0, false); + const before = Math.atan2(cam.forwardX, cam.forwardZ); + cam.update(step, step, { x: 0, z: 0 }, { x: -0.05, z: -1 }, 0, false); + const after = Math.atan2(cam.forwardX, cam.forwardZ); + expect(Math.abs(Math.atan2(Math.sin(after - before), Math.cos(after - before)))).toBeLessThan(0.1); + }); + + it("swings round toward the side when the fighters are chest to chest", () => { + const orbit = (cam: FighterCam, opponentX: number): number => { + let frame = cam.update(step, 0, { x: 0, z: 0 }, { x: opponentX, z: 0 }, 0, false); + for (let index = 0; index < 240; index += 1) frame = cam.update(step, step * index, { x: 0, z: 0 }, { x: opponentX, z: 0 }, 0, false); + return Math.atan2(frame.position.z, -frame.position.x); + }; + const apart = orbit(new FighterCam(), 1.6); + const together = orbit(new FighterCam(), 0.4); + expect(THREE.MathUtils.radToDeg(apart)).toBeCloseTo(31, 0); + expect(THREE.MathUtils.radToDeg(together)).toBeGreaterThan(55); + }); + + it("keeps its last direction when the fighters stand on the same spot", () => { + const cam = new FighterCam(); + cam.update(step, 0, { x: 0, z: 0 }, { x: 1, z: 0 }, 0, false); + cam.update(step, step, { x: 0.5, z: 0 }, { x: 0.5, z: 0 }, 0, false); + expect(cam.forwardX).toBeCloseTo(1); + }); + + it("shakes with a heavy blow unless motion is reduced", () => { + const still = new FighterCam(); + const shaken = new FighterCam(); + const calm = new FighterCam(); + const a = still.update(step, 0.4, { x: -0.6, z: 0 }, { x: 0.6, z: 0 }, 0, false).position.clone(); + const b = shaken.update(step, 0.4, { x: -0.6, z: 0 }, { x: 0.6, z: 0 }, 0.05, false).position.clone(); + const c = calm.update(step, 0.4, { x: -0.6, z: 0 }, { x: 0.6, z: 0 }, 0.05, true).position.clone(); + expect(a.distanceTo(b)).toBeGreaterThan(0.005); + expect(a.distanceTo(c)).toBe(0); + }); +}); + +describe("the close broadcast camera", () => { + it("stands nearer the fighters than the broadcast camera and from the same side", () => { + const broadcast = new CameraDirector(); + const close = new CameraDirector(); + let wide = { position: new THREE.Vector3(), lookAt: new THREE.Vector3() }; + let near = { position: new THREE.Vector3(), lookAt: new THREE.Vector3() }; + for (let frame = 0; frame < 240; frame += 1) { + wide = broadcast.update(1 / 30, frame / 30, { x: -0.6, z: 0 }, { x: 0.6, z: 0 }, 1.2, false, 0, true); + near = close.update(1 / 30, frame / 30, { x: -0.6, z: 0 }, { x: 0.6, z: 0 }, 1.2, false, 0, true, true); + } + expect(near.position.distanceTo(near.lookAt)).toBeLessThan(wide.position.distanceTo(wide.lookAt) * 0.8); + expect(near.position.z).toBeGreaterThan(2.5); + expect(near.position.y).toBeLessThan(wide.position.y); + }); +}); diff --git a/web/hands/src/render/camera.ts b/web/hands/src/render/camera.ts index 782c7e3c..5b6ef0a8 100644 --- a/web/hands/src/render/camera.ts +++ b/web/hands/src/render/camera.ts @@ -3,14 +3,22 @@ import * as THREE from "three"; const BASE_HEIGHT = 2.05; const BASE_DISTANCE = 5.5; const LOOK_HEIGHT = 1.05; +const CLOSE_HEIGHT = 1.78; +const CLOSE_DISTANCE = 3.75; +/** How far over a count the shot moves from between the fighters to the one on the canvas, and how low it looks. */ +const DOWNED_FOCUS = 0.85; +const DOWNED_LOOK_HEIGHT = 0.45; +const CLOSE_LOOK_HEIGHT = 1.18; export interface CameraFrame { readonly position: THREE.Vector3; readonly lookAt: THREE.Vector3; } +/** Seconds before the bell at which the corners are cleared: the fighters stand and the crews climb out. */ +export const SECONDS_OUT = 3; export const CORNER_SHOT_LEAD_SECONDS = 2.5; -export const CORNER_SHOT_TAIL_SECONDS = 2; +export const CORNER_SHOT_TAIL_SECONDS = SECONDS_OUT; export const CORNER_SHOT_SECONDS = 5.5; /** Which corner the broadcast cuts to during the rest, or null for the wide shot while the fighters walk over and before the bell. */ @@ -51,6 +59,24 @@ export function cornerFrame(index: 0 | 1, corner: number, shotSeconds: number, p cornerPoint(index, corner, 0.62, 27, lookAt).setY(1.05); } +/** The three at the decision, from above the raised glove down to the waist. */ +const CEREMONY_SUBJECT = { top: 2.42, bottom: 0.95, width: 2.4 } as const; + +/** + * Frames the announcement in the part of the screen the result card leaves free: how far back the + * camera stands and the height it looks at. `covered` is the share of the screen's height under the + * card and `clear` the share under the top bar. + */ +export function ceremonyShot(aspect: number, fovDegrees: number, covered: number, clear = 0.09): { distance: number; height: number } { + const free = Math.max(0.2, 1 - covered - clear); + const span = Math.max((CEREMONY_SUBJECT.top - CEREMONY_SUBJECT.bottom) / free, CEREMONY_SUBJECT.width / Math.max(0.1, aspect)); + const middle = (CEREMONY_SUBJECT.top + CEREMONY_SUBJECT.bottom) / 2; + return { + distance: span / (2 * Math.tan(THREE.MathUtils.degToRad(fovDegrees) / 2)), + height: middle - (0.5 - (clear + free / 2)) * span, + }; +} + export class CameraDirector { private readonly current = new THREE.Vector3(0, BASE_HEIGHT, BASE_DISTANCE); private readonly look = new THREE.Vector3(0, LOOK_HEIGHT, 0); @@ -65,15 +91,22 @@ export class CameraDirector { knockdown: boolean, shake: number, reducedMotion: boolean, + close = false, + downed: { readonly x: number; readonly z: number } | null = null, ): CameraFrame { this.swayPhase += dt; - const midX = (fighterA.x + fighterB.x) / 2; - const midZ = (fighterA.z + fighterB.z) / 2; + // Over a count the shot centres on the man on the canvas, not on the gap between the two, where he + // would lie along the bottom of the picture behind the plates. + const onCanvas = knockdown && downed !== null ? DOWNED_FOCUS : 0; + const midX = (fighterA.x + fighterB.x) / 2 + ((downed?.x ?? 0) - (fighterA.x + fighterB.x) / 2) * onCanvas; + const midZ = (fighterA.z + fighterB.z) / 2 + ((downed?.z ?? 0) - (fighterA.z + fighterB.z) / 2) * onCanvas; const clampedX = THREE.MathUtils.clamp(midX, -1.4, 1.4); const clampedZ = THREE.MathUtils.clamp(midZ, -1.1, 1.1); - const distance = THREE.MathUtils.clamp(BASE_DISTANCE + separation * 0.6 - (knockdown ? 0.9 : 0), 4.6, 8.4); - const height = BASE_HEIGHT + separation * 0.08 - (knockdown ? 0.45 : 0); + const distance = close + ? THREE.MathUtils.clamp(CLOSE_DISTANCE + separation * 0.45 - (knockdown ? 0.6 : 0), 3.3, 5.6) + : THREE.MathUtils.clamp(BASE_DISTANCE + separation * 0.6 - (knockdown ? 0.9 : 0), 4.6, 8.4); + const height = (close ? CLOSE_HEIGHT : BASE_HEIGHT) + separation * 0.08 - (knockdown ? 0.45 : 0); const sway = reducedMotion ? 0 : Math.sin(this.swayPhase * 0.21) * 0.35; const drift = reducedMotion ? 0 : Math.sin(this.swayPhase * 0.13) * 0.3; @@ -87,7 +120,7 @@ export class CameraDirector { this.current.z += (targetZ - this.current.z) * (1 - Math.exp(-followRate * dt)); this.look.x += (clampedX - this.look.x) * (1 - Math.exp(-3.2 * dt)); - this.look.y += ((knockdown ? 0.7 : LOOK_HEIGHT) - this.look.y) * (1 - Math.exp(-3.2 * dt)); + this.look.y += ((knockdown ? (onCanvas > 0 ? DOWNED_LOOK_HEIGHT : 0.7) : close ? CLOSE_LOOK_HEIGHT : LOOK_HEIGHT) - this.look.y) * (1 - Math.exp(-3.2 * dt)); this.look.z += (clampedZ * 0.6 - this.look.z) * (1 - Math.exp(-3.2 * dt)); if (!reducedMotion && shake > 0.0005) { @@ -101,3 +134,101 @@ export class CameraDirector { return { position: this.current, lookAt: this.look }; } } + +const FIGHTER_CAM_DISTANCE = 2.45; +/** Degrees round from straight behind the fighter toward his right: wider when the two are chest to chest. */ +export const FIGHTER_CAM_ORBIT = 31; +export const FIGHTER_CAM_CLOSE_ORBIT = 60; +const FIGHTER_CAM_HEIGHT = 2.25; +const FIGHTER_CAM_LOOK_HEIGHT = 1.35; +/** Over the apron at most (it ends at 3.85 m): the camera stands high enough there to look over the top rope (1.26 m). */ +export const FIGHTER_CAM_LIMIT = 3.5; +export const FIGHTER_CAM_FOV_SCALE = 1.25; +/** On a screen narrower than it is tall the orbit shrinks with the aspect, to no less than this share of it. */ +const FIGHTER_CAM_NARROW_ORBIT = 0.45; + +/** + * Where the over-the-shoulder camera stands: behind the player's own fighter and `orbitDegrees` + * round toward his right shoulder, `forward` being the unit direction he looks along the canvas, + * aimed at the opponent. + */ +export function fighterCamFrame( + viewer: { x: number; z: number }, + opponent: { x: number; z: number }, + forwardX: number, + forwardZ: number, + position: THREE.Vector3, + lookAt: THREE.Vector3, + orbitDegrees = FIGHTER_CAM_ORBIT, +): void { + const orbit = THREE.MathUtils.degToRad(orbitDegrees); + const back = Math.cos(orbit) * FIGHTER_CAM_DISTANCE; + const side = Math.sin(orbit) * FIGHTER_CAM_DISTANCE; + const rightX = -forwardZ; + const rightZ = forwardX; + const offsetX = -forwardX * back + rightX * side; + const offsetZ = -forwardZ * back + rightZ * side; + // Short of room behind him, the camera comes in along the same line rather than sliding round to his side. + const room = (from: number, offset: number): number => Math.abs(offset) < 1e-9 ? 1 : THREE.MathUtils.clamp((Math.sign(offset) * FIGHTER_CAM_LIMIT - from) / offset, 0, 1); + const reach = Math.min(room(viewer.x, offsetX), room(viewer.z, offsetZ)); + position.set( + THREE.MathUtils.clamp(viewer.x + offsetX * reach, -FIGHTER_CAM_LIMIT, FIGHTER_CAM_LIMIT), + FIGHTER_CAM_HEIGHT, + THREE.MathUtils.clamp(viewer.z + offsetZ * reach, -FIGHTER_CAM_LIMIT, FIGHTER_CAM_LIMIT), + ); + lookAt.set(opponent.x, FIGHTER_CAM_LOOK_HEIGHT, opponent.z); +} + +/** The player's own view of the fight: it turns with him toward the opponent, a beat behind. */ +export class FighterCam { + private angle = Math.PI; + private closeness = 0; + private initialised = false; + private readonly target = new THREE.Vector3(); + private readonly position = new THREE.Vector3(); + private readonly look = new THREE.Vector3(); + private readonly aim = new THREE.Vector3(); + + /** Unit direction the camera looks along the canvas (world x and z). */ + get forwardX(): number { return Math.sin(this.angle); } + get forwardZ(): number { return Math.cos(this.angle); } + + reset(): void { this.initialised = false; } + + /** + * `aspect` is the screen's width over its height. A phone held upright sees too little to the sides for + * the orbit, so the camera comes round behind his back until he and the opponent fit across it. + */ + update(dt: number, time: number, viewer: { x: number; z: number }, opponent: { x: number; z: number }, shake: number, reducedMotion: boolean, aspect = 16 / 9): CameraFrame & { tight: true } { + const dx = opponent.x - viewer.x; + const dz = opponent.z - viewer.z; + const apart = Math.hypot(dx, dz); + const wanted = apart > 0.05 ? Math.atan2(dx, dz) : this.angle; + const closeness = THREE.MathUtils.clamp((1.3 - apart) / 0.8, 0, 1); + if (!this.initialised) { + this.angle = wanted; + this.closeness = closeness; + } else { + const turn = Math.atan2(Math.sin(wanted - this.angle), Math.cos(wanted - this.angle)); + this.angle += turn * (1 - Math.exp(-4.5 * dt)); + this.closeness += (closeness - this.closeness) * (1 - Math.exp(-3 * dt)); + } + const narrow = THREE.MathUtils.clamp(aspect, FIGHTER_CAM_NARROW_ORBIT, 1); + fighterCamFrame(viewer, opponent, this.forwardX, this.forwardZ, this.target, this.aim, THREE.MathUtils.lerp(FIGHTER_CAM_ORBIT, FIGHTER_CAM_CLOSE_ORBIT, this.closeness) * narrow); + if (!this.initialised) { + this.position.copy(this.target); + this.look.copy(this.aim); + this.initialised = true; + } else { + const follow = 1 - Math.exp(-10 * dt); + this.position.lerp(this.target, follow); + this.look.lerp(this.aim, follow); + } + const frame = { position: this.position, lookAt: this.look, tight: true as const }; + if (reducedMotion || shake <= 0.0005) return frame; + const t = time * 61; + this.target.set(this.position.x + Math.sin(t * 1.31) * shake, this.position.y + Math.cos(t * 1.97) * shake * 0.6, this.position.z); + this.aim.set(this.look.x + Math.sin(t * 1.53) * shake * 0.5, this.look.y + Math.cos(t * 2.11) * shake * 0.35, this.look.z); + return { position: this.target, lookAt: this.aim, tight: true }; + } +} diff --git a/web/hands/src/render/canvas-blood.test.ts b/web/hands/src/render/canvas-blood.test.ts new file mode 100644 index 00000000..0efdfdbd --- /dev/null +++ b/web/hands/src/render/canvas-blood.test.ts @@ -0,0 +1,479 @@ +import * as THREE from "three"; +import { describe, expect, it, vi } from "vitest"; +import boutJson from "../test/gore-bout.json"; +import type { CombatEvent } from "../types"; +import { CANVAS_BLOOD_UPLOAD_INTERVAL, CanvasBlood, splatRecipe } from "./canvas-blood"; +import { Effects3D } from "./effects"; +import { teethFor } from "./gore"; +import { mouthpieceFlies } from "./mouthpiece"; +import { poolRadius } from "./renderer"; +import { CANVAS_TOP, RING_FIGHT_HALF, worldMapping } from "./world"; + +interface Shape { x: number; y: number; rx: number; ry: number; turn: number } + +/** + * A 2D context that paints in software, alpha only, the shapes a blood stamp is drawn with: ellipses + * and circles, filled (non-zero, so the shapes of one path fill as their union) or stroked, over what + * is there or cutting it out. + */ +class PaintedAlpha { + readonly alpha: Float32Array; + globalAlpha = 1; + globalCompositeOperation = "source-over"; + lineWidth = 1; + fillStyle: unknown = ""; + strokeStyle: unknown = ""; + private path: Shape[] = []; + + constructor(readonly width: number, readonly height: number) { + this.alpha = new Float32Array(width * height); + } + + static inside(shape: Shape, x: number, y: number, grow: number): boolean { + const dx = x - shape.x; + const dy = y - shape.y; + const u = dx * Math.cos(shape.turn) + dy * Math.sin(shape.turn); + const v = -dx * Math.sin(shape.turn) + dy * Math.cos(shape.turn); + const rx = shape.rx + grow; + const ry = shape.ry + grow; + return rx > 0 && ry > 0 && (u * u) / (rx * rx) + (v * v) / (ry * ry) <= 1; + } + + clearRect(): void { this.alpha.fill(0); } + beginPath(): void { this.path = []; } + moveTo(): void {} + ellipse(x: number, y: number, rx: number, ry: number, turn: number): void { this.path.push({ x, y, rx, ry, turn }); } + arc(x: number, y: number, radius: number): void { this.path.push({ x, y, rx: radius, ry: radius, turn: 0 }); } + fill(): void { this.paint(0, (shape, x, y) => PaintedAlpha.inside(shape, x, y, 0)); } + stroke(): void { + const half = this.lineWidth / 2; + this.paint(half, (shape, x, y) => PaintedAlpha.inside(shape, x, y, half) && !PaintedAlpha.inside(shape, x, y, -half)); + } + save(): void {} + restore(): void {} + translate(): void {} + rotate(): void {} + drawImage(): void {} + fillRect(): void {} + createRadialGradient(): { addColorStop(): void } { return { addColorStop: () => {} }; } + + /** Four by four samples a pixel over the path's bounds, composited at the global alpha. */ + private paint(grow: number, covers: (shape: Shape, x: number, y: number) => boolean): void { + const reach = (shape: Shape): number => Math.max(shape.rx, shape.ry) + grow + 1; + const left = Math.max(0, Math.floor(Math.min(...this.path.map((shape) => shape.x - reach(shape))))); + const right = Math.min(this.width, Math.ceil(Math.max(...this.path.map((shape) => shape.x + reach(shape))))); + const top = Math.max(0, Math.floor(Math.min(...this.path.map((shape) => shape.y - reach(shape))))); + const bottom = Math.min(this.height, Math.ceil(Math.max(...this.path.map((shape) => shape.y + reach(shape))))); + for (let y = top; y < bottom; y += 1) { + for (let x = left; x < right; x += 1) { + let hits = 0; + for (let sample = 0; sample < 16; sample += 1) { + const px = x + ((sample % 4) + 0.5) / 4; + const py = y + (Math.floor(sample / 4) + 0.5) / 4; + if (this.path.some((shape) => covers(shape, px, py))) hits += 1; + } + const paint = (hits / 16) * this.globalAlpha; + const index = y * this.width + x; + this.alpha[index] = this.globalCompositeOperation === "destination-out" ? this.alpha[index]! * (1 - paint) : this.alpha[index]! + paint * (1 - this.alpha[index]!); + } + } + } +} + +/** Paints every canvas made inside `make` in software, and hands back what each one holds. */ +function paintedCanvases(make: () => T): { made: T; painted: Map } { + const painted = new Map(); + const getContext = vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(function (this: HTMLCanvasElement) { + const context = painted.get(this) ?? new PaintedAlpha(this.width, this.height); + painted.set(this, context); + return context as unknown as CanvasRenderingContext2D; + }); + const made = make(); + getContext.mockRestore(); + return { made, painted }; +} + +/** A real bout as the engine played it; `about` in the file says how it was recorded and how it reads. */ +interface RecordedBout { + readonly sampleTicks: number; + readonly rounds: readonly number[]; + readonly samples: readonly (readonly number[])[]; + readonly events: readonly (readonly [number, string, number, number, number, string, number])[]; +} +const BOUT = boutJson as unknown as RecordedBout; +const BOUT_KINDS: Readonly> = { h: "hit", c: "counter_hit", b: "block", p: "perfect_block", k: "knockdown", l: "bleed" }; +const BOUT_PUNCHES: Readonly> = { j: "jab", s: "straight", h: "hook", u: "uppercut" }; + +/** A stain as it was painted, or the dark middle of a pool. */ +interface PaintedStain { x: number; z: number; width: number; depth: number; turn: number; opacity: number; stamp: number; pool?: number } + +/** + * Plays a real bout's blood through the effects as the renderer does: its punches, blocks, knockdowns + * and bleeding as they land, a cut fighter's drip and a downed one's pool. Records every stain, how + * many there were at the end of each round, and where each pool was spilled. + */ +function playBout(): { stains: PaintedStain[]; roundEnds: number[]; pools: THREE.Vector2[] } { + const mapping = worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }); + const effects = new Effects3D(new THREE.Scene(), 1024); + const stains: PaintedStain[] = []; + const pools: THREE.Vector2[] = []; + vi.spyOn(CanvasBlood.prototype, "stain").mockImplementation((x, z, width, depth, turn, opacity, _color, stamp) => { + if (Math.abs(x) <= RING_FIGHT_HALF && Math.abs(z) <= RING_FIGHT_HALF) stains.push({ x, z, width, depth, turn, opacity, stamp: stamp ?? stains.length }); + }); + const pool = CanvasBlood.prototype.pool; + vi.spyOn(CanvasBlood.prototype, "pool").mockImplementation(function (this: CanvasBlood, x, z, radius, opacity, stamp) { + pool.call(this, x, z, radius, opacity, stamp); + stains.push({ x, z, width: 0, depth: 0, turn: stamp * 0.9, opacity, stamp, pool: radius }); + pools.push(new THREE.Vector2(x, z)); + }); + const fighter = (tick: number, index: number): { x: number; z: number; severity: number; down: boolean } => { + const at = Math.min(BOUT.samples.length - 1.001, tick / BOUT.sampleTicks); + const a = BOUT.samples[Math.floor(at)]!; + const b = BOUT.samples[Math.floor(at) + 1] ?? a; + const along = at - Math.floor(at); + const lerp = (column: number): number => a[column]! + (b[column]! - a[column]!) * along; + // The renderer's drip and pool measure a fighter's bleeding and both cuts against 380. + return { x: mapping.x(lerp(index * 2)), z: mapping.z(lerp(index * 2 + 1)), severity: a[4 + index]! / 380, down: (a[6]! & (index + 1)) !== 0 }; + }; + const roundEnds: number[] = []; + const pooled = [0, 0]; + const spills = [0, 0]; + const head = new THREE.Vector3(); + const target = new THREE.Vector3(); + const endTick = (BOUT.samples.length - 1) * BOUT.sampleTicks; + let next = 0; + for (let frame = 0; frame / 2 <= endTick; frame += 1) { + const tick = frame / 2; + if (BOUT.rounds.slice(1).includes(tick)) roundEnds.push(stains.length); + for (const index of [0, 1]) { + const self = fighter(tick, index); + if (self.severity > 0.05 && !self.down) { + pooled[index] = 0; + spills[index] = 0; + effects.drip(head.set(self.x, 1.56, self.z), self.severity, false, index); + } else if (self.severity > 0.2 && self.down) { + // Two spills a second from under his head, which lies away from the man who put him down. + effects.stopDrip(index); + pooled[index]! += 2 / 60; + const other = fighter(tick, 1 - index); + const apart = Math.hypot(self.x - other.x, self.z - other.z) || 1; + while (pooled[index]! >= 1) { + pooled[index]! -= 1; + const count = spills[index]!; + spills[index] = count + 1; + const spread = poolRadius(count, self.severity); + const angle = count * 2.399_963 + index * Math.PI; + effects.pool(self.x + ((self.x - other.x) / apart) * 0.6 + Math.sin(angle) * spread * 0.25, self.z + ((self.z - other.z) / apart) * 0.6 + Math.cos(angle) * spread * 0.25, spread, count + index * 7); + } + } else { + effects.stopDrip(index); + pooled[index] = 0; + } + } + for (; next < BOUT.events.length && BOUT.events[next]![0] <= tick; next += 1) { + const [at, code, recipient, amount, blood, punch, direction] = BOUT.events[next]!; + const kind = BOUT_KINDS[code]!; + const hurt = fighter(at, recipient); + const puncher = fighter(at, 1 - recipient); + const apart = Math.hypot(hurt.x - puncher.x, hurt.z - puncher.z); + const spray = kind === "bleed" || apart < 1e-3 ? undefined : { x: (hurt.x - puncher.x) / apart, z: (hurt.z - puncher.z) / apart }; + const detail = punch.length === 2 ? `${BOUT_PUNCHES[punch[0]!]}:${punch[1] === "b" ? "body" : "head"}` : ""; + const event: CombatEvent = { event_id: next + 1, tick: at, kind, actor_id: null, target_id: null, amount, detail, blood, direction, action_id: null }; + effects.addEvent(event, target.set(hurt.x, 0, hurt.z), false, spray); + const teeth = teethFor(kind, amount, detail.endsWith(":head")); + if (teeth > 0) effects.spawnTeeth(head.set(hurt.x, 1.49, hurt.z), spray ?? direction, teeth, event.event_id); + if (mouthpieceFlies(kind, amount, detail.endsWith(":head"))) effects.ejectMouthpiece(recipient, head.set(hurt.x, 1.48, hurt.z), new THREE.Quaternion(), spray ?? direction, event.event_id, 0x1d4ed8); + } + effects.update(1 / 60); + } + roundEnds.push(stains.length); + vi.restoreAllMocks(); + effects.dispose(); + return { stains, roundEnds, pools }; +} + +/** The canvas a bout leaves, a centimetre a cell. */ +const CELL = 0.01; +const CELLS = Math.round((RING_FIGHT_HALF * 2) / CELL); + +/** Composites stains into `canvas` (alpha a cell) over what is there, as the blood canvas paints them. */ +function composite(canvas: Float32Array, stains: readonly PaintedStain[], stamps: readonly Float32Array[]): void { + for (const stain of stains) { + const reach = stain.pool === undefined ? Math.hypot(stain.width, stain.depth) / 2 : stain.pool * 1.15; + const cos = Math.cos(stain.turn); + const sin = Math.sin(stain.turn); + for (let row = Math.max(0, Math.floor((stain.z - reach + RING_FIGHT_HALF) / CELL)); row < Math.min(CELLS, (stain.z + reach + RING_FIGHT_HALF) / CELL); row += 1) { + for (let column = Math.max(0, Math.floor((stain.x - reach + RING_FIGHT_HALF) / CELL)); column < Math.min(CELLS, (stain.x + reach + RING_FIGHT_HALF) / CELL); column += 1) { + const dx = (column + 0.5) * CELL - RING_FIGHT_HALF - stain.x; + const dz = (row + 0.5) * CELL - RING_FIGHT_HALF - stain.z; + const u = dx * cos + dz * sin; + const v = -dx * sin + dz * cos; + let alpha: number; + if (stain.pool !== undefined) { + // The pool's middle: 0.9 at the centre, 0.65 two thirds of the way out and nothing at its edge. + const out = Math.hypot(u, v / 0.86) / reach; + if (out > 1) continue; + alpha = stain.opacity * (out < 0.65 ? 0.9 - (0.25 * out) / 0.65 : (0.65 * (1 - out)) / 0.35); + } else { + const x = Math.floor((u / stain.width + 0.5) * 128); + const y = Math.floor((v / stain.depth + 0.5) * 128); + if (x < 0 || y < 0 || x >= 128 || y >= 128) continue; + alpha = stamps[stain.stamp % stamps.length]![y * 128 + x]! * stain.opacity; + } + const cell = row * CELLS + column; + canvas[cell] = canvas[cell]! + alpha * (1 - canvas[cell]!); + } + } + } +} + +describe("blood on the canvas", () => { + it("is painted over the whole canvas the fighters stand on, the ring's corners at the texture's", () => { + const blood = new CanvasBlood(new THREE.Scene(), 512); + expect(blood.pixel(-RING_FIGHT_HALF, -RING_FIGHT_HALF)).toEqual({ x: 0, y: 0 }); + expect(blood.pixel(RING_FIGHT_HALF, RING_FIGHT_HALF)).toEqual({ x: 512, y: 512 }); + expect(blood.pixel(0, 0)).toEqual({ x: 256, y: 256 }); + expect(blood.mesh.position.y).toBeGreaterThan(CANVAS_TOP); + expect(blood.mesh.position.y).toBeLessThan(CANVAS_TOP + 0.01); + blood.dispose(); + }); + + it("soaks in as a lumpy blot, darker where it dries at the rim than in the middle, never a flat disc", () => { + for (let seed = 0; seed < 8; seed += 1) { + const recipe = splatRecipe(0x3a1f_00d1 + seed * 977); + expect(recipe.lobes.length).toBeGreaterThanOrEqual(5); + const sizes = recipe.lobes.map((lobe) => lobe.rx); + expect(Math.max(...sizes) - Math.min(...sizes)).toBeGreaterThan(2); + expect(recipe.lobes.some((lobe) => Math.hypot(lobe.x - 64, lobe.y - 64) > 5)).toBe(true); + expect(recipe.rim).toBeGreaterThan(recipe.fill + 0.15); + } + expect(splatRecipe(7)).toEqual(splatRecipe(7)); + }); + + it("is painted lighter in the middle than at the rim, however many of its lobes overlap there", () => { + const { made: blood, painted } = paintedCanvases(() => new CanvasBlood(new THREE.Scene(), 256)); + const stamps = (blood as unknown as { stamps: HTMLCanvasElement[] }).stamps; + expect(stamps).toHaveLength(4); + for (const [index, stamp] of stamps.entries()) { + const recipe = splatRecipe(0x3a1f_00d1 + index * 977); + const alpha = painted.get(stamp)!.alpha; + const within = (x: number, y: number, grow: number): boolean => recipe.lobes.some((lobe) => PaintedAlpha.inside(lobe, x, y, grow)); + let centre = 0; + let centreCount = 0; + let rim = 0; + let rimCount = 0; + for (let y = 0; y < stamp.height; y += 1) { + for (let x = 0; x < stamp.width; x += 1) { + const a = alpha[y * stamp.width + x]!; + if (Math.hypot(x + 0.5 - 64, y + 0.5 - 64) <= 6) { + centre += a; + centreCount += 1; + } else if (within(x + 0.5, y + 0.5, 1.75) && !within(x + 0.5, y + 0.5, -1.5)) { + rim += a; + rimCount += 1; + } + } + } + // Where four or five lobes lie over each other the middle is as light as anywhere inside the blot. + expect(centre / centreCount).toBeCloseTo(recipe.fill, 2); + expect(centre / centreCount).toBeLessThan(rim / rimCount - 0.15); + } + blood.dispose(); + }); + + it("is matte, as blood soaked into canvas is, so the ring lights do not glaze it lavender", () => { + const blood = new CanvasBlood(new THREE.Scene(), 256); + expect((blood.mesh.material as THREE.MeshStandardMaterial).roughness).toBeGreaterThanOrEqual(0.75); + blood.dispose(); + }); + + it("keeps every stain for the bout, where the old decals recycled after 48", () => { + const blood = new CanvasBlood(new THREE.Scene()); + for (let index = 0; index < 500; index += 1) blood.stain((index % 50) * 0.1 - 2.5, 0.3, 0.1, 0.08, index, 0.6, 0x6e0d13); + expect(blood.stains).toBe(500); + blood.stain(RING_FIGHT_HALF + 0.2, 0, 0.1, 0.1, 0, 0.5, 0x6e0d13); + blood.stain(0, Number.NaN, 0.1, 0.1, 0, 0.5, 0x6e0d13); + expect(blood.stains).toBe(500); + blood.dispose(); + }); + + it("sends what it has painted to the GPU at most every eighth of a second", () => { + const blood = new CanvasBlood(new THREE.Scene()); + const texture = (blood.mesh.material as THREE.MeshStandardMaterial).map!; + blood.update(1); + const version = texture.version; + blood.stain(0, 0, 0.2, 0.2, 0, 0.8, 0x6e0d13); + expect(blood.uploadPending).toBe(true); + blood.update(1); + expect(texture.version).toBe(version + 1); + blood.stain(0.1, 0, 0.2, 0.2, 0, 0.8, 0x6e0d13); + blood.update(CANVAS_BLOOD_UPLOAD_INTERVAL / 2); + expect(blood.uploadPending).toBe(true); + expect(texture.version).toBe(version + 1); + blood.update(CANVAS_BLOOD_UPLOAD_INTERVAL / 2); + expect(blood.uploadPending).toBe(false); + expect(texture.version).toBe(version + 2); + blood.update(1); + expect(texture.version).toBe(version + 2); + blood.dispose(); + }); + + it("sends only the part painted since the last upload once the renderer can take a region", () => { + const blood = new CanvasBlood(new THREE.Scene(), 1024); + const texture = (blood.mesh.material as THREE.MeshStandardMaterial).map!; + const sent: { source: THREE.Texture; destination: THREE.Texture; min: THREE.Vector2; max: THREE.Vector2; at: THREE.Vector2 }[] = []; + const uploader = { + initTexture: vi.fn(), + copyTextureToTexture: vi.fn((source: THREE.Texture, destination: THREE.Texture, region: THREE.Box2, at: THREE.Vector2) => { + sent.push({ source, destination, min: region.min.clone(), max: region.max.clone(), at: at.clone() }); + }), + }; + blood.useUploader(uploader); + expect(uploader.initTexture).toHaveBeenCalledExactlyOnceWith(texture); + const version = texture.version; + blood.stain(0, 0, 0.2, 0.2, 0, 0.8, 0x6e0d13); + blood.update(1); + expect(sent).toHaveLength(1); + const [first] = sent; + expect(first!.destination).toBe(texture); + expect(first!.source).not.toBe(texture); + expect(first!.source.image).toBe(texture.image); + expect(first!.at).toEqual(first!.min); + const centre = blood.pixel(0, 0); + expect(first!.min.x).toBeLessThan(centre.x); + expect(first!.min.y).toBeLessThan(centre.y); + expect(first!.max.x).toBeGreaterThan(centre.x); + expect(first!.max.y).toBeGreaterThan(centre.y); + expect((first!.max.x - first!.min.x) * (first!.max.y - first!.min.y)).toBeLessThan(1024 * 1024 * 0.01); + expect(texture.version).toBe(version); + + blood.stain(RING_FIGHT_HALF - 0.01, -RING_FIGHT_HALF + 0.01, 0.3, 0.3, 1, 0.8, 0x6e0d13); + blood.update(1); + const [, edge] = sent; + expect(edge!.min.x).toBeGreaterThan(centre.x); + expect(edge!.max.y).toBeLessThan(centre.y); + expect(edge!.max.x).toBe(1024); + expect(edge!.min.y).toBe(0); + expect(Number.isInteger(edge!.min.x) && Number.isInteger(edge!.max.y)).toBe(true); + + blood.update(1); + expect(sent).toHaveLength(2); + blood.clear(); + expect(texture.version).toBe(version + 1); + blood.pool(-1, 1, 0.3, 0.9, 2); + blood.update(1); + expect(sent).toHaveLength(3); + expect(sent[2]!.max.x).toBeLessThan(centre.x); + expect(sent[2]!.min.y).toBeGreaterThan(centre.y); + blood.dispose(); + }); + + it("sends half as often on a struggling client, and as often again once it recovers", () => { + const blood = new CanvasBlood(new THREE.Scene(), 256); + const texture = (blood.mesh.material as THREE.MeshStandardMaterial).map!; + blood.setLowTier(true); + const version = texture.version; + blood.stain(0, 0, 0.2, 0.2, 0, 0.8, 0x6e0d13); + blood.update(CANVAS_BLOOD_UPLOAD_INTERVAL * 1.5); + expect(texture.version).toBe(version); + blood.update(CANVAS_BLOOD_UPLOAD_INTERVAL * 0.6); + expect(texture.version).toBe(version + 1); + blood.setLowTier(false); + blood.stain(0, 0, 0.2, 0.2, 0, 0.8, 0x6e0d13); + blood.update(CANVAS_BLOOD_UPLOAD_INTERVAL * 1.05); + expect(texture.version).toBe(version + 2); + blood.dispose(); + }); + + it("lies over the ring the same way round as it is painted", () => { + const blood = new CanvasBlood(new THREE.Scene(), 512); + const texture = (blood.mesh.material as THREE.MeshStandardMaterial).map!; + expect(texture.flipY).toBe(false); + blood.mesh.updateMatrixWorld(true); + const position = blood.mesh.geometry.getAttribute("position"); + const uv = blood.mesh.geometry.getAttribute("uv"); + const corner = new THREE.Vector3(); + for (let index = 0; index < position.count; index += 1) { + corner.fromBufferAttribute(position, index).applyMatrix4(blood.mesh.matrixWorld); + const painted = blood.pixel(corner.x, corner.z); + expect(uv.getX(index)).toBeCloseTo(painted.x / 512, 6); + expect(uv.getY(index)).toBeCloseTo(painted.y / 512, 6); + } + blood.dispose(); + }); + + it("is hidden until blood lands and when the canvas is cleaned, and leaves nothing behind", () => { + const scene = new THREE.Scene(); + const blood = new CanvasBlood(scene); + expect(blood.mesh.visible).toBe(false); + blood.pool(0.4, -0.2, 0.3, 0.9, 2); + expect(blood.mesh.visible).toBe(true); + expect(blood.stains).toBe(1); + blood.clear(); + expect(blood.mesh.visible).toBe(false); + expect(blood.stains).toBe(0); + const material = blood.mesh.material as THREE.MeshStandardMaterial; + const disposed = [vi.spyOn(material, "dispose"), vi.spyOn(material.map!, "dispose"), vi.spyOn(blood.mesh.geometry, "dispose")]; + blood.dispose(); + for (const spy of disposed) expect(spy).toHaveBeenCalledOnce(); + expect(scene.children).toHaveLength(0); + }); + + it("pools under a fighter on the canvas, smaller with reduced blood and never with blood off", () => { + const pooled = (level: "full" | "reduced" | "off"): number => { + const effects = new Effects3D(new THREE.Scene(), 256); + effects.setBloodLevel(level); + effects.pool(0.5, 0.5, 0.3, 1); + const stains = effects.canvasStains; + effects.dispose(); + return stains; + }; + expect(pooled("full")).toBe(1); + expect(pooled("reduced")).toBe(1); + expect(pooled("off")).toBe(0); + }); + + it("gets bloodier round by round through a real bout, where it once painted the ring solid in the first", () => { + const { made: blood, painted } = paintedCanvases(() => new CanvasBlood(new THREE.Scene(), 256)); + const stamps = (blood as unknown as { stamps: HTMLCanvasElement[] }).stamps.map((stamp) => painted.get(stamp)!.alpha); + blood.dispose(); + const { stains, roundEnds, pools } = playBout(); + expect(roundEnds).toHaveLength(3); + const canvas = new Float32Array(CELLS * CELLS); + /** Square metres of the canvas at least this dark. */ + const area = (alpha: number): number => canvas.filter((cell) => cell >= alpha).length * CELL * CELL; + const after: { stained: number; soaked: number }[] = []; + for (const [round, end] of roundEnds.entries()) { + composite(canvas, stains.slice(round === 0 ? 0 : roundEnds[round - 1], end), stamps); + after.push({ stained: area(0.25), soaked: area(0.8) }); + if (round > 0) continue; + // The knockdown late in round 1 leaves a pool soaked through where it started. + const first = pools[0]!; + let darkest = 0; + for (let cell = 0; cell < canvas.length; cell += 1) { + const x = ((cell % CELLS) + 0.5) * CELL - RING_FIGHT_HALF; + const z = (Math.floor(cell / CELLS) + 0.5) * CELL - RING_FIGHT_HALF; + if (Math.hypot(x - first.x, z - first.y) <= 0.1) darkest = Math.max(darkest, canvas[cell]!); + } + expect(darkest).toBeGreaterThan(0.9); + } + const [one, two, three] = after as [{ stained: number; soaked: number }, { stained: number; soaked: number }, { stained: number; soaked: number }]; + // A cut opens 21 s into round 1: by the bell a couple of square metres of the 37 show blood, and + // little but the knockdown's pool is soaked through, where the old pacing painted 4,176 stains. + expect(roundEnds[0]).toBeLessThan(1500); + expect(one.stained).toBeLessThan(4); + expect(one.soaked).toBeLessThan(0.5); + // Every round leaves more, the worst of the cut in round 2 most of all. + expect(two.stained).toBeGreaterThan(one.stained * 2); + expect(three.stained).toBeGreaterThan(one.stained * 3); + expect(three.soaked).toBeGreaterThan(one.soaked * 3); + }, 60_000); + + it("spreads fast at first and then slower, wider the worse the fighter bleeds, to under half a metre", () => { + expect(poolRadius(0, 1)).toBeCloseTo(0.05, 5); + expect(poolRadius(4, 1) - poolRadius(0, 1)).toBeGreaterThan(poolRadius(16, 1) - poolRadius(12, 1)); + expect(poolRadius(16, 1.4)).toBeGreaterThan(poolRadius(16, 0.5)); + expect(poolRadius(20, 1)).toBeGreaterThan(0.2); + expect(poolRadius(10_000, 10)).toBe(0.42); + }); +}); diff --git a/web/hands/src/render/canvas-blood.ts b/web/hands/src/render/canvas-blood.ts new file mode 100644 index 00000000..d75eceea --- /dev/null +++ b/web/hands/src/render/canvas-blood.ts @@ -0,0 +1,270 @@ +import * as THREE from "three"; +import { CANVAS_TOP, RING_FIGHT_HALF } from "./world"; + +const STAMP_SIZE = 128; +const STAMP_COUNT = 4; +/** Seconds between uploads of the painted canvas to the GPU while blood keeps landing. */ +export const CANVAS_BLOOD_UPLOAD_INTERVAL = 0.12; +export const CANVAS_BLOOD_ROUGHNESS = 0.82; + +const seeded = (seed: number): (() => number) => () => { + seed = Math.imul(seed ^ (seed >>> 15), 1 | seed); + seed ^= seed + Math.imul(seed ^ (seed >>> 7), 61 | seed); + return ((seed ^ (seed >>> 14)) >>> 0) / 4294967296; +}; + +/** + * An irregular splash, white on clear: a lumpy main blot thinner in the middle than at its rim, where + * soaked-in blood dries darkest, satellite blots of their own weight and flecks thrown clear of it. + */ +export function splatRecipe(seed: number): { lobes: { x: number; y: number; rx: number; ry: number; turn: number }[]; fill: number; rim: number; satellites: number } { + const rand = seeded(seed); + const lobes = Array.from({ length: 5 + Math.floor(rand() * 4) }, (_, index) => { + const angle = rand() * Math.PI * 2; + const reach = index === 0 ? 0 : 6 + rand() * 12; + const radius = index === 0 ? 16 + rand() * 6 : 8 + rand() * 9; + return { x: 64 + Math.cos(angle) * reach, y: 64 + Math.sin(angle) * reach, rx: radius, ry: radius * (0.55 + rand() * 0.45), turn: rand() * Math.PI }; + }); + return { lobes, fill: 0.55 + rand() * 0.2, rim: 0.95, satellites: 6 + Math.floor(rand() * 6) }; +} + +function splatStamp(seed: number): HTMLCanvasElement { + const canvas = document.createElement("canvas"); + canvas.width = STAMP_SIZE; + canvas.height = STAMP_SIZE; + const ctx = canvas.getContext("2d"); + if (ctx !== null) { + const recipe = splatRecipe(seed); + const rand = seeded(seed ^ 0x5bd1e995); + ctx.clearRect(0, 0, STAMP_SIZE, STAMP_SIZE); + ctx.fillStyle = "rgba(255,255,255,1)"; + ctx.strokeStyle = "rgba(255,255,255,1)"; + // The rim first, as a ragged band round every lobe, then the thinner middle over it. + ctx.globalAlpha = recipe.rim; + ctx.lineWidth = 3.5; + for (const lobe of recipe.lobes) { + ctx.beginPath(); + ctx.ellipse(lobe.x, lobe.y, lobe.rx, lobe.ry, lobe.turn, 0, Math.PI * 2); + ctx.stroke(); + } + // The middle is the lobes' union, cleared and filled once: filled lobe by lobe, it darkened where + // they overlap, which is the middle, until it was as dark as the rim. + const middle = (): void => { + ctx.beginPath(); + for (const lobe of recipe.lobes) { + const rx = Math.max(1, lobe.rx - 1.5); + ctx.moveTo(lobe.x + Math.cos(lobe.turn) * rx, lobe.y + Math.sin(lobe.turn) * rx); + ctx.ellipse(lobe.x, lobe.y, rx, Math.max(1, lobe.ry - 1.5), lobe.turn, 0, Math.PI * 2); + } + ctx.fill(); + }; + ctx.globalCompositeOperation = "destination-out"; + ctx.globalAlpha = 1; + middle(); + ctx.globalCompositeOperation = "source-over"; + ctx.globalAlpha = recipe.fill; + middle(); + for (let i = 0; i < recipe.satellites; i += 1) { + const angle = rand() * Math.PI * 2; + const distance = 26 + rand() * 22; + const radius = 2.5 + rand() * 7; + ctx.globalAlpha = 0.5 + rand() * 0.45; + ctx.beginPath(); + ctx.ellipse(64 + Math.cos(angle) * distance, 64 + Math.sin(angle) * distance, radius, radius * (0.55 + rand() * 0.5), rand() * Math.PI, 0, Math.PI * 2); + ctx.fill(); + } + ctx.globalAlpha = 1; + for (let i = 0; i < 12; i += 1) { + const angle = rand() * Math.PI * 2; + const distance = 30 + rand() * 30; + ctx.beginPath(); + ctx.arc(64 + Math.cos(angle) * distance, 64 + Math.sin(angle) * distance, 1 + rand() * 3, 0, Math.PI * 2); + ctx.fill(); + } + } + return canvas; +} + +/** Blood soaks into the canvas darker than it flies: stains are drawn at four fifths of their colour. */ +const css = (color: number): string => { + const shade = (shift: number): number => Math.round(((color >> shift) & 0xff) * 0.8); + return `rgb(${shade(16)},${shade(8)},${shade(0)})`; +}; + +/** The part of the renderer that sends a painted region to the GPU without the rest of the texture. */ +export interface RegionUploader { + initTexture(texture: THREE.Texture): void; + copyTextureToTexture(source: THREE.Texture, destination: THREE.Texture, region: THREE.Box2, position: THREE.Vector2): void; +} + +/** + * Blood on the canvas, painted into one texture that lies over the ring: a stain stays where it fell + * for the whole bout, it darkens where stains overlap, and any number of them cost one draw. Only the + * part painted since the last upload goes to the GPU. + */ +export class CanvasBlood { + readonly mesh: THREE.Mesh; + private readonly canvas: HTMLCanvasElement; + private readonly context: CanvasRenderingContext2D | null; + private readonly texture: THREE.CanvasTexture; + /** The same canvas as an upload source the renderer never draws, so copying from it reads the painted pixels. */ + private readonly source: THREE.Texture; + private uploader: RegionUploader | null = null; + private readonly region = new THREE.Box2(); + private readonly regionAt = new THREE.Vector2(); + private readonly material: THREE.MeshStandardMaterial; + private readonly geometry: THREE.PlaneGeometry; + private readonly stamps: HTMLCanvasElement[] = []; + private readonly scratch: HTMLCanvasElement; + private readonly scratchContext: CanvasRenderingContext2D | null; + private readonly pixelsPerMetre: number; + private dirty = false; + private sinceUpload = 0; + private uploadInterval = CANVAS_BLOOD_UPLOAD_INTERVAL; + private painted = 0; + + constructor(private readonly scene: THREE.Scene, readonly size = 1024) { + this.canvas = document.createElement("canvas"); + this.canvas.width = size; + this.canvas.height = size; + this.context = this.canvas.getContext("2d"); + this.context?.clearRect(0, 0, size, size); + this.scratch = document.createElement("canvas"); + this.scratch.width = STAMP_SIZE; + this.scratch.height = STAMP_SIZE; + this.scratchContext = this.scratch.getContext("2d"); + for (let index = 0; index < STAMP_COUNT; index += 1) this.stamps.push(splatStamp(0x3a1f_00d1 + index * 977)); + this.pixelsPerMetre = size / (RING_FIGHT_HALF * 2); + this.texture = new THREE.CanvasTexture(this.canvas); + this.texture.colorSpace = THREE.SRGBColorSpace; + this.texture.anisotropy = 4; + // Rows go up the canvas as they go down the image, so a region copies without flipping; the plane's + // texture coordinates turn the other way to match. + this.texture.flipY = false; + this.source = new THREE.Texture(this.canvas); + this.region.makeEmpty(); + // Blood soaks into canvas: a glossy coat picked up the ring lights' blue and turned the stains lavender. + this.material = new THREE.MeshStandardMaterial({ map: this.texture, transparent: true, depthWrite: false, roughness: CANVAS_BLOOD_ROUGHNESS, metalness: 0, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 }); + this.geometry = new THREE.PlaneGeometry(RING_FIGHT_HALF * 2, RING_FIGHT_HALF * 2); + const uv = this.geometry.getAttribute("uv"); + for (let index = 0; index < uv.count; index += 1) uv.setY(index, 1 - uv.getY(index)); + this.mesh = new THREE.Mesh(this.geometry, this.material); + this.mesh.rotation.x = -Math.PI / 2; + this.mesh.position.y = CANVAS_TOP + 0.003; + this.mesh.receiveShadow = true; + this.mesh.visible = false; + scene.add(this.mesh); + } + + /** Stains painted since the canvas was last cleaned. */ + get stains(): number { + return this.painted; + } + + get uploadPending(): boolean { + return this.dirty; + } + + /** Lets the canvas send only what has been painted; without it every upload is the whole texture. */ + useUploader(uploader: RegionUploader | null): void { + this.uploader = uploader; + uploader?.initTexture(this.texture); + } + + /** A struggling client sends the canvas half as often. */ + setLowTier(low: boolean): void { + this.uploadInterval = low ? CANVAS_BLOOD_UPLOAD_INTERVAL * 2 : CANVAS_BLOOD_UPLOAD_INTERVAL; + } + + private grow(x: number, y: number, radius: number): void { + this.region.expandByPoint(this.regionAt.set(x - radius - 1, y - radius - 1)); + this.region.expandByPoint(this.regionAt.set(x + radius + 1, y + radius + 1)); + } + + /** Where a point of the ring lands on the painted canvas, in pixels. */ + pixel(x: number, z: number): { x: number; y: number } { + return { x: (x + RING_FIGHT_HALF) * this.pixelsPerMetre, y: (z + RING_FIGHT_HALF) * this.pixelsPerMetre }; + } + + /** A splash `width` by `depth` metres, turned by `rotation`, of the given colour and opacity. Blood off the canvas is lost. */ + stain(x: number, z: number, width: number, depth: number, rotation: number, opacity: number, color: number, stamp = this.painted): void { + if (!Number.isFinite(x) || !Number.isFinite(z) || Math.abs(x) > RING_FIGHT_HALF || Math.abs(z) > RING_FIGHT_HALF) return; + const ctx = this.context; + const tint = this.scratchContext; + this.painted += 1; + this.dirty = true; + this.mesh.visible = true; + if (ctx === null || tint === null) return; + tint.globalCompositeOperation = "copy"; + tint.drawImage(this.stamps[Math.abs(Math.trunc(stamp)) % STAMP_COUNT]!, 0, 0); + tint.globalCompositeOperation = "source-in"; + tint.fillStyle = css(color); + tint.fillRect(0, 0, STAMP_SIZE, STAMP_SIZE); + tint.globalCompositeOperation = "source-over"; + const at = this.pixel(x, z); + const w = Math.max(1, width * this.pixelsPerMetre); + const h = Math.max(1, depth * this.pixelsPerMetre); + this.grow(at.x, at.y, Math.hypot(w, h) / 2); + ctx.save(); + ctx.globalAlpha = THREE.MathUtils.clamp(opacity, 0, 1); + ctx.translate(at.x, at.y); + ctx.rotate(Number.isFinite(rotation) ? rotation : 0); + ctx.drawImage(this.scratch, -w / 2, -h / 2, w, h); + ctx.restore(); + } + + /** A pool spreading from under a fighter: a ragged edge round a dark, thick middle, `radius` metres across. */ + pool(x: number, z: number, radius: number, opacity: number, stamp: number): void { + this.stain(x, z, radius * 2.3, radius * 2.0, stamp * 1.7, opacity * 0.8, 0x4a070b, stamp); + const ctx = this.context; + if (ctx === null || Math.abs(x) > RING_FIGHT_HALF || Math.abs(z) > RING_FIGHT_HALF) return; + const at = this.pixel(x, z); + const core = Math.max(1, radius * 1.15 * this.pixelsPerMetre); + this.grow(at.x, at.y, core); + const gradient = ctx.createRadialGradient(at.x, at.y, 0, at.x, at.y, core); + gradient.addColorStop(0, "rgba(40,3,6,0.9)"); + gradient.addColorStop(0.65, "rgba(58,5,9,0.65)"); + gradient.addColorStop(1, "rgba(70,7,11,0)"); + ctx.save(); + ctx.globalAlpha = THREE.MathUtils.clamp(opacity, 0, 1); + ctx.fillStyle = gradient; + ctx.beginPath(); + ctx.ellipse(at.x, at.y, core, core * 0.86, stamp * 0.9, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + } + + /** Sends what has been painted to the GPU, no more often than every `CANVAS_BLOOD_UPLOAD_INTERVAL` seconds (twice that at the low tier). */ + update(dt: number): void { + this.sinceUpload += Number.isFinite(dt) ? Math.max(0, dt) : 0; + if (!this.dirty || this.sinceUpload < this.uploadInterval) return; + const region = this.region; + region.min.set(Math.max(0, Math.floor(region.min.x)), Math.max(0, Math.floor(region.min.y))); + region.max.set(Math.min(this.size, Math.ceil(region.max.x)), Math.min(this.size, Math.ceil(region.max.y))); + if (this.uploader !== null && !region.isEmpty() && region.max.x > region.min.x && region.max.y > region.min.y) { + this.uploader.copyTextureToTexture(this.source, this.texture, region, this.regionAt.copy(region.min)); + } else { + this.texture.needsUpdate = true; + } + region.makeEmpty(); + this.dirty = false; + this.sinceUpload = 0; + } + + clear(): void { + this.context?.clearRect(0, 0, this.size, this.size); + this.painted = 0; + this.dirty = false; + this.region.makeEmpty(); + this.texture.needsUpdate = true; + this.mesh.visible = false; + } + + dispose(): void { + this.scene.remove(this.mesh); + this.texture.dispose(); + this.source.dispose(); + this.material.dispose(); + this.geometry.dispose(); + } +} diff --git a/web/hands/src/render/caption.test.ts b/web/hands/src/render/caption.test.ts new file mode 100644 index 00000000..eefde44b --- /dev/null +++ b/web/hands/src/render/caption.test.ts @@ -0,0 +1,393 @@ +import { readFileSync } from "node:fs"; +import { fighter, mockHudContext, publicPlayers, snapshot } from "../test/fixtures"; +import type { MatchPhase } from "../types"; +import { cardMetrics, captionSlot, drawCaption, splitLine, type CaptionScene } from "./caption"; +import type { BroadcastLine, Caption } from "./commentary"; +import { COUNT_BELOW_HEADLINE, drawHud, headlineBaseline, panelHeightFor, topPanelOffset, TOUCH_PADS } from "./hud"; + +const scene = (width: number, height: number, phase: MatchPhase = "fight", extra: Partial = {}): CaptionScene => ({ width, height, phase, resultTop: null, touch: false, hint: false, viewerDown: false, replay: false, ...extra }); +const line = (text: string, extra: Partial = {}): BroadcastLine => ({ speaker: "play", text, priority: 50, urgent: false, hold: 3, card: null, ...extra }); +const shown = (value: BroadcastLine, opacity = 1, entering = 0): Caption => ({ line: value, opacity, entering }); +/** + * The touch pads as Edge lays out style.css: upright one 190 x 274 column 12 px in and 118 px up; on its side the + * modifiers and punch pads, 190 x 196, with the moves 122 x 102 at the foot, 12 px to their left; and on a screen + * 350 px tall or less, 188 x 154 and 120 x 86, 112 px up. + */ +const padRects = (width: number, height: number) => width <= height + ? [{ left: width - 202, right: width - 12, top: height - 392, bottom: height - 118 }] + : height <= 350 + ? [{ left: width - 200, right: width - 12, top: height - 266, bottom: height - 112 }, { left: width - 332, right: width - 212, top: height - 198, bottom: height - 112 }] + : [{ left: width - 202, right: width - 12, top: height - 314, bottom: height - 118 }, { left: width - 336, right: width - 214, top: height - 220, bottom: height - 118 }]; +/** Phones held upright and on their side, from the smallest to the largest. */ +const PHONES = [[320, 568], [360, 640], [375, 667], [360, 740], [375, 812], [390, 844], [412, 915], [568, 320], [600, 330], [640, 320], [640, 360], [667, 375], [740, 360], [812, 375], [844, 390], [915, 412], [932, 430]] as const; + +describe("caption placement", () => { + it("sits in the lower third above the plates on a desktop, and above the controls hint in the countdown", () => { + expect(captionSlot(scene(1280, 720))).toMatchObject({ anchor: "bottom", y: 720 - 110, x: 640, maxWidth: 680, scale: 1 }); + expect(captionSlot(scene(1280, 720, "fight", { hint: true }))).toMatchObject({ anchor: "bottom", y: 720 - 168 }); + expect(captionSlot(scene(1280, 720, "knockdown", { viewerDown: true }))).toMatchObject({ anchor: "bottom" }); + }); + + it("keeps above the fighter's corner panel between rounds", () => { + // The panel's top edge as the browser lays it out at 1280x720: 124 px off the bottom, about 130 px tall. + const slot = captionSlot(scene(1280, 720, "rest", { cornerPanelTop: 466 })); + expect(slot).toMatchObject({ anchor: "bottom" }); + expect(slot!.y).toBeLessThanOrEqual(466 - 8); + expect(captionSlot(scene(1280, 720, "rest", { cornerPanelTop: null }))).toMatchObject({ anchor: "bottom", y: 720 - 110 }); + // A panel reaching too far up for a line above it sends the caption up under the rest panel instead. + const high = captionSlot(scene(1280, 720, "rest", { cornerPanelTop: 340 })); + expect(high === null || (high.anchor === "top" && high.y + 74 <= 340)).toBe(true); + }); + + it("keeps below a big callout on phones", () => { + for (const [width, height] of [[390, 844], [844, 390]] as const) { + const quiet = captionSlot(scene(width, height, "fight", { touch: true }))!; + const slot = captionSlot(scene(width, height, "fight", { touch: true, callout: true })); + // The callout is 46 px type on a baseline 22% down the screen. + const calloutBottom = height * 0.22 + 14; + expect(slot === null || (slot.anchor === "top" && slot.y >= calloutBottom)).toBe(true); + expect(quiet.y).toBeLessThan(calloutBottom); + } + }); + + it("moves to the top on phones, under the round clock in portrait and clear of the touch pads", () => { + expect(captionSlot(scene(390, 844, "fight", { touch: true }))).toMatchObject({ anchor: "top", y: 136, maxWidth: 366, scale: 0.85 }); + expect(captionSlot(scene(844, 390, "fight", { touch: true }))).toMatchObject({ anchor: "top", y: 54, scale: 0.78, maxWidth: 844 - 432 }); + expect(captionSlot(scene(844, 390, "fight"))).toMatchObject({ maxWidth: 680 }); + }); + + it("covers the countdown panel with the introductions on every screen and keeps below the rest panel", () => { + for (const [width, height, touch] of [[390, 844, true], [844, 390, true], [1280, 720, false]] as const) { + const countdown = captionSlot(scene(width, height, "countdown", { touch, hint: true }))!; + expect(countdown.y).toBeCloseTo(height / 2 + topPanelOffset(width, height) - panelHeightFor(height) / 2); + expect(countdown.cover).toEqual({ width: Math.min(320, width - 24), height: panelHeightFor(height) }); + } + const rest = captionSlot(scene(844, 390, "rest", { touch: true }))!; + expect(rest.y).toBeCloseTo(390 / 2 + topPanelOffset(844, 390) + panelHeightFor(390) / 2 + 8); + }); + + it("gives way where nothing would stay clear of the fighters", () => { + expect(captionSlot(scene(844, 390, "knockdown", { touch: true }))).toBeNull(); + expect(captionSlot(scene(390, 844, "knockdown", { touch: true, viewerDown: true }))).toBeNull(); + }); + + it("leaves the slow-connection warning its corner beside the touch pads", () => { + // hud.ts sets it from x 24 on a 76 px baseline: "SLOW CONNECTION 180 ms" in 11 px monospace is about 135 px. + const quiet = captionSlot(scene(932, 430, "fight", { touch: true }))!; + const slow = captionSlot(scene(932, 430, "fight", { touch: true, lag: true }))!; + expect(quiet.x - quiet.maxWidth / 2).toBeLessThan(24 + 135); + expect(slow.x - slow.maxWidth / 2).toBeGreaterThanOrEqual(24 + 160 + 8); + // A phone held upright has no warning on its broadcast, and a caption below it has nothing to leave room for. + expect(captionSlot(scene(390, 844, "fight", { touch: true, lag: true }))).toEqual(captionSlot(scene(390, 844, "fight", { touch: true }))); + expect(captionSlot(scene(932, 430, "rest", { touch: true, lag: true }))).toEqual(captionSlot(scene(932, 430, "rest", { touch: true }))); + }); + + it("keeps a knockdown caption under the count on a phone, or gives way", () => { + for (const [width, height] of [[320, 568], [360, 640], [375, 667], [360, 740], [375, 812], [390, 844], [412, 915]] as const) { + // The 36 px figures stand on this baseline (hud.ts), with nothing below it. + const count = headlineBaseline(height, true) + COUNT_BELOW_HEADLINE; + for (const touch of [false, true]) { + const slot = captionSlot(scene(width, height, "knockdown", { touch })); + const top = slot === null ? null : slot.anchor === "top" ? slot.y : slot.y - 74 * slot.scale; + expect(top === null || top >= count + 4, `${width}x${height}${touch ? " with the pads" : ""}: ${top} under ${count}`).toBe(true); + } + } + expect(captionSlot(scene(390, 844, "knockdown", { touch: true }))?.y).toBeGreaterThanOrEqual(headlineBaseline(844, true) + COUNT_BELOW_HEADLINE + 4); + }); + + it("never draws a caption under the touch pads, upright or on its side", () => { + // The longest line the commentary has, which wraps onto two rows. + const longest = line("Crimson Geometry is just covering up as Azure Vector lets the hands go, and the referee is taking a long look!"); + let drawn = 0; + for (const [width, height] of [[320, 568], [360, 640], [375, 667], [390, 844], [412, 915], [568, 320], [640, 320], [640, 350], [640, 360], [667, 375], [740, 360], [844, 390], [932, 430]] as const) { + for (const phase of ["fight", "knockdown", "rest", "foul_recovery"] as const) { + for (const extra of [{}, { callout: true }, { replay: true }]) { + const slot = captionSlot(scene(width, height, phase, { touch: true, ...extra })); + if (slot === null) continue; + drawn += 1; + const plates: Array<{ x: number; y: number; w: number; h: number }> = []; + drawCaption(Object.assign(mockHudContext([]), { fillRect: (x: number, y: number, w: number, h: number) => plates.push({ x, y, w, h }) }), shown(longest), slot, true); + const plate = plates[0]!; + expect(plate.w, `${width}x${height} ${phase}`).toBeGreaterThan(0); + for (const pad of padRects(width, height)) { + const clear = plate.x + plate.w <= pad.left || plate.x >= pad.right || plate.y + plate.h <= pad.top || plate.y >= pad.bottom; + expect(clear, `${width}x${height} ${phase} ${JSON.stringify(extra)}: caption ${JSON.stringify(plate)} under the pads ${JSON.stringify(pad)}`).toBe(true); + } + } + } + } + // Most of these still have their caption, beside the pads or above them. + expect(drawn).toBeGreaterThan(40); + }); + + it("keeps the centre panel and the introductions drawn over it clear of the touch pads, upright or on its side", () => { + const players = Object.fromEntries(publicPlayers.map((player) => [player.id, { ...player, name: player.id === "one" ? "Azure Vector" : "Crimson Geometry" }])); + const card = line("In the red corner, Crimson Geometry.", { speaker: "announcer", urgent: true, card: { kicker: "IN THE RED CORNER", title: "CRIMSON GEOMETRY", detail: "SWARMER · 19-5-1 (12 KO) · RATED 1512", corner: 1 } }); + for (const [width, height] of PHONES) { + for (const phase of ["countdown", "rest", "foul_recovery"] as const) { + // The panel is the HUD's one fill of its colour in these phases; the introduction's plate is the caption's first. + const panels: Array<{ x: number; y: number; w: number; h: number }> = []; + let fill = ""; + const hud = Object.assign(mockHudContext([]), { fillRect: (x: number, y: number, w: number, h: number) => { if (fill === "rgba(3,6,12,0.88)") panels.push({ x, y, w, h }); } }); + Object.defineProperty(hud, "fillStyle", { set: (value: string) => { fill = value; } }); + drawHud(hud, width, height, { ...snapshot(), phase }, players, "one", null, 0, 30, null, null, null, null, null, null, null, true); + expect(panels, `${width}x${height} ${phase}`).toHaveLength(1); + const boxes = [panels[0]!]; + if (phase === "countdown") { + const plates: Array<{ x: number; y: number; w: number; h: number }> = []; + drawCaption(Object.assign(mockHudContext([]), { fillRect: (x: number, y: number, w: number, h: number) => plates.push({ x, y, w, h }) }), shown(card), captionSlot(scene(width, height, "countdown", { touch: true, hint: true }))!, true); + boxes.push(plates[0]!); + } + for (const box of boxes) { + const where = `${width}x${height} ${phase}: ${JSON.stringify(box)}`; + for (const pad of padRects(width, height)) expect(box.x + box.w <= pad.left || box.x >= pad.right || box.y + box.h <= pad.top - 8 || box.y >= pad.bottom, `${where} under the pads ${JSON.stringify(pad)}`).toBe(true); + // Under the top bar, on the screen, and clear of the round card a narrow screen puts under the top bar. + expect(box.y >= 54 && box.x >= 12 && box.x + box.w <= width - 12, where).toBe(true); + if (width < 640) expect(box.x + box.w <= width / 2 - 84 || box.x >= width / 2 + 84 || box.y >= 112, `${where} on the round card`).toBe(true); + } + } + } + }); + + it("keeps a downed player's get-up panel clear of the get-up pads and his thumbs on them", () => { + // While he is down only the punch pads show (style.css), as the get-up pads: as Edge lays them out, 190 x 118 + // at the foot of the block, or 188 x 92 on a screen on its side 350 px tall or less; 8 px round them for thumbs. + const getUpPads = (width: number, height: number) => (width > height && height <= 350 + ? { left: width - 200 - 8, right: width - 12 + 8, top: height - 204 - 8, bottom: height - 112 + 8 } + : { left: width - 202 - 8, right: width - 12 + 8, top: height - 236 - 8, bottom: height - 118 + 8 }); + const named = Object.fromEntries(publicPlayers.map((player) => [player.id, { ...player, name: player.id === "one" ? "Azure Vector" : "Crimson Geometry" }])); + for (const [width, height] of PHONES) { + const fills: Array<{ x: number; y: number; w: number; h: number; style: string }> = []; + const texts: Array<{ text: string; x: number; y: number; font: string }> = []; + let style = ""; + let font = ""; + const hud = Object.assign(mockHudContext([]), { + fillRect: (x: number, y: number, w: number, h: number) => fills.push({ x, y, w, h, style }), + fillText: (text: string, x: number, y: number) => texts.push({ text, x, y, font }), + }); + Object.defineProperty(hud, "fillStyle", { set: (value: string) => { style = value; } }); + Object.defineProperty(hud, "font", { set: (value: string) => { font = value; } }); + // Down, in his rhythm window: the count, the arrow, NOW! and the meter. + const down = { ...snapshot(100), phase: "knockdown" as const, fighters: [{ ...fighter("one", -100), is_downed: true, get_up_count: 3, get_up_prompt: "get_up_left" as const, get_up_meter: 18, get_up_required: 40, get_up_window_start_tick: 95, get_up_window_end_tick: 105 }, { ...fighter("two", 100), get_up_count: 3 }] as const }; + drawHud(hud, width, height, down, named, "one", null, 0, 30, null, null, null, null, null, null, "one", true); + const where = `${width}x${height}`; + const panels = fills.filter((fill) => fill.style === "rgba(3,6,12,0.88)"); + expect(panels, where).toHaveLength(1); + const panel = panels[0]!; + const meter = fills.find((fill) => fill.style === "rgba(2,4,9,0.9)")!; + const pads = getUpPads(width, height); + for (const box of [panel, meter]) expect(box.x + box.w <= pads.left || box.x >= pads.right || box.y + box.h <= pads.top || box.y >= pads.bottom, `${where}: ${JSON.stringify(box)} at the get-up pads`).toBe(true); + // On the screen, under the top bar and above the plates' guard and poise bars. + expect(panel.x >= 12 && panel.x + panel.w <= width - 12 && panel.y >= 54 && panel.y + panel.h <= height - 112, `${where}: ${JSON.stringify(panel)}`).toBe(true); + for (const prompt of ["COUNT 3", "←", "NOW!"]) { + const text = texts.find((candidate) => candidate.text === prompt && candidate.y > panel.y); + expect(text !== undefined && text.x > panel.x && text.x < panel.x + panel.w && text.y < panel.y + panel.h, `${where}: ${prompt} in the panel`).toBe(true); + } + // The headline it gives way to is not drawn under it, nor the round card's clock on a narrow screen. + for (const text of texts.filter((candidate) => candidate.text === "KNOCKDOWN" && candidate.font.includes("44px") || candidate.text.endsWith("IS DOWN"))) expect(text.y + 2, `${where}: ${text.text}`).toBeLessThanOrEqual(panel.y); + if (width < 640 && panel.y < 112 && panel.x < width / 2 + 84 && panel.x + panel.w > width / 2 - 84) expect(texts.some((text) => /^\d:\d\d$/u.test(text.text)), `${where}: the clock under the panel`).toBe(false); + } + }); + + it("keeps the paused bout's panel, and the captions with it, clear of the touch pads", () => { + const named = Object.fromEntries(publicPlayers.map((player) => [player.id, player])); + const longest = line("Crimson Geometry is just covering up as Azure Vector lets the hands go, and the referee is taking a long look!"); + for (const [width, height] of PHONES) { + for (const phase of ["fight", "rest", "countdown"] as const) { + const panels: Array<{ x: number; y: number; w: number; h: number }> = []; + let fill = ""; + const hud = Object.assign(mockHudContext([]), { fillRect: (x: number, y: number, w: number, h: number) => { if (fill === "rgba(3,6,12,0.88)") panels.push({ x, y, w, h }); } }); + Object.defineProperty(hud, "fillStyle", { set: (value: string) => { fill = value; } }); + drawHud(hud, width, height, { ...snapshot(), phase }, named, "one", null, 4000, 30, null, null, null, null, null, null, null, true); + // The pause is drawn last, over the countdown's or the rest's panel. + const pause = panels.at(-1)!; + const boxes = [pause]; + const slot = captionSlot(scene(width, height, phase, { touch: true, paused: true })); + if (slot !== null) { + const plates: Array<{ x: number; y: number; w: number; h: number }> = []; + drawCaption(Object.assign(mockHudContext([]), { fillRect: (x: number, y: number, w: number, h: number) => plates.push({ x, y, w, h }) }), shown(longest), slot, true); + const plate = plates[0]!; + expect(plate.y >= pause.y + pause.h || plate.y + plate.h <= pause.y, `${width}x${height} ${phase}: the caption over the pause`).toBe(true); + boxes.push(plate); + } + for (const box of boxes) { + const where = `${width}x${height} ${phase}: ${JSON.stringify(box)}`; + for (const pad of padRects(width, height)) expect(box.x + box.w <= pad.left || box.x >= pad.right || box.y + box.h <= pad.top - 8 || box.y >= pad.bottom, `${where} under the pads ${JSON.stringify(pad)}`).toBe(true); + expect(box.y >= 54, where).toBe(true); + if (width < 640) expect(box.x + box.w <= width / 2 - 84 || box.x >= width / 2 + 84 || box.y >= 112, `${where} on the round card`).toBe(true); + } + } + } + }); + + it("keeps clear of the replay tag", () => { + expect(captionSlot(scene(390, 844, "complete", { touch: true, replay: true }))!.y).toBe(120 + 34 + 8); + }); + + it("goes above the result card once it is up, or nowhere", () => { + expect(captionSlot(scene(1280, 720, "complete", { resultTop: 420 }))).toMatchObject({ anchor: "top", y: 54 }); + expect(captionSlot(scene(844, 390, "complete", { resultTop: 110 }))).toBeNull(); + }); +}); + +describe("the touch pads, as style.css lays them out", () => { + const sheet = readFileSync("src/style.css", "utf8"); + /** A media block of the sheet, from its opening to its last rule. */ + const media = (query: string): string => { + const start = sheet.indexOf(`@media${query}{`); + return start < 0 ? "" : sheet.slice(start, sheet.indexOf("}}", start) + 2); + }; + const declarations = (css: string, selector: string): string => new RegExp(`[{}\\n]${selector.replace(/[.()[\]]/gu, "\\$&")}\\{([^}]*)\\}`, "u").exec(`\n${css}`)?.[1] ?? ""; + const px = (css: string, property: string): number => Number(new RegExp(`(?:^|;)${property}:(?:max\\()?(\\d+)px`, "u").exec(css)?.[1]); + /** The width of a grid of fixed columns. */ + const columns = (css: string, gap: number): number => { + const [, count, size] = /repeat\((\d+),(\d+)px\)/u.exec(css)!; + return Number(count) * Number(size) + (Number(count) - 1) * gap; + }; + const rows = (count: number, height: number, gap: number): number => count * height + (count - 1) * gap; + + it("are the size the captions keep clear of", () => { + const pads = declarations(sheet, ".touch-pads"); + const grid = declarations(sheet, ".touch-grid"); + const mods = declarations(sheet, ".touch-mods"); + const moves = declarations(sheet, ".touch-moves"); + const [pad, mod, move] = [".touch-pad", ".touch-mod", ".touch-move"].map((selector) => px(declarations(sheet, selector), "height")) as [number, number, number]; + // Upright: two rows of moves, the modifiers and the punch pads, one above another. + const stack = rows(2, mod, px(mods, "gap")) + px(pads, "gap") + rows(2, pad, px(grid, "gap")); + expect({ bottom: px(pads, "bottom"), height: rows(2, move, px(moves, "gap")) + px(moves, "margin-bottom") + px(pads, "gap") + stack, grid: rows(2, pad, px(grid, "gap")) }).toEqual({ bottom: TOUCH_PADS.upright.bottom, height: TOUCH_PADS.upright.height, grid: TOUCH_PADS.upright.grid }); + expect(px(pads, "right") + columns(grid, px(grid, "gap")) + 14).toBe(TOUCH_PADS.reach); + // On its side the moves stand in three rows in a column of their own, to the left of the other two. + const side = media("(orientation:landscape)"); + expect({ height: stack, moves: rows(3, move, px(moves, "gap")), grid: rows(2, pad, px(grid, "gap")) }).toEqual({ height: TOUCH_PADS.landscape.height, moves: TOUCH_PADS.landscape.moves, grid: TOUCH_PADS.landscape.grid }); + expect(columns(declarations(side, ".touch-moves"), px(moves, "gap")) + px(declarations(side, ".touch-pads"), "column-gap")).toBe(TOUCH_PADS.movesReach); + // A short screen on its side gets smaller ones, lower down. + const short = media(`(orientation:landscape) and (max-height:${TOUCH_PADS.shortHeight}px)`); + const gap = px(declarations(short, ".touch-grid,.touch-mods,.touch-moves"), "gap"); + const [shortPad, shortMod] = [".touch-pad", ".touch-mod,.touch-move"].map((selector) => px(declarations(short, selector), "height")) as [number, number]; + const shortPads = declarations(short, ".touch-pads"); + expect({ bottom: px(shortPads, "bottom"), height: rows(2, shortMod, gap) + px(shortPads, "row-gap") + rows(2, shortPad, gap), moves: rows(3, shortMod, gap), grid: rows(2, shortPad, gap) }).toEqual(TOUCH_PADS.short); + }); + + it("keep the touch hint 12 px to their left, held upright or on its side", () => { + // max-width: min(44vw, 300px, calc(100vw - Npx - the side insets)), and the hint stands 12 px in from the left. + const cap = (css: string): [number, number, number] => { + const [, vw, most, offset] = /max-width:min\((\d+)vw,(\d+)px,calc\(100vw - (\d+)px - env\(safe-area-inset-left\) - env\(safe-area-inset-right\)\)\)/u.exec(css) ?? []; + return [Number(vw), Number(most), Number(offset)]; + }; + const upright = cap(declarations(sheet, ".overlay[data-touch-hint]")); + const sideways = cap(declarations(sheet.slice(sheet.indexOf("@media(orientation:landscape){.overlay[data-touch-hint]")), ".overlay[data-touch-hint]")); + const right = ([vw, most, offset]: [number, number, number], width: number): number => 12 + Math.min((vw / 100) * width, most, width - offset); + // Where the pads start: TOUCH_PADS.reach has 14 px to spare beyond them. + const pads = (width: number, onItsSide: boolean): number => width - (TOUCH_PADS.reach - 14) - (onItsSide ? TOUCH_PADS.movesReach : 0); + for (const width of [320, 360, 375, 390, 412]) expect(right(upright, width), `${width} upright`).toBeLessThanOrEqual(pads(width, false) - 12); + for (const width of [568, 640, 667, 740, 844, 932]) expect(right(sideways, width), `${width} on its side`).toBeLessThanOrEqual(pads(width, true) - 12); + }); + + it("let a tap between them through, so the block's empty corner never takes the top bar's buttons", () => { + expect(declarations(sheet, ".touch-pads")).toContain("pointer-events:none"); + expect(declarations(sheet, ".touch-pad,.touch-mod,.touch-move")).toContain("pointer-events:auto"); + }); + + it("show only the get-up pads while the player is down, where they stood", () => { + // Hidden, not removed: the get-up pads keep their place under his thumbs, and a held guard still hears its finger lift. + expect(declarations(sheet, ".touch-controls.knockdown .touch-move,.touch-controls.knockdown .touch-mod")).toBe("visibility:hidden"); + }); +}); + +describe("caption drawing", () => { + it("names the speaker and draws the line", () => { + const texts: string[] = []; + drawCaption(mockHudContext(texts), shown(line("Down goes Crimson Geometry!")), captionSlot(scene(1280, 720))!, false); + expect(texts).toEqual(["CALLAHAN", "Down goes Crimson Geometry!"]); + const colour: string[] = []; + drawCaption(mockHudContext(colour), shown(line("Tough round to score.", { speaker: "colour" })), captionSlot(scene(1280, 720))!, false); + expect(colour[0]).toBe("OKAFOR"); + }); + + it("wraps a long line onto two rows on a phone rather than shrinking it away", () => { + const texts: string[] = []; + const text = "Crimson Geometry is just covering up as Azure Vector lets the hands go!"; + drawCaption(mockHudContext(texts), shown(line(text)), captionSlot(scene(390, 844, "fight", { touch: true }))!, false); + expect(texts).toHaveLength(3); + expect(`${texts[1]} ${texts[2]}`).toBe(text); + }); + + it("splits at the space that balances the rows", () => { + expect(splitLine("aaaa bbbb cccc dddd", (part) => part.length <= 10)).toEqual(["aaaa bbbb", "cccc dddd"]); + expect(splitLine("aaaaaaaa bb", (part) => part.length <= 8)).toEqual(["aaaaaaaa", "bb"]); + }); + + it("draws an announcer card as kicker, title and detail", () => { + const texts: string[] = []; + const card = line("In the blue corner, Azure Vector.", { speaker: "announcer", urgent: true, card: { kicker: "IN THE BLUE CORNER", title: "AZURE VECTOR", detail: "RATED 1512", corner: 0 } }); + drawCaption(mockHudContext(texts), shown(card), captionSlot(scene(1280, 720))!, false); + expect(texts).toEqual(["IN THE BLUE CORNER", "AZURE VECTOR", "RATED 1512"]); + }); + + it("fits an introduction inside the panel it covers: tighter, then without the rating, then a smaller name", () => { + expect(cardMetrics(1, true, null).height).toBeGreaterThan(78); + const tall = cardMetrics(1, true, 78); + expect(tall.height).toBeLessThanOrEqual(78); + expect(tall.detail).toBeGreaterThan(0); + expect(tall.title).toBe(28); + const short = cardMetrics(0.78, true, 50); + expect(short.height).toBeLessThanOrEqual(50); + expect(short.detail).toBe(0); + expect(short.title).toBe(Math.round(28 * 0.78)); + const tiny = cardMetrics(0.78, true, 36); + expect(tiny.height).toBeLessThanOrEqual(36); + expect(tiny.title).toBeLessThan(Math.round(28 * 0.78)); + expect(cardMetrics(1, true, 200)).toEqual(cardMetrics(1, true, null)); + }); + + it("draws the introduction over the whole countdown panel", () => { + const rects: Array<[number, number, number, number]> = []; + const context = Object.assign(mockHudContext([]), { fillRect: (x: number, y: number, w: number, h: number) => rects.push([x, y, w, h]) }); + const slot = captionSlot(scene(390, 844, "countdown", { touch: true }))!; + const card = line("In the blue corner, Azure Vector.", { speaker: "announcer", urgent: true, card: { kicker: "IN THE BLUE CORNER", title: "AZURE VECTOR", detail: "RATED 1512", corner: 0 } }); + drawCaption(context, shown(card, 1, 1), slot, false); + const [x, y, w, h] = rects[0]!; + expect(x).toBeLessThanOrEqual(390 / 2 - 160); + expect(x + w).toBeGreaterThanOrEqual(390 / 2 + 160); + expect(y).toBeCloseTo(slot.y); + expect(h).toBeGreaterThanOrEqual(panelHeightFor(844)); + }); + + it("fades and eases in, but with reduced motion it simply appears", () => { + const fading = mockHudContext([]); + drawCaption(fading, shown(line("Big right hand from Azure Vector!"), 0.4, 0.6), captionSlot(scene(1280, 720))!, false); + // The last thing drawn is the words, which fade as the square of the plate's opacity. + expect(fading.globalAlpha).toBeCloseTo(0.16, 6); + const still = mockHudContext([]); + drawCaption(still, shown(line("Big right hand from Azure Vector!"), 0.4, 0.6), captionSlot(scene(1280, 720))!, true); + expect(still.globalAlpha).toBe(1); + const hidden: string[] = []; + drawCaption(mockHudContext(hidden), shown(line("Gone"), 0), captionSlot(scene(1280, 720))!, false); + expect(hidden).toEqual([]); + }); +}); + +describe("a caption fading in or out", () => { + it("fades its words faster than its plate, so no ghost text floats over the fighters", () => { + const plates: number[] = []; + const words: number[] = []; + const ctx = { + globalAlpha: 1, font: "", fillStyle: "", textAlign: "left", textBaseline: "alphabetic", + save: () => undefined, restore: () => undefined, + measureText: (text: string) => ({ width: text.length * 7 }), + fillRect(this: { globalAlpha: number }) { plates.push(this.globalAlpha); }, + fillText(this: { globalAlpha: number }) { words.push(this.globalAlpha); }, + } as unknown as CanvasRenderingContext2D; + const slot = { x: 640, y: 600, maxWidth: 600, anchor: "bottom" as const, scale: 1, cover: null }; + const line = { speaker: "play" as const, text: "Down goes Two!", priority: 95, urgent: true, hold: 3, card: null }; + drawCaption(ctx, { line, opacity: 0.3, entering: 0 }, slot, false); + expect(Math.min(...plates)).toBeCloseTo(0.3, 6); + expect(Math.max(...words)).toBeCloseTo(0.09, 6); + plates.length = 0; + words.length = 0; + drawCaption(ctx, { line: { ...line, card: { kicker: "THE WINNER, BY UNANIMOUS DECISION", title: "TWO", detail: "", corner: 1 } }, opacity: 0.3, entering: 0 }, slot, false); + expect(Math.max(...words)).toBeCloseTo(0.09, 6); + drawCaption(ctx, { line, opacity: 1, entering: 0 }, slot, false); + expect(words.at(-1)).toBe(1); + }); +}); diff --git a/web/hands/src/render/caption.ts b/web/hands/src/render/caption.ts new file mode 100644 index 00000000..5f61a015 --- /dev/null +++ b/web/hands/src/render/caption.ts @@ -0,0 +1,252 @@ +import type { MatchPhase } from "../types"; +import { BROADCAST_VOICES, type AnnouncerCard, type BroadcastLine, type Caption, type Speaker } from "./commentary"; +import { CALLOUT_BASELINE, CALLOUT_BELOW_BASELINE, COUNT_BELOW_HEADLINE, fitFontSize, headlineBaseline, topPanel, touchPadsReach } from "./hud"; + +/** What else is on the screen, so the caption keeps out of its way. */ +export interface CaptionScene { + readonly width: number; + readonly height: number; + readonly phase: MatchPhase; + /** Top edge of the result card while it is shown. */ + readonly resultTop: number | null; + /** The on-screen touch pads are up over the lower right of the screen. */ + readonly touch: boolean; + /** The controls hint sits above the plates during the countdown. */ + readonly hint: boolean; + /** The viewer is down and has the get-up panel to watch. */ + readonly viewerDown: boolean; + readonly replay: boolean; + /** Where the fighter's own corner panel starts, measured down the screen, while it is up between rounds. */ + readonly cornerPanelTop?: number | null; + /** A big callout ("ROUND 2", "PARRIED") is on screen. */ + readonly callout?: boolean; + /** The slow-connection warning is up: beside the touch pads it stands at the top left (hud.ts). */ + readonly lag?: boolean; + /** The bout is paused for a reconnection: with the touch pads up its panel stands in the centre panels' place (hud.ts). */ + readonly paused?: boolean; +} + +export interface CaptionSlot { + /** Centre of the caption. */ + readonly x: number; + /** Its bottom edge for a lower third, its top edge otherwise. */ + readonly y: number; + readonly maxWidth: number; + readonly anchor: "top" | "bottom"; + readonly scale: number; + /** The countdown panel the introductions are drawn over, which the caption covers completely. */ + readonly cover: { readonly width: number; readonly height: number } | null; +} + +/** Below the top bar. */ +const TOP_CLEARANCE = 54; +/** Below the round clock, the portraits and the input readout on narrow screens. */ +const COMPACT_TOP = 136; +/** Above the plates' guard and poise bars. */ +const LOWER_THIRD = 110; +/** Above the controls hint the page shows over the plates during the countdown. */ +const ABOVE_HINT = 168; +/** The tallest caption, an announcer card, at full size. */ +const CAPTION_ROOM = 74; +/** Below the knockdown count's 36 px figures (hud.ts), which stand this far under the headline. */ +const BELOW_COUNT = COUNT_BELOW_HEADLINE + 8; +const REPLAY_TAG = { right: 220, wideTop: 64, compactTop: 120, height: 34 } as const; +/** A caption squeezed between the pads and their mirror image narrower than this gives way instead. */ +const BESIDE_PADS_MIN_WIDTH = 200; +/** The slow-connection warning at the top left (hud.ts): how far in its longest reading reaches, with room to spare, and its foot. */ +const LAG_WARNING = { reach: 24 + 160 + 8, bottom: 80 } as const; + +const BACKGROUND = "rgba(3,6,12,0.86)"; +const COVER_BACKGROUND = "rgb(4,7,13)"; +const SPEAKER_ACCENTS: Readonly> = { play: "#f6d57a", colour: "#9ec7ff", announcer: "#f6d57a" }; +const CORNER_ACCENTS = ["#4f86d9", "#d9483c"] as const; +const FAMILY = "Inter, system-ui, sans-serif"; + +/** + * Where the caption goes: over the countdown panel for the introductions, a lower third above the plates on a + * desktop, otherwise under the top bar and below whatever panel is up. Null when there is no room that keeps + * clear of the fighters and the scoreboard. + */ +export function captionSlot(scene: CaptionScene): CaptionSlot | null { + const { width, height } = scene; + const compact = width < 640; + const short = height < 480; + const scale = short ? 0.78 : compact ? 0.85 : 1; + const maxWidth = Math.min(680, width - 24); + const result = scene.resultTop; + // The pads are part of the page, over the canvas, until the bout is over. + const pads = result === null && scene.touch && scene.phase !== "complete"; + const panel = topPanel(width, height, pads); + // With the pads up a pause's panel stands where the introductions would cover it. + if (pads && scene.paused === true && scene.phase === "countdown") return null; + if (result === null && scene.phase === "countdown") { + // The introductions take the countdown panel's place, and on a phone on its side stay as clear of the pads as it. + const centre = panel.x + panel.width / 2; + const right = width - (pads && width > height ? touchPadsReach(width, height, panel.y + panel.height) : 12); + return { x: centre, y: panel.y, maxWidth: Math.min(maxWidth, 2 * Math.min(centre - 12, right - centre)), anchor: "top", scale, cover: { width: panel.width, height: panel.height } }; + } + if (result === null && !scene.touch && !short) { + const lowerThird = height - (scene.hint ? ABOVE_HINT : LOWER_THIRD); + const bottom = scene.cornerPanelTop === undefined || scene.cornerPanelTop === null ? lowerThird : Math.min(lowerThird, scene.cornerPanelTop - 8); + // Above the corner panel when there is room for it there, otherwise up at the top with the panels. + if (bottom - CAPTION_ROOM * scale >= height * 0.45) return { x: width / 2, y: bottom, maxWidth, anchor: "bottom", scale, cover: null }; + } + if (result === null && scene.viewerDown) return null; + let top = result === null && compact ? COMPACT_TOP : TOP_CLEARANCE; + if (result === null) { + if (scene.phase === "rest" || scene.phase === "foul_recovery" || (pads && scene.paused === true)) top = Math.max(top, panel.y + panel.height + 8); + else if (scene.phase === "knockdown") top = Math.max(top, headlineBaseline(height, compact) + BELOW_COUNT); + if (scene.replay && width / 2 - maxWidth / 2 < REPLAY_TAG.right + 8) top = Math.max(top, (compact ? REPLAY_TAG.compactTop : REPLAY_TAG.wideTop) + REPLAY_TAG.height + 8); + if (scene.callout === true && scene.phase === "fight") top = Math.max(top, height * CALLOUT_BASELINE + CALLOUT_BELOW_BASELINE + 8); + } + const limit = result !== null ? result - 8 : height * (short ? 0.46 : 0.5); + if (top + CAPTION_ROOM * scale > limit) return null; + // With the pads up the slow-connection warning has the top left corner, which the caption leaves it as it + // leaves the pads their side. + const lag = pads && scene.lag === true && !compact && !scene.replay && top < LAG_WARNING.bottom ? LAG_WARNING.reach : 0; + const reach = Math.max(pads ? touchPadsReach(width, height, top + lineRoom(scale)) : 0, lag); + if (reach === 0) return { x: width / 2, y: top, maxWidth, anchor: "top", scale, cover: null }; + const beside = Math.min(maxWidth, width - reach * 2); + return beside < BESIDE_PADS_MIN_WIDTH ? null : { x: width / 2, y: top, maxWidth: beside, anchor: "top", scale, cover: null }; +} + +/** + * The tallest commentary line, two rows at full size, as drawLine sets it. Only lines are shown while the touch + * pads are up: the announcer's cards come with the countdown, over its panel, and after the bout. + */ +const lineRoom = (s: number): number => Math.round(8 * s) + Math.round(10 * s) + Math.round(4 * s) + 2 * Math.round(Math.round(17 * s) * 1.3) + Math.round(5 * s); + +/** Draws the caption in its slot: faded and eased in unless motion is reduced, when it simply appears. */ +export function drawCaption(ctx: CanvasRenderingContext2D, caption: Caption, slot: CaptionSlot, reducedMotion: boolean): void { + const alpha = reducedMotion ? 1 : caption.opacity; + if (alpha <= 0.01) return; + const slide = reducedMotion || slot.cover !== null ? 0 : caption.entering * 8 * (slot.anchor === "bottom" ? 1 : -1); + ctx.save(); + ctx.globalAlpha = alpha; + ctx.textBaseline = "alphabetic"; + // The dark plate is all but invisible over the arena while it fades, so the words fade faster than the plate + // and never float over the fighters as ghost text. + const textAlpha = alpha * alpha; + if (caption.line.card !== null) drawCard(ctx, caption.line.card, slot, slide, textAlpha); + else drawLine(ctx, caption.line, slot, slide, textAlpha); + ctx.restore(); +} + +function truncate(ctx: CanvasRenderingContext2D, text: string, width: number): string { + if (ctx.measureText(text).width <= width) return text; + let value = text; + while (value.length > 1 && ctx.measureText(`${value}…`).width > width) value = value.slice(0, -1); + return `${value}…`; +} + +/** Splits a line in two at the space that best balances the halves while both fit. */ +export function splitLine(text: string, fits: (part: string) => boolean): [string, string] { + const middle = text.length / 2; + let best: [string, string] | null = null; + let bestDistance = Infinity; + for (let index = text.indexOf(" "); index >= 0; index = text.indexOf(" ", index + 1)) { + const first = text.slice(0, index); + const second = text.slice(index + 1); + const distance = Math.abs(index - middle); + if (fits(first) && fits(second) && distance < bestDistance) { + best = [first, second]; + bestDistance = distance; + } + } + if (best !== null) return best; + const space = text.lastIndexOf(" ", Math.floor(middle)); + return space > 0 ? [text.slice(0, space), text.slice(space + 1)] : [text, ""]; +} + +function drawLine(ctx: CanvasRenderingContext2D, line: BroadcastLine, slot: CaptionSlot, slide: number, textAlpha: number): void { + const s = slot.scale; + const padX = Math.round(14 * s); + const tagSize = Math.round(10 * s); + const inner = slot.maxWidth - padX * 2 - 4; + const measure = (text: string, size: number): number => { + ctx.font = `700 ${size}px ${FAMILY}`; + return ctx.measureText(text).width; + }; + const size = fitFontSize((candidate) => measure(line.text, candidate), inner, Math.round(17 * s), Math.round(13 * s)); + const rows = measure(line.text, size) <= inner ? [line.text] : splitLine(line.text, (part) => measure(part, size) <= inner).filter((row) => row.length > 0); + const textWidth = Math.max(...rows.map((row) => Math.min(inner, measure(row, size)))); + const tag = BROADCAST_VOICES[line.speaker]; + ctx.font = `800 ${tagSize}px ${FAMILY}`; + const tagWidth = ctx.measureText(tag).width; + const width = Math.min(slot.maxWidth, Math.max(textWidth, tagWidth) + padX * 2 + 4); + const rowHeight = Math.round(size * 1.3); + const head = Math.round(8 * s) + tagSize; + const height = head + Math.round(4 * s) + rows.length * rowHeight + Math.round(5 * s); + const top = (slot.anchor === "bottom" ? slot.y - height : slot.y) + slide; + const left = slot.x - width / 2; + const accent = SPEAKER_ACCENTS[line.speaker]; + ctx.fillStyle = BACKGROUND; + ctx.fillRect(left, top, width, height); + ctx.fillStyle = accent; + ctx.fillRect(left, top, 4, height); + ctx.globalAlpha = textAlpha; + ctx.textAlign = "left"; + ctx.font = `800 ${tagSize}px ${FAMILY}`; + ctx.fillText(tag, left + 4 + padX, top + head); + ctx.fillStyle = "#f6f7fb"; + ctx.font = `700 ${size}px ${FAMILY}`; + rows.forEach((row, index) => ctx.fillText(truncate(ctx, row, inner), left + 4 + padX, top + head + Math.round(4 * s) + rowHeight * (index + 1) - Math.round(size * 0.28))); +} + +/** Kicker, title and detail sizes for a card fitted to a panel it covers: tighter first, then without the detail, then a smaller title. */ +export function cardMetrics(scale: number, hasDetail: boolean, room: number | null): { kicker: number; title: number; detail: number; gap: number; pad: number; height: number } { + const kicker = Math.round(11 * scale); + const title = Math.round(28 * scale); + const detailGap = Math.round(6 * scale); + const loose = { pad: Math.round(10 * scale), gap: Math.round(7 * scale) }; + const tight = { pad: Math.max(4, Math.round(6 * scale)), gap: Math.max(3, Math.round(4 * scale)) }; + const fullDetail = hasDetail ? Math.round(10 * scale) : 0; + const layouts = [{ ...loose, detail: fullDetail }, { ...tight, detail: fullDetail }, { ...tight, detail: 0 }]; + const heightOf = (layout: { pad: number; gap: number; detail: number }, size: number): number => layout.pad * 2 + kicker + layout.gap + size + (layout.detail === 0 ? 0 : detailGap + layout.detail); + for (const layout of layouts) { + if (room === null || heightOf(layout, title) <= room) return { kicker, title, ...layout, height: heightOf(layout, title) }; + } + const last = layouts[2]!; + const fitted = Math.max(12, Math.min(title, (room ?? Infinity) - heightOf(last, 0))); + return { kicker, title: fitted, ...last, height: heightOf(last, fitted) }; +} + +function drawCard(ctx: CanvasRenderingContext2D, card: AnnouncerCard, slot: CaptionSlot, slide: number, textAlpha: number): void { + const s = slot.scale; + const pad = Math.round(18 * s); + const inner = slot.maxWidth - pad * 2; + const metrics = cardMetrics(s, card.detail.length > 0, slot.cover?.height ?? null); + const kickerSize = metrics.kicker; + const detailSize = metrics.detail; + const measure = (text: string, weight: number, size: number): number => { + ctx.font = `${weight} ${size}px ${FAMILY}`; + return ctx.measureText(text).width; + }; + const titleSize = fitFontSize((candidate) => measure(card.title, 900, candidate), inner, metrics.title, Math.min(metrics.title, Math.round(15 * s))); + const contentWidth = Math.max(measure(card.kicker, 800, kickerSize), Math.min(inner, measure(card.title, 900, titleSize)), detailSize === 0 ? 0 : measure(card.detail, 700, detailSize)); + const width = Math.min(slot.maxWidth, Math.max(240 * s, contentWidth + pad * 2, slot.cover?.width ?? 0)); + const content = metrics.pad + kickerSize + metrics.gap + titleSize + (detailSize === 0 ? 0 : Math.round(6 * s) + detailSize) + metrics.pad; + const height = Math.max(content, slot.cover?.height ?? 0); + const top = (slot.anchor === "bottom" ? slot.y - height : slot.y) + slide; + const left = slot.x - width / 2; + const accent = card.corner === null ? SPEAKER_ACCENTS.announcer : CORNER_ACCENTS[card.corner]; + ctx.fillStyle = slot.cover === null ? BACKGROUND : COVER_BACKGROUND; + ctx.fillRect(left, top, width, height); + ctx.fillStyle = accent; + ctx.fillRect(left, top, width, 3); + ctx.globalAlpha = textAlpha; + ctx.textAlign = "center"; + let baseline = top + (height - content) / 2 + metrics.pad + kickerSize; + ctx.font = `800 ${kickerSize}px ${FAMILY}`; + ctx.fillText(truncate(ctx, card.kicker, inner), slot.x, baseline); + baseline += metrics.gap + titleSize; + ctx.fillStyle = "#ffffff"; + ctx.font = `900 ${titleSize}px ${FAMILY}`; + ctx.fillText(truncate(ctx, card.title, inner), slot.x, baseline - Math.round(titleSize * 0.12)); + if (detailSize > 0) { + baseline += Math.round(6 * s) + detailSize; + ctx.fillStyle = "#93a3bd"; + ctx.font = `700 ${detailSize}px ${FAMILY}`; + ctx.fillText(truncate(ctx, card.detail, inner), slot.x, baseline); + } +} diff --git a/web/hands/src/render/commentary.test.ts b/web/hands/src/render/commentary.test.ts new file mode 100644 index 00000000..7bac0180 --- /dev/null +++ b/web/hands/src/render/commentary.test.ts @@ -0,0 +1,591 @@ +import manifest from "../../../../src/intelstream/hands/combat-manifest.json"; +import { ROCKED_BASE_TICKS, ROCKED_MAX_TICKS } from "../manifest"; +import { FIGHTER_STYLES } from "../protocol"; +import { fighter } from "../test/fixtures"; +import { PROTOCOL_VERSION, type CombatEvent, type EngineSnapshot, type FighterSnapshot, type FinalMessage, type MatchPhase, type PublicPlayer } from "../types"; +import { CommentaryDirector, crowdTension, fillLine, holdFor, introductionScript, punchName, recordSpoken, spokenNumber, spokenSeconds, type CommentaryHooks } from "./commentary"; + +/** The opening countdown the room gives the introductions, in ticks. */ +const OPENING_TICKS = manifest.countdown.opening_ticks; + +const players: Record = { + one: { id: "one", name: "Azure Vector", avatar: null, rating: 1512, connected: true }, + two: { id: "two", name: "Crimson Geometry", avatar: null, rating: 1494, connected: true }, +}; + +let nextId = 1; +const event = (kind: string, tick: number, fields: Partial = {}): CombatEvent => ({ event_id: nextId++, tick, kind, actor_id: null, target_id: null, amount: 0, detail: "", blood: 0, direction: 0, action_id: null, ...fields }); +const hit = (tick: number, actor: string, detail = "jab:head", amount = 30, kind = "hit"): CombatEvent => event(kind, tick, { actor_id: actor, target_id: actor === "one" ? "two" : "one", amount, detail }); + +const state = (tick: number, phase: MatchPhase = "fight", edits: [Partial, Partial] = [{}, {}], extra: Partial = {}): EngineSnapshot => ({ + tick, + phase, + round_number: 1, + phase_ticks_remaining: 3000, + fighters: [{ ...fighter("one", -100), ...edits[0] }, { ...fighter("two", 100), ...edits[1] }], + events: [], + result: null, + checksum: "a".repeat(64), + ...extra, +}); + +/** Runs the director's clock forward and returns every distinct line that reached the screen. */ +function watch(director: CommentaryDirector, from: number, to: number, step = 1 / 30): string[] { + const seen: string[] = []; + for (let now = from; now <= to; now += step) { + const caption = director.current(now); + if (caption !== null && seen[seen.length - 1] !== caption.line.text) seen.push(caption.line.text); + } + return seen; +} + +function feed(director: CommentaryDirector, snapshot: EngineSnapshot, events: CombatEvent[], now: number): void { + director.observe({ ...snapshot, events }, events, players, 30, now); +} + +beforeEach(() => { + nextId = 1; +}); + +describe("commentary lines", () => { + it("fills names and capitalises the start of a line", () => { + expect(fillLine("{b} is hurt!", { b: "the blue corner" })).toBe("The blue corner is hurt!"); + expect(fillLine("Down goes {b}!", { b: "Crimson Geometry" })).toBe("Down goes Crimson Geometry!"); + }); + + it("names punches the way a commentator does", () => { + const puncher = (action: FighterSnapshot["action"], hand: FighterSnapshot["action_hand"]): FighterSnapshot => ({ ...fighter("one"), action, action_hand: hand }); + expect(punchName("hook:head", puncher("hook", "right"))).toBe("right hook"); + expect(punchName("straight:head", puncher("straight", "right"))).toBe("right hand"); + expect(punchName("uppercut:body", puncher("uppercut", "left"))).toBe("left uppercut to the body"); + expect(punchName("jab:head", puncher("jab", "left"))).toBe("jab"); + expect(punchName("hook:head", puncher("jab", "left"))).toBe("hook"); + expect(punchName("right:hook:body", puncher("hook", "right"))).toBe("right hook to the body"); + }); + + it("holds a line long enough to read and no longer than five seconds", () => { + expect(holdFor("Down goes Two!")).toBe(2.2); + expect(holdFor("x".repeat(200))).toBe(5.2); + expect(holdFor("x".repeat(40))).toBeCloseTo(3.4); + }); +}); + +describe("the commentary team", () => { + it("calls the knockdown over the punch and the hurt fighter of the same moment", () => { + const director = new CommentaryDirector(); + const tick = 600; + feed(director, state(tick, "knockdown", [{}, { is_downed: true, poise: 0 }]), [ + hit(tick, "one", "hook:head", 120, "counter_hit"), + event("stun", tick, { actor_id: "one", target_id: "two", amount: 120 }), + event("knockdown", tick, { actor_id: "one", target_id: "two", amount: 1 }), + ], 10); + expect(director.current(10.1)).toBeNull(); + const lines = watch(director, 10.3, 20); + expect(lines).toHaveLength(1); + expect(lines[0]).toMatch(/Crimson Geometry/u); + expect(lines[0]).toMatch(/down|drops/iu); + }); + + it("calls a man badly hurt only when the shot has rocked him about as long as one can", () => { + const call = (stunned: number): string => { + const director = new CommentaryDirector(); + feed(director, state(400, "fight", [{}, { stunned_ticks: stunned, poise: 400 }]), [ + event("stun", 400, { actor_id: "one", target_id: "two", amount: 80 }), + ], 5); + return watch(director, 5.2, 12)[0] ?? ""; + }; + const badly = /out on their feet|all sorts of trouble|badly hurt/u; + expect(call(ROCKED_MAX_TICKS)).toMatch(badly); + expect(call(ROCKED_BASE_TICKS)).not.toMatch(badly); + }); + + it("never shows two lines at once and leaves a pause between lines", () => { + const director = new CommentaryDirector(); + feed(director, state(100), [hit(100, "one", "jab:head"), hit(110, "one", "straight:head"), hit(120, "one", "hook:head")], 1); + const first = director.current(1.3)!; + const end = 1.3 + first.line.hold; + feed(director, state(300), [event("taunt", 300, { actor_id: "two" })], end - 0.6); + const shown: Array<{ text: string; at: number }> = []; + for (let now = 1.3; now <= 14; now += 1 / 60) { + const caption = director.current(now); + if (caption !== null && shown[shown.length - 1]?.text !== caption.line.text) shown.push({ text: caption.line.text, at: now }); + } + expect(shown.map((entry) => entry.text)).toHaveLength(2); + expect(shown[0]!.text).toMatch(/Azure Vector/u); + expect(shown[1]!.text).toMatch(/Crimson Geometry/u); + expect(shown[1]!.at - shown[0]!.at).toBeGreaterThanOrEqual(holdFor(shown[0]!.text) + 0.9 - 1e-6); + }); + + it("lets an urgent call cut in over colour, but not straight away", () => { + const director = new CommentaryDirector(); + feed(director, state(100), [event("taunt", 100, { actor_id: "two" })], 0); + expect(director.current(0.4)?.line.text).toMatch(/Crimson Geometry/u); + feed(director, state(118), [event("stun", 118, { actor_id: "two", target_id: "one", amount: 80 })], 0.5); + expect(director.current(0.85)?.line.speaker).toBe("colour"); + expect(director.current(1.25)?.line.text).toMatch(/Azure Vector/u); + expect(director.current(1.25)?.line.urgent).toBe(true); + }); + + it("drops a line that could not be shown while the action was fresh", () => { + const director = new CommentaryDirector(); + feed(director, state(100), [event("get_up", 100, { actor_id: "two", amount: 9 })], 0); + feed(director, state(101), [event("taunt", 101, { actor_id: "one" })], 0.1); + const lines = watch(director, 0.3, 30); + expect(lines.some((text) => /nine/u.test(text))).toBe(true); + expect(lines.some((text) => /taunt|showboat|invites|chin/iu.test(text))).toBe(false); + }); + + it("does not repeat the same phrasing back to back", () => { + const director = new CommentaryDirector(); + const texts: string[] = []; + for (let index = 0; index < 8; index += 1) { + const tick = 1000 + index * 300; + const now = index * 10; + feed(director, state(tick), [event("stun", tick, { actor_id: "one", target_id: "two", amount: 60 })], now); + const text = director.current(now + 0.4)?.line.text; + if (text !== undefined) texts.push(text); + } + expect(texts).toHaveLength(8); + for (let index = 1; index < texts.length; index += 1) expect(texts[index]).not.toBe(texts[index - 1]); + }); + + it("gives every viewer the same call for the same fight", () => { + const run = (): string[] => { + nextId = 1; + const director = new CommentaryDirector(); + feed(director, state(100), [hit(100, "one", "hook:head", 130, "counter_hit")], 0); + feed(director, state(400), [event("stun", 400, { actor_id: "two", target_id: "one", amount: 70 })], 10); + feed(director, state(700), [event("clinch", 700, { actor_id: "one", target_id: "two" })], 20); + return watch(director, 0, 30); + }; + expect(run()).toEqual(run()); + }); + + it("falls back to the corner when a name is missing", () => { + const director = new CommentaryDirector(); + director.observe({ ...state(50), events: [] }, [event("stun", 50, { actor_id: "two", target_id: "one", amount: 60 })], {}, 30, 0); + expect(director.current(0.5)?.line.text).toMatch(/blue corner/iu); + }); +}); + +describe("the ring announcer", () => { + it("introduces both corners during the opening countdown, reading each one as its card comes up", () => { + const speak = vi.fn(); + const director = new CommentaryDirector({ speak }); + feed(director, state(1, "countdown", [{}, {}], { phase_ticks_remaining: OPENING_TICKS }), [], 0); + expect(speak).not.toHaveBeenCalled(); + const blue = director.current(0); + expect(blue?.line.card).toEqual({ kicker: "IN THE BLUE CORNER", title: "AZURE VECTOR", detail: "RATED 1512", corner: 0 }); + expect(speak.mock.calls).toEqual([[["In the blue corner, Azure Vector!"]]]); + // The red card comes up as the blue corner's line ends, and its line is read with it. + const redAt = spokenSeconds("In the blue corner, Azure Vector!"); + expect(director.current(redAt - 0.05)?.line.card?.corner).toBe(0); + expect(speak).toHaveBeenCalledOnce(); + const red = director.current(redAt + 0.01); + expect(red?.line.card).toMatchObject({ kicker: "IN THE RED CORNER", title: "CRIMSON GEOMETRY", corner: 1 }); + expect(speak.mock.calls.at(-1)).toEqual([["And in the red corner, Crimson Geometry!"]]); + // Both are read before the opening bell, with room to spare for a slower voice. + expect(redAt + spokenSeconds("And in the red corner, Crimson Geometry!")).toBeLessThanOrEqual(OPENING_TICKS / 30 - 0.3); + }); + + it("keeps the records and styles on the cards, and reads them before the names only when the countdown has the time", () => { + const speak = vi.fn(); + const director = new CommentaryDirector({ speak }); + const recorded = { one: { ...players.one!, record: { wins: 12, losses: 3, draws: 1, knockouts: 8 } }, two: { ...players.two!, record: { wins: 0, losses: 0, draws: 0, knockouts: 0 } } }; + director.observe({ ...state(1, "countdown", [{ style: "boxer" }, { style: "swarmer" }], { phase_ticks_remaining: OPENING_TICKS }), events: [] }, [], recorded, 30, 0); + expect(watch(director, 0, 8)).toEqual(["In the blue corner, Azure Vector.", "In the red corner, Crimson Geometry."]); + const cards = new CommentaryDirector(); + cards.observe({ ...state(1, "countdown", [{ style: "boxer" }, { style: "swarmer" }], { phase_ticks_remaining: OPENING_TICKS }), events: [] }, [], recorded, 30, 0); + expect(cards.current(0.05)?.line.card?.detail).toBe("BOXER · 12-3-1 (8 KO) · RATED 1512"); + expect(cards.current(5)?.line.card?.detail).toBe("SWARMER · PRO DEBUT · RATED 1494"); + // Eight seconds hold the names; the bell would cut a record or a style off before them. + expect(speak.mock.calls.flatMap((call) => call[0] as string[])).toEqual(["In the blue corner, Azure Vector!", "And in the red corner, Crimson Geometry!"]); + const corners = [{ name: "Azure Vector", record: recorded.one.record, style: "boxer" }, { name: "Crimson Geometry", record: recorded.two.record, style: "swarmer" }] as const; + expect(introductionScript(corners, 20)).toEqual([ + "In the blue corner, twelve, three and one, eight by knockout, the boxer... Azure Vector!", + "And in the red corner, making a pro debut, the swarmer... Crimson Geometry!", + ]); + expect(introductionScript([{ ...corners[0], record: undefined, style: "slugger" }, { ...corners[1], record: undefined, style: "balanced" }], 10)).toEqual([ + "In the blue corner, the slugger... Azure Vector!", + "And in the red corner, Crimson Geometry!", + ]); + }); + + it("fits the spoken introductions inside the opening countdown, names and all, for every pairing the room can make", () => { + const names = ["Kid Cole", "Azure Vector", "Crimson Geometry", "Viktor 'Iron' Volkov", "Marcus 'Hammer' Reed", "W".repeat(32)]; + const records = [undefined, { wins: 0, losses: 0, draws: 0, knockouts: 0 }, { wins: 12, losses: 3, draws: 1, knockouts: 8 }, { wins: 36, losses: 1, draws: 0, knockouts: 29 }]; + const styles = [undefined, ...FIGHTER_STYLES]; + const budget = OPENING_TICKS / 30 - 0.3; + for (const blue of names) for (const red of names) for (const record of records) for (const style of styles) { + const script = introductionScript([{ name: blue, record, style }, { name: red, record, style }], OPENING_TICKS / 30); + expect(script.length).toBeGreaterThan(0); + expect(script.reduce((total, line) => total + spokenSeconds(line), 0)).toBeLessThanOrEqual(budget); + expect(script.join(" ")).toContain(`${blue}`); + expect(script.join(" ")).toContain(`${red}!`); + } + // The computer is introduced from its own corner, from the first snapshot of the countdown a player sees. + const contender = { name: "Marcus 'Hammer' Reed", record: { wins: 19, losses: 5, draws: 1, knockouts: 12 }, style: "swarmer" } as const; + expect(introductionScript([{ name: "Azure Vector", record: records[1], style: "boxer" }, contender], (OPENING_TICKS - 1) / 30)).toEqual(["In the blue corner, Azure Vector!", "And in the red corner, Marcus 'Hammer' Reed!"]); + // A late arrival with too little countdown left hears nothing rather than half a name. + expect(introductionScript([{ name: "Azure Vector", record: undefined, style: undefined }, { name: "Crimson Geometry", record: undefined, style: undefined }], 3)).toEqual([]); + }); + + it("estimates the announcer's voice on the slow side", () => { + // Seconds Windows' default voice (Microsoft David, SAPI rate 0) takes; the page reads at 0.96. + const timed: Array<[string, number]> = [ + ["In the blue corner, Azure Vector!", 2.95], + ["And in the red corner, Crimson Geometry!", 3.54], + ["And in the red corner, Viktor 'Iron' Volkov!", 3.6], + ["Azure Vector... versus Crimson Geometry!", 3.7], + ["In the blue corner, twelve and three, eight by knockout, the boxer... Azure Vector!", 7.15], + ["And in the red corner, thirty-six and one, twenty-nine by knockout, the counter-puncher... Viktor 'Iron' Volkov!", 8.76], + ]; + for (const [line, seconds] of timed) expect(spokenSeconds(line)).toBeGreaterThanOrEqual(seconds / 0.96); + }); + + it("says a record the way a ring announcer does", () => { + expect([7, 19, 20, 29, 36, 104, 120, 999, 1000].map(spokenNumber)).toEqual(["seven", "nineteen", "twenty", "twenty-nine", "thirty-six", "one hundred and four", "one hundred and twenty", "nine hundred and ninety-nine", "1000"]); + expect(recordSpoken({ wins: 0, losses: 0, draws: 0, knockouts: 0 })).toBe("making a pro debut"); + expect(recordSpoken({ wins: 12, losses: 0, draws: 0, knockouts: 8 })).toBe("twelve and oh, eight by knockout"); + expect(recordSpoken({ wins: 19, losses: 5, draws: 1, knockouts: 12 })).toBe("nineteen, five and one, twelve by knockout"); + expect(recordSpoken({ wins: 36, losses: 1, draws: 0, knockouts: 29 }, false)).toBe("thirty-six and one"); + expect(recordSpoken({ wins: 3, losses: 4, draws: 0, knockouts: 0 })).toBe("three and four"); + }); + + it("does not introduce the fighters to someone who arrives after the opening bell", () => { + const speak = vi.fn(); + const director = new CommentaryDirector({ speak }); + feed(director, state(400, "fight"), [], 0); + feed(director, state(400, "countdown", [{}, {}], { round_number: 1, phase_ticks_remaining: 90 }), [], 1); + expect(watch(director, 0, 6)).toEqual([]); + expect(speak).not.toHaveBeenCalled(); + }); + + const decision = (winner: string | null, cards: Array<[number[], number[]]>): FinalMessage => ({ + version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: winner, method: winner === null ? "draw" : "decision", round: 3, + scorecards: cards.map(([one, two], index) => ({ judge: `J${index}`, player_one: one, player_two: two })), ratings: {}, + }); + + it("goes to the scorecards and then names the winner with the judges' verdict", () => { + const speak = vi.fn(); + const director = new CommentaryDirector({ speak }); + feed(director, state(5000, "complete"), [], 0); + director.finish(decision("one", [[[10, 10, 10], [9, 9, 9]], [[10, 10, 9], [9, 9, 10]], [[10, 9, 10], [9, 10, 9]]]), players, true, 1); + expect(director.current(1.1)?.line.card?.title).toBe("WE GO TO THE SCORECARDS"); + director.verdict(3); + const verdict = director.current(3.9); + expect(verdict?.line.card).toEqual({ kicker: "THE WINNER, BY UNANIMOUS DECISION", title: "AZURE VECTOR", detail: "", corner: 0 }); + expect(speak.mock.calls.map((call) => call[0])).toEqual([["Ladies and gentlemen, we go to the scorecards."], ["And the winner, by unanimous decision... Azure Vector!"]]); + }); + + it("scores a draw as a draw", () => { + const director = new CommentaryDirector(); + feed(director, state(5000, "complete"), [], 0); + director.finish(decision(null, [[[10, 10, 10], [9, 10, 10]], [[10, 10, 10], [10, 10, 10]], [[9, 10, 10], [10, 10, 9]]]), players, true, 0); + director.verdict(1); + expect(director.current(1.9)?.line.card).toMatchObject({ kicker: "AFTER THREE ROUNDS", title: "MAJORITY DRAW", corner: null }); + }); + + it("announces a stoppage with the time it came in the round", () => { + const speak = vi.fn(); + const director = new CommentaryDirector({ speak }); + feed(director, state(1, "countdown", [{}, {}], { phase_ticks_remaining: 90 }), [], 0); + feed(director, state(91, "fight", [{}, {}], { phase_ticks_remaining: 3599 }), [], 3); + feed(director, state(1350, "fight", [{}, {}], { phase_ticks_remaining: 2340 }), [], 45); + feed(director, state(1351, "knockdown", [{}, { is_downed: true }], { phase_ticks_remaining: 300 }), [event("knockdown", 1351, { actor_id: "one", target_id: "two", amount: 1 })], 46); + feed(director, state(1651, "complete", [{}, { is_downed: true }], { phase_ticks_remaining: 0 }), [event("result", 1651, { actor_id: "one", detail: "ko" })], 56); + director.finish({ version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "one", method: "ko", round: 1, scorecards: [], ratings: {} }, players, false, 56.2); + director.resultShown(70); + expect(director.current(70.5)?.line.card).toEqual({ kicker: "STOPPED AT 0:42 OF ROUND 1", title: "AZURE VECTOR", detail: "WINS BY KNOCKOUT", corner: 0 }); + expect(speak.mock.calls.at(-1)![0]).toEqual(["The referee stops the contest at 0:42 of round one. Your winner, by knockout... Azure Vector!"]); + }); + + it("calls the knockout on the spot and the replay when it rolls", () => { + const director = new CommentaryDirector(); + feed(director, state(900, "complete", [{}, { is_downed: true }]), [event("knockdown", 900, { actor_id: "one", target_id: "two", amount: 1 }), event("result", 900, { actor_id: "one", detail: "flash_ko" })], 0); + const call = director.current(0.1); + expect(call?.line.urgent).toBe(true); + expect(call?.line.text).toMatch(/Lights out|One punch|Good night/u); + director.replay(6); + expect(watch(director, 6, 12).some((text) => /again|one more time|set it up/u.test(text))).toBe(true); + }); +}); + +describe("reading the fight", () => { + it("sums up the round from the punch counts and gives the analyst's opinion", () => { + const director = new CommentaryDirector(); + const events: CombatEvent[] = []; + for (let index = 0; index < 12; index += 1) events.push(event("punch_start", 100 + index * 20, { actor_id: "one", detail: "left:jab:head" })); + for (let index = 0; index < 9; index += 1) events.push(hit(105 + index * 20, "one", "straight:head", 50)); + for (let index = 0; index < 6; index += 1) events.push(event("punch_start", 110 + index * 20, { actor_id: "two", detail: "left:jab:head" })); + events.push(hit(115, "two", "jab:head", 30)); + feed(director, state(400), events, 0); + watch(director, 0, 30); + feed(director, state(3600, "rest"), [event("bell", 3600, { detail: "round_end" })], 40); + const lines = watch(director, 40, 55); + expect(lines.some((text) => /round 1/u.test(text) && /bell|books/u.test(text))).toBe(true); + expect(lines).toContain("Round 1: Azure Vector landed 9 of 12, Crimson Geometry 1 of 6."); + expect(lines.some((text) => /Azure Vector/u.test(text) && /round/u.test(text) && !/landed/u.test(text))).toBe(true); + }); + + it("has no numbers to read out for a round in which nobody threw a punch", () => { + const director = new CommentaryDirector(); + feed(director, state(3600, "rest"), [event("bell", 3600, { detail: "round_end" })], 0); + expect(watch(director, 0, 15).some((text) => /landed/u.test(text))).toBe(false); + }); + + it("calls a close round close and a knockdown round big", () => { + const close = new CommentaryDirector(); + feed(close, state(400), [hit(100, "one"), hit(130, "two")], 0); + watch(close, 0, 10); + feed(close, state(3600, "rest"), [event("bell", 3600, { detail: "round_end" })], 20); + expect(watch(close, 20, 35).some((text) => /either way|Tough round|Nothing between/u.test(text))).toBe(true); + + const big = new CommentaryDirector(); + feed(big, state(400), [hit(100, "two", "hook:head", 90), event("knockdown", 100, { actor_id: "two", target_id: "one", amount: 1 })], 0); + watch(big, 0, 20); + feed(big, state(3600, "rest"), [event("bell", 3600, { detail: "round_end" })], 30); + expect(watch(big, 30, 45).some((text) => /knockdown/u.test(text) && /Crimson Geometry/u.test(text))).toBe(true); + }); + + it("always calls the bell, straight after the line already on screen", () => { + const director = new CommentaryDirector(); + feed(director, state(3590), [hit(3590, "one", "hook:head", 120)], 0); + const shot = director.current(0.4)!.line; + expect(shot.text).toMatch(/hook/u); + feed(director, state(3600, "rest"), [event("bell", 3600, { detail: "round_end" })], 0.5); + expect(director.current(0.4 + shot.hold + 0.05)?.line.text).toMatch(/round 1/u); + }); + + it("leaves the bell unsaid rather than calling it seconds late", () => { + const director = new CommentaryDirector(); + feed(director, state(3590), [hit(3590, "one", "hook:head", 120)], 0); + const first = director.current(0.4)!.line; + feed(director, state(3595, "knockdown", [{}, { is_downed: true }]), [event("knockdown", 3595, { actor_id: "one", target_id: "two", amount: 1 })], 0.45); + feed(director, state(3600, "rest"), [event("bell", 3600, { detail: "round_end" })], 0.5); + const lines = watch(director, 0.5, 12); + expect(first.text).toMatch(/hook/u); + expect(lines.some((text) => /round 1/u.test(text))).toBe(false); + }); + + it("drops the count lines once the fighter is up and the introductions at the bell", () => { + const director = new CommentaryDirector(); + feed(director, state(100, "knockdown", [{}, { is_downed: true }]), [event("knockdown", 100, { actor_id: "one", target_id: "two", amount: 1 })], 0); + feed(director, state(280, "knockdown", [{}, { is_downed: true }]), [event("count", 280, { target_id: "two", amount: 6 })], 6); + feed(director, state(281, "fight"), [event("get_up", 281, { actor_id: "two", amount: 6 })], 6.05); + expect(watch(director, 6, 14).some((text) => /beat the count|get up\?|trying to/u.test(text))).toBe(false); + const intro = new CommentaryDirector(); + feed(intro, state(1, "countdown", [{}, {}], { phase_ticks_remaining: 240 }), [], 0); + expect(intro.current(0.05)?.line.card).not.toBeNull(); + feed(intro, state(241, "fight"), [event("bell", 241, { detail: "round_start" })], 0.5); + expect(intro.current(1)).toBeNull(); + expect(watch(intro, 1, 10).some((text) => text.startsWith("In the"))).toBe(false); + }); + + it("never asks whether a fighter who is up can beat the count while the referee counts on to the eight", () => { + // The engine's order: a count a second, the rise, the mandatory count on to eight with the phase still the + // knockdown's, two seconds for the referee's look, then the box. Snapshots at 30 Hz, the screen at 60 Hz. + const struggle = /struggling to get up|trying to beat the count|get up\?/u; + for (const rise of [1, 2, 3, 4, 5, 6, 7]) { + for (const offset of [2, 12, 25]) { + const director = new CommentaryDirector(); + const knockdown = 300; + const risen = knockdown + rise * 30 + offset; + const box = knockdown + 8 * 30 + 60; + const afterRise: string[] = []; + let called = false; + for (let tick = knockdown; tick <= box + 90; tick += 1) { + const now = (tick - knockdown) / 30; + const count = Math.floor((tick - knockdown) / 30); + const events: CombatEvent[] = []; + if (tick === knockdown) events.push(event("knockdown", tick, { actor_id: "one", target_id: "two", amount: 1 })); + if (tick > knockdown && (tick - knockdown) % 30 === 0 && count <= 8 && (tick < risen || count > rise)) events.push(event("count", tick, { target_id: "two", amount: count })); + if (tick === risen) events.push(event("get_up", tick, { actor_id: "two", amount: rise })); + if (tick === box) events.push(event("box", tick, { target_id: "two" })); + feed(director, state(tick, tick < box ? "knockdown" : "fight", [{}, { is_downed: tick < risen }]), events, now); + for (const at of [now, now + 1 / 60]) { + const text = director.current(at)?.line.text ?? ""; + called ||= struggle.test(text); + // A line on screen at the rise is given the fade to go. + if (at >= (risen - knockdown) / 30 + 0.3 && struggle.test(text)) afterRise.push(text); + } + } + expect(afterRise, `up at ${rise}`).toEqual([]); + // Down for the six, he is still asked about while he is on the canvas. + if (rise >= 6 && offset === 25) expect(called, `up at ${rise}`).toBe(true); + } + } + }); + + it("introduces the computer as the computer, not by a rating", () => { + const director = new CommentaryDirector(); + const computer = { ...players, two: { ...players.two!, cpu: true, record: { wins: 19, losses: 5, draws: 1, knockouts: 12 } } }; + director.observe({ ...state(1, "countdown", [{}, {}], { phase_ticks_remaining: 90 }), events: [] }, [], computer, 30, 0); + expect(director.current(0.05)?.line.card?.detail).toBe("RATED 1512"); + expect(director.current(1.6)?.line.card?.detail).toBe("19-5-1 (12 KO) · COMPUTER"); + }); + + it("says a hurt fighter was saved by the bell", () => { + const director = new CommentaryDirector(); + feed(director, state(3600, "rest", [{ stunned_ticks: 20 }, {}]), [event("bell", 3600, { detail: "round_end" })], 0); + expect(director.current(0.4)?.line.text).toMatch(/Azure Vector/u); + expect(director.current(0.4)?.line.text).toMatch(/bell/u); + }); + + it("says when a fighter beats the count late and what comes next", () => { + const director = new CommentaryDirector(); + feed(director, state(800, "knockdown", [{}, { is_downed: true }]), [event("knockdown", 600, { actor_id: "one", target_id: "two", amount: 1 }), event("count", 780, { target_id: "two", amount: 6 })], 0); + watch(director, 0, 6); + feed(director, state(860, "fight"), [event("get_up", 860, { actor_id: "two", amount: 8 })], 8); + const lines = watch(director, 8, 20); + expect(lines[0]).toMatch(/eight/u); + expect(lines.some((text) => /finish|survive|clear the head/u.test(text))).toBe(true); + }); + + it("hears the crowd start chanting when one fighter takes over", () => { + const cue = vi.fn(); + const director = new CommentaryDirector({ cue } satisfies CommentaryHooks); + const events = Array.from({ length: 8 }, (_, index) => hit(100 + index * 40, "one", index % 2 === 0 ? "jab:head" : "straight:head", 40)); + feed(director, state(500), events, 0); + feed(director, state(900), [hit(900, "one", "hook:head", 50), hit(930, "one", "jab:head", 30)], 14); + expect(cue).toHaveBeenCalledTimes(1); + expect(cue).toHaveBeenCalledWith("chant"); + }); + + it("notices a cut opening and getting worse on the doctor's scale", async () => { + const { DOCTOR_CUT } = await import("../manifest"); + const director = new CommentaryDirector(); + const cut = (left_cut: number): Partial => ({ trauma: { ...fighter("two").trauma, left_cut } }); + const worse = (text: string): boolean => /worse|pouring|badly/u.test(text); + feed(director, state(100, "fight", [{}, cut(30)]), [], 0); + feed(director, state(130, "fight", [{}, cut(70)]), [], 1); + expect(watch(director, 1, 5).some((text) => /Crimson Geometry/u.test(text) && /left eye/u.test(text))).toBe(true); + // Worse at 60% of the cut the doctor stops a bout on: the old 420 mark, 60% of a 700 doctor, never moved with him. + feed(director, state(900, "fight", [{}, cut(Math.round(DOCTOR_CUT * 0.6) - 1)]), [], 20); + expect(watch(director, 20, 25).some(worse)).toBe(false); + feed(director, state(930, "fight", [{}, cut(Math.round(DOCTOR_CUT * 0.6))]), [], 30); + expect(watch(director, 30, 35).some(worse)).toBe(true); + feed(director, state(960, "fight", [{}, cut(Math.round(DOCTOR_CUT * 0.8))]), [], 40); + expect(watch(director, 40, 45).some((text) => /doctor/u.test(text))).toBe(true); + }); + + it("has the doctor look at a cut or a swelling near his stoppage at the bell", async () => { + const { DOCTOR_CUT, DOCTOR_SWELLING } = await import("../manifest"); + const doctorLooks = (trauma: Partial): boolean => { + const director = new CommentaryDirector(); + feed(director, state(3600, "rest", [{}, { trauma: { ...fighter("two").trauma, ...trauma } }]), [event("bell", 3600, { detail: "round_end" })], 0); + return watch(director, 0, 20).some((text) => /doctor/u.test(text) && /Crimson Geometry/u.test(text)); + }; + expect(doctorLooks({ left_cut: Math.round(DOCTOR_CUT * 0.7) })).toBe(true); + expect(doctorLooks({ left_cut: Math.round(DOCTOR_CUT * 0.7) - 1 })).toBe(false); + expect(doctorLooks({ swelling: Math.round(DOCTOR_SWELLING * 0.75) })).toBe(true); + expect(doctorLooks({ swelling: Math.round(DOCTOR_SWELLING * 0.75) - 1 })).toBe(false); + }); + + it("does not call a big shot that the guard took", () => { + const blocked = new CommentaryDirector(); + const block = event("block", 100, { actor_id: "two", target_id: "one", action_id: "p1" }); + const leaked = { ...hit(100, "one", "hook:head", 70, "counter_hit"), action_id: "p1" }; + feed(blocked, state(100), [block, leaked], 0); + expect(watch(blocked, 0, 4).some((text) => /counter|Big|thudding|flush|felt/u.test(text))).toBe(false); + const clean = new CommentaryDirector(); + feed(clean, state(100), [{ ...hit(100, "one", "hook:head", 70, "counter_hit"), action_id: "p2" }], 0); + expect(watch(clean, 0, 4).some((text) => /counter|Big|thudding|flush|felt/u.test(text))).toBe(true); + }); + + it("notices an eye swelling and calls it shut only when the engine says it has closed", () => { + const director = new CommentaryDirector(); + const eye = (left_eye: number): Partial => ({ trauma: { ...fighter("two").trauma, left_eye } }); + feed(director, state(100, "fight", [{}, eye(520)]), [], 0); + const early = watch(director, 0, 4); + expect(early.some((text) => /Crimson Geometry/u.test(text) && /left eye/u.test(text) && /swelling|trouble seeing/u.test(text))).toBe(true); + feed(director, state(400, "fight", [{}, eye(760)]), [], 10); + expect(watch(director, 10, 14).some((text) => /closed|shut|one eye/u.test(text))).toBe(false); + feed(director, state(430, "fight", [{}, eye(780)]), [event("eye_shut", 430, { actor_id: "one", target_id: "two", detail: "left" })], 20); + expect(watch(director, 20, 24).some((text) => /closed|shut|one eye/u.test(text))).toBe(true); + }); + + it("sends them back to it after the eight count", () => { + const director = new CommentaryDirector(); + feed(director, state(200, "knockdown"), [event("box", 200, { target_id: "two" })], 0); + const lines = watch(director, 0, 4); + expect(lines.some((text) => /Crimson Geometry|Azure Vector/u.test(text) && /referee|Box|Here comes/u.test(text))).toBe(true); + }); + + it("calls the blind side, a parry and the corner's work", () => { + const blind = new CommentaryDirector(); + feed(blind, state(100), [event("blind_side", 100, { actor_id: "one", target_id: "two", detail: "left" })], 0); + expect(watch(blind, 0, 4).some((text) => /blind side|can't see|left eye/u.test(text))).toBe(true); + const parry = new CommentaryDirector(); + feed(parry, state(100), [event("parry", 100, { actor_id: "one", target_id: "two", amount: 20 })], 0); + expect(watch(parry, 0, 4).some((text) => /^(Azure Vector picks|Beautiful parry from Azure Vector|Azure Vector slaps)/u.test(text))).toBe(true); + for (const [pick, pattern] of [["cut", /cut/u], ["swelling", /swelling|enswell/u], ["breath", /breath/u]] as const) { + const corner = new CommentaryDirector(); + feed(corner, state(100, "rest"), [event("corner", 100, { actor_id: "two", detail: pick })], 0); + expect(watch(corner, 0, 4).some((text) => /Crimson Geometry/u.test(text) && pattern.test(text))).toBe(true); + } + }); + + it("calls a body shot folding a fighter and the knockdown it causes", () => { + const director = new CommentaryDirector(); + feed(director, state(100), [event("body_collapse", 100, { actor_id: "one", target_id: "two", amount: 10 })], 0); + expect(watch(director, 0, 3).some((text) => /body/iu.test(text) && /Crimson Geometry/u.test(text))).toBe(true); + const down = new CommentaryDirector(); + feed(down, state(110, "knockdown", [{}, { is_downed: true }]), [event("knockdown", 110, { actor_id: "one", target_id: "two", amount: 1, detail: "body" })], 0); + expect(watch(down, 0, 3).some((text) => /body shot|takes a knee|downstairs/u.test(text))).toBe(true); + }); + + it("says once a round that a fighter has run out of gas", () => { + const director = new CommentaryDirector(); + const tired: [Partial, Partial] = [{ stamina: 150, maximum_stamina: 1000 }, {}]; + feed(director, state(100, "fight", tired), [], 0); + feed(director, state(140, "fight", tired), [], 1); + expect(watch(director, 1, 3)).toEqual([]); + feed(director, state(170, "fight", tired), [], 2); + feed(director, state(400, "fight", tired), [event("exhausted", 400, { actor_id: "one" })], 12); + const lines = watch(director, 2, 20); + expect(lines.filter((text) => /Azure Vector/u.test(text))).toHaveLength(1); + }); + + it("knows a hurt fighter who grabs hold is holding on", () => { + const director = new CommentaryDirector(); + feed(director, state(100), [event("stun", 100, { actor_id: "two", target_id: "one", amount: 60 })], 0); + watch(director, 0, 6); + feed(director, state(140), [event("clinch", 140, { actor_id: "one", target_id: "two" })], 7); + expect(watch(director, 7, 12).some((text) => /hold/u.test(text) && /Azure Vector/u.test(text))).toBe(true); + }); + + it("follows a low blow with a warning", () => { + const director = new CommentaryDirector(); + feed(director, state(100, "foul_recovery", [{ warnings: 1 }, {}]), [event("foul", 100, { actor_id: "one", target_id: "two", detail: "low_blow" })], 0); + const lines = watch(director, 0, 12); + expect(lines[0]).toMatch(/low|below the belt/iu); + expect(lines.some((text) => /warn/iu.test(text))).toBe(true); + }); + + it("fades a line in and out", () => { + const director = new CommentaryDirector(); + feed(director, state(100), [event("stun", 100, { actor_id: "two", target_id: "one", amount: 60 })], 0); + const first = director.current(0.3)!; + expect(first.opacity).toBe(0); + expect(first.entering).toBe(1); + const settled = director.current(0.7)!; + expect(settled.opacity).toBe(1); + expect(settled.entering).toBe(0); + const end = 0.3 + settled.line.hold; + expect(director.current(end - 0.1)!.opacity).toBeCloseTo(0.4); + expect(director.current(end + 0.01)).toBeNull(); + }); +}); + +describe("crowd tension", () => { + it("rises for a hurt fighter, a fighter pinned on the ropes and a long count", () => { + expect(crowdTension(state(1))).toBe(0); + expect(crowdTension(state(1, "fight", [{}, { stunned_ticks: 10 }]))).toBe(0.75); + expect(crowdTension(state(1, "fight", [{}, { poise: 100 }]))).toBe(0.45); + expect(crowdTension(state(1, "fight", [{ x: 360, y: 0 }, { x: 470, y: 0 }]))).toBe(0.4); + expect(crowdTension(state(1, "fight", [{ x: 0, y: 0 }, { x: 470, y: 0 }]))).toBe(0); + expect(crowdTension(state(1, "knockdown", [{}, { is_downed: true, get_up_count: 5 }]))).toBeCloseTo(0.65); + expect(crowdTension(state(1, "rest", [{}, { stunned_ticks: 10 }]))).toBe(0); + }); +}); diff --git a/web/hands/src/render/commentary.ts b/web/hands/src/render/commentary.ts new file mode 100644 index 00000000..0c08dad0 --- /dev/null +++ b/web/hands/src/render/commentary.ts @@ -0,0 +1,870 @@ +import { DOCTOR_CUT, DOCTOR_SWELLING, ROCKED_MAX_TICKS } from "../manifest"; +import { isDebut, recordCard } from "../record"; +import { styleTag, styleTitle } from "../styles"; +import type { CombatEvent, EngineSnapshot, FighterRecord, FighterSnapshot, FighterStyle, FinalMessage, MatchPhase, PublicPlayer } from "../types"; +import { decisionLabel, wasBlocked } from "./hud"; + +export type Speaker = "play" | "colour" | "announcer"; + +/** An announcer's card: a small kicker over a name or a verdict, in a corner's colour when it presents a fighter. */ +export interface AnnouncerCard { + readonly kicker: string; + readonly title: string; + readonly detail: string; + readonly corner: 0 | 1 | null; +} + +export interface BroadcastLine { + readonly speaker: Speaker; + readonly text: string; + readonly priority: number; + /** Cuts in over a lesser line and skips the pause between lines. */ + readonly urgent: boolean; + /** Seconds on screen. */ + readonly hold: number; + readonly card: AnnouncerCard | null; +} + +export interface Caption { + readonly line: BroadcastLine; + /** 0 to 1 through the fade in and out. */ + readonly opacity: number; + /** 1 on the first frame of the entrance, 0 once it has settled. */ + readonly entering: number; +} + +export type CrowdCue = "chant"; + +export interface CommentaryHooks { + /** Lines for the announcer's voice, in order. */ + readonly speak?: (lines: readonly string[]) => void; + readonly cue?: (cue: CrowdCue) => void; +} + +/** The broadcast team (fictional) as named on the caption. */ +export const BROADCAST_VOICES: Readonly> = { play: "CALLAHAN", colour: "OKAFOR", announcer: "RING ANNOUNCER" }; + +/** Lines appear a beat after the action, as a commentator reacts. */ +const REACTION_SECONDS = 0.3; +const GAP_SECONDS = 0.9; +const FADE_SECONDS = 0.25; +const STALE_SECONDS = 2.6; +const URGENT_STALE_SECONDS = 4.5; +/** An urgent line may cut in once the line on screen has been up this long. */ +const PREEMPT_AFTER_SECONDS = 0.8; +const MAX_QUEUE = 6; +const ANNOUNCER_HOLD_SECONDS = 9; +const SCORECARDS_HOLD_SECONDS = 6; + +const COMBO_GAP_TICKS = 42; +const JAB_WINDOW_SECONDS = 8; +const TAKEOVER_WINDOW_SECONDS = 15; +const EVASION_WINDOW_SECONDS = 10; +/** A cut is called as it opens, worse at 60% of the cut the doctor stops a bout on, and near his limit at 80%. */ +const CUT_MARKS = [60, Math.round(DOCTOR_CUT * 0.6), Math.round(DOCTOR_CUT * 0.8)] as const; +/** At the bell the doctor looks at a cut or a swelling this near his stoppage. */ +const DOCTOR_LOOK = { cut: Math.round(DOCTOR_CUT * 0.7), swelling: Math.round(DOCTOR_SWELLING * 0.75) } as const; +/** Below the engine's shut eye (manifest blind_side.eye_threshold, 700), so the swelling is noticed first. */ +const EYE_SWELLING = 500; +const TIRED_RATIO = 0.2; +const RECOVERED_RATIO = 0.3; +const HURT_POISE = 120; + +const LINES = { + knockdown: ["Down goes {b}!", "{a} drops {b}!", "{b} is down! What a shot from {a}!", "Down goes {b}! {a} found the chin!"], + knockdownAgain: ["{b} is down again!", "Down for the second time! {b} is in deep trouble!", "{a} puts {b} down again!"], + knockdownCounter: ["Down goes {b}, off a perfect counter!", "{a} times it, and {b} is down!"], + struggle: ["{b} is struggling to get up...", "{b} is trying to beat the count...", "Can {b} get up?"], + upLate: ["{b} beats the count at {n}!", "Up at {n}! {b} is still in this!", "{b} makes it up at {n}, but the legs look heavy."], + upEarly: ["{b} is up at {n}.", "{b} is back up at the count of {n}.", "Up at {n}. {b} wants to keep fighting."], + finishCall: ["{a} has to go for the finish now.", "{b} needs to survive the next minute.", "{b} has to hold and clear the head."], + hurt: ["{b} is hurt!", "{a} has {b} in trouble!", "{b}'s legs just buckled!", "Big shot! {b} is wobbled!", "{b} is rocked! Keep the pressure on, {a}!"], + hurtBadly: ["{b} is out on their feet!", "{b} is in all sorts of trouble!", "{b} is badly hurt!"], + guardBreak: ["{a} blasts right through the guard!", "The guard is gone! {b} can't keep those hands up!", "{a} breaks the guard down!"], + counter: ["What a counter from {a}!", "{a} times it perfectly! A {punch} on the counter!", "{b} walked right into that {punch}!", "Counter {punch} from {a}! Beautiful timing."], + bigShot: ["Big {punch} from {a}!", "{a} lands a thudding {punch}!", "That {punch} landed flush!", "Huge {punch}! {b} felt that one."], + cut: ["{b} is cut over the {side} eye.", "There's blood now. A cut over {b}'s {side} eye.", "That's opened a cut over the {side} eye of {b}."], + cutWorse: ["That cut over {b}'s {side} eye is getting worse.", "The blood is pouring from that {side} eye now.", "{b} is bleeding badly from the {side} eye."], + cutDoctor: ["The doctor will be watching that cut very closely.", "If that cut gets any worse, the doctor could stop this."], + eyeSwelling: ["{b}'s {side} eye is swelling badly.", "{b} is having trouble seeing out of that {side} eye."], + eyeShut: ["{b}'s {side} eye has closed completely!", "That {side} eye is shut! {b} can't see them coming from that side.", "{b} is fighting with one eye now."], + blindSide: ["{a} is working the blind side.", "{b} never saw that one. Nothing is getting through that {side} eye.", "Smart from {a}, punching where {b} can't see."], + parried: ["{a} picks it off and {b} is wide open!", "Beautiful parry from {a}! {b} is off balance.", "{a} slaps it away and {b} is left stranded."], + bodyCollapse: ["Body shot! {b} is going down!", "{a} digs to the body and {b} is folding!", "Oh, that body shot! {b} is in agony!"], + knockdownBody: ["Down goes {b} from the body shot!", "{b} takes a knee! That body shot took everything out!", "{a} goes downstairs and {b} can't take it!"], + cornerCut: ["{a}'s cutman is working on that cut.", "They're working hard on the cut in {a}'s corner."], + cornerSwelling: ["{a}'s corner is getting the swelling down.", "Ice and the enswell on that eye in {a}'s corner."], + cornerBreath: ["{a}'s corner wants the breath back first. Big drink of water.", "{a} is told to breathe and settle down."], + combo: ["A three-punch combination from {a}!", "{a} puts it together beautifully.", "Lovely combination from {a}!"], + unanswered: ["Five unanswered punches from {a}!", "{a} is unloading! {b} has to fire back!", "{b} is just covering up as {a} lets the hands go!"], + jab: ["{a}'s jab is finding a home.", "Stiff jab from {a}, and another.", "{a} keeps popping that jab in {b}'s face."], + body: ["{a} is going to work on the body.", "{a} keeps digging to the body.", "Another one downstairs from {a}."], + payoff: ["The body work is paying off. {b} is slowing down.", "Those body shots are taking it out of {b}.", "{a} has invested in the body, and {b} is paying for it now."], + tiring: ["{b} is running on empty.", "{b} is breathing hard. Those punches have nothing on them.", "{b} has punched out. The tank is empty."], + takeover: ["{a} is taking over this round.", "This is all {a} right now.", "{a} is in complete control."], + clinch: ["{a} ties {b} up.", "{a} grabs hold.", "They're tied up in the clinch."], + holdingOn: ["{a} is hurt and holding on!", "{a} grabs on for dear life!", "Smart from {a}, holding on to clear the head."], + refBreak: ["The referee breaks them.", "The referee steps in to separate them.", "Break! The referee pulls them apart."], + taunt: ["{a} drops the hands and invites {b} in!", "Showboating from {a}! The crowd loves it.", "{a} is taunting {b} now!", "{a} sticks the chin out. Come and get it!"], + lowBlow: ["Low blow from {a}! The referee steps in.", "That's below the belt! {b} is doubled over.", "{a} goes low, and the referee calls time."], + headbutt: ["Headbutt from {a}! The referee calls time.", "{a} leads with the head! That's a foul."], + warning: ["A warning for {a}. One more and it costs a point.", "The referee warns {a}. That can't happen again."], + deduction: ["The referee takes a point from {a}!", "That costs {a} a point!", "Point deduction! {a} has been warned enough."], + resume: ["And we're back underway.", "Time in. Let's box."], + box: ["The referee waves them on. {b} has to survive now.", "Box! {a} will be looking to finish it.", "{b} has convinced the referee. Here comes {a}."], + evade: ["{a} slips it! Lovely head movement.", "{b} misses, and {a} was never there.", "Great defence from {a}."], + parry: ["{a} picks that off with the gloves.", "{a} parries it away.", "Tight defence from {a}."], + whiff: ["{a} misses wildly with the {punch}.", "Big swing and a miss from {a}.", "{a} throws everything into that {punch} and hits nothing but air."], + southpaw: ["{a} switches to southpaw.", "{a} turns southpaw now."], + orthodox: ["{a} goes back to orthodox.", "{a} switches back to orthodox."], + roundStart: ["Round {r}. Here we go.", "We're underway in round {r}.", "Round {r} is on."], + roundEnd: ["That's the bell to end round {r}.", "The bell ends round {r}.", "And that's round {r} in the books."], + savedByBell: ["Saved by the bell! {b} was in real trouble.", "{b} is saved by the bell!", "The bell rescues {b}!"], + finalBell: ["That's the final bell!", "The final bell! We go to the scorecards."], + summary: ["Round {r}: {a} landed {x} of {y}, {b} {z} of {w}."], + roundFor: ["I had that round for {a}.", "{a} took that round.", "That was {a}'s round."], + roundBig: ["The knockdown makes that a big round for {a}.", "{a} wins that round big with the knockdown."], + roundClose: ["Close round. That one could go either way.", "Tough round to score.", "Nothing between them in that round."], + pullingAway: ["{a} is pulling away on the cards.", "{b} needs something big. {a} is building a lead."], + doctor: ["The ringside doctor is taking a close look at {b}.", "The doctor is in {b}'s corner checking that cut."], + ko: ["It's all over! {a} wins by knockout!", "{b} can't beat the count! {a} wins it!", "That's it! {a} has knocked out {b}!"], + flashKo: ["Lights out! {b} is out cold!", "One punch, and it's over! {a} wins!", "Good night! {a} ends it with one shot!"], + tko: ["Three knockdowns, and it's over! {a} wins!", "The referee waves it off! {a} wins by stoppage!"], + doctorStop: ["The doctor has seen enough! It's stopped on the cut!", "The doctor stops it! {b} can't continue!"], + disqualified: ["{b} is disqualified!", "That's it! {b} has been thrown out!"], + forfeit: ["{b} has left the ring. {a} wins by forfeit."], + replay: ["Here it is again.", "Let's see that one more time.", "Watch {a} set it up..."], +} as const satisfies Record; + +type LineKey = keyof typeof LINES; + +const RESULT_LINES: Readonly>> = { ko: "ko", flash_ko: "flashKo", tko: "tko", doctor_stoppage: "doctorStop", disqualification: "disqualified", forfeit: "forfeit" }; + +/** Lines about a moment that ends with its phase: "can he beat the count?" means nothing once he is up. */ +const PHASES: Readonly>>> = { struggle: new Set(["knockdown"]) }; +/** The moment of "can he beat the count?", which ends when he gets up even though the phase runs on to the eight. */ +const STRUGGLE_MOMENT = "struggle"; +const COUNTDOWN_ONLY: ReadonlySet = new Set(["countdown"]); +/** The bell is called as it rings, or straight after the line on screen, or not at all: seconds late reads as a mistake. */ +const BELL_FRESH_SECONDS = 3.2; + +const URGENT: ReadonlySet = new Set(["knockdown", "knockdownAgain", "knockdownCounter", "knockdownBody", "bodyCollapse", "upLate", "upEarly", "hurt", "hurtBadly", "lowBlow", "headbutt", "deduction", "ko", "flashKo", "tko", "doctorStop", "disqualified", "forfeit", "replay", "savedByBell", "finalBell", "roundEnd", "roundStart"]); + +/** Knockdowns over hurt fighters over big counters over cuts over combinations over colour. */ +const PRIORITY: Readonly> = { + ko: 98, flashKo: 98, tko: 98, doctorStop: 98, disqualified: 98, forfeit: 98, + knockdown: 95, knockdownAgain: 95, knockdownCounter: 95, knockdownBody: 95, bodyCollapse: 86, upLate: 90, upEarly: 90, replay: 88, + deduction: 84, lowBlow: 82, headbutt: 82, hurt: 80, box: 78, hurtBadly: 80, holdingOn: 76, savedByBell: 74, finalBell: 72, struggle: 70, + guardBreak: 65, eyeShut: 62, counter: 60, parried: 58, bigShot: 55, roundEnd: 52, cut: 50, cutWorse: 50, summary: 50, cutDoctor: 48, doctor: 48, eyeSwelling: 48, + cornerCut: 47, cornerSwelling: 47, cornerBreath: 47, blindSide: 42, + roundFor: 46, roundBig: 46, roundClose: 46, pullingAway: 46, unanswered: 45, finishCall: 44, warning: 42, + combo: 40, payoff: 40, takeover: 38, body: 35, jab: 30, tiring: 30, roundStart: 28, taunt: 25, parry: 22, evade: 20, clinch: 20, + whiff: 18, refBreak: 12, southpaw: 10, orthodox: 10, resume: 10, +}; + +const SPEAKER: Readonly>> = { + finishCall: "colour", payoff: "colour", tiring: "colour", takeover: "colour", holdingOn: "colour", taunt: "colour", parry: "colour", evade: "colour", + warning: "colour", blindSide: "colour", cornerCut: "colour", cornerSwelling: "colour", cornerBreath: "colour", roundFor: "colour", roundBig: "colour", roundClose: "colour", pullingAway: "colour", doctor: "colour", cutDoctor: "colour", jab: "colour", +}; + +/** Seconds before the same kind of line about the same fighter may come again. */ +const COOLDOWN: Readonly>> = { + combo: 6, jab: 20, body: 18, evade: 12, parry: 15, parried: 8, blindSide: 20, whiff: 12, clinch: 15, refBreak: 20, taunt: 10, southpaw: 15, orthodox: 15, bigShot: 4, counter: 3, unanswered: 15, takeover: 25, +}; + +const NUMBER_WORDS = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen"] as const; +const numberWord = (value: number): string => NUMBER_WORDS[value] ?? String(value); + +const UNITS = [...NUMBER_WORDS, "sixteen", "seventeen", "eighteen", "nineteen"] as const; +const TENS = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"] as const; + +/** A number in words, as the ring announcer says it: "thirty-six", "one hundred and four". */ +export function spokenNumber(value: number): string { + if (!Number.isInteger(value) || value < 0 || value >= 1000) return String(value); + if (value < 20) return UNITS[value]!; + if (value < 100) return `${TENS[Math.floor(value / 10)]}${value % 10 === 0 ? "" : `-${UNITS[value % 10]}`}`; + return `${UNITS[Math.floor(value / 100)]} hundred${value % 100 === 0 ? "" : ` and ${spokenNumber(value % 100)}`}`; +} + +/** A record as the ring announcer reads it when there is time: "twelve and three, eight by knockout", "making a pro debut". */ +export function recordSpoken(record: FighterRecord, knockouts = true): string { + if (isDebut(record)) return "making a pro debut"; + const results = record.draws > 0 + ? `${spokenNumber(record.wins)}, ${spokenNumber(record.losses)} and ${spokenNumber(record.draws)}` + : `${spokenNumber(record.wins)} and ${record.losses === 0 ? "oh" : spokenNumber(record.losses)}`; + return knockouts && record.knockouts > 0 ? `${results}, ${spokenNumber(record.knockouts)} by knockout` : results; +} + +/** + * Seconds the announcer's voice takes to read `text`, on the slow side. Windows' default voice + * (Microsoft David) at the page's rate of 0.96 reads about 13 characters a second, breathes at each + * "...", and leaves most of a second around every line. + */ +export function spokenSeconds(text: string): number { + return 0.85 + text.length * 0.077 + (text.split("...").length - 1) * 0.35; +} + +/** Left over at the opening bell for a voice slower than the estimate. */ +const SPOKEN_MARGIN_SECONDS = 0.3; + +export interface IntroducedCorner { + readonly name: string; + readonly record: FighterRecord | undefined; + readonly style: FighterStyle | undefined; +} + +/** + * What the ring announcer says in the `seconds` the opening countdown has: each corner's record and style + * before the name where there is time for both lines, otherwise the names alone, otherwise one + * "Azure Vector... versus Crimson Geometry!", otherwise nothing, as the cards carry it all. + */ +export function introductionScript(corners: readonly [IntroducedCorner, IntroducedCorner], seconds: number): readonly string[] { + const budget = seconds - SPOKEN_MARGIN_SECONDS; + const levels: readonly ((corner: IntroducedCorner) => readonly (string | null)[])[] = [ + (corner) => [corner.record === undefined ? null : recordSpoken(corner.record), styleTitle(corner.style)], + (corner) => [styleTitle(corner.style)], + () => [], + ]; + for (const level of levels) { + const lines = corners.map((corner, seat) => { + const opening = seat === 0 ? "In the blue corner" : "And in the red corner"; + const parts = level(corner).filter((part): part is string => part !== null); + return parts.length === 0 ? `${opening}, ${corner.name}!` : `${opening}, ${parts.join(", ")}... ${corner.name}!`; + }); + if (lines.reduce((total, line) => total + spokenSeconds(line), 0) <= budget) return lines; + } + const versus = `${corners[0].name}... versus ${corners[1].name}!`; + return spokenSeconds(versus) <= budget ? [versus] : []; +} + +type Vars = Partial>; + +export function fillLine(template: string, vars: Vars): string { + const text = template.replace(/\{(a|b|n|side|punch|r|x|y|z|w)\}/gu, (_match, key: keyof Vars) => String(vars[key] ?? "")); + return text.charAt(0).toUpperCase() + text.slice(1); +} + +const mix = (seed: number, salt: number): number => { + let value = Math.imul((seed | 0) ^ Math.imul(salt, 0x9e3779b1), 0x85ebca6b); + value ^= value >>> 13; + value = Math.imul(value, 0xc2b2ae35); + return (value ^ (value >>> 16)) >>> 0; +}; + +/** Class and target from a hit's detail ("hook:head"), ignoring any leading hand. */ +export function punchParts(detail: string): [string | undefined, string | undefined] { + const parts = detail.split(":"); + return [parts.at(-2), parts.at(-1)]; +} + +/** The punch as a commentator names it: "jab", "right hook", "left hand to the body". */ +export function punchName(detail: string, puncher: FighterSnapshot | undefined): string { + const [punchClass = "punch", target = "head"] = punchParts(detail); + const hand = puncher !== undefined && puncher.action === punchClass ? puncher.action_hand : null; + const base = punchClass === "jab" + ? "jab" + : punchClass === "straight" + ? hand === null ? "straight" : `${hand} hand` + : hand === null ? punchClass : `${hand} ${punchClass}`; + return target === "body" ? `${base} to the body` : base; +} + +/** How loud the crowd should murmur: a fighter hurt, a fighter pinned on the ropes, a count running. */ +export function crowdTension(snapshot: EngineSnapshot): number { + if (snapshot.phase === "knockdown") { + const count = Math.max(...snapshot.fighters.map((fighter) => fighter.get_up_count)); + return Math.min(1, 0.3 + count * 0.07); + } + if (snapshot.phase !== "fight") return 0; + let tension = 0; + for (const [index, fighter] of snapshot.fighters.entries()) { + const other = snapshot.fighters[1 - index]!; + if (fighter.is_downed) continue; + if (fighter.stunned_ticks > 0) tension = Math.max(tension, 0.75); + else if (fighter.poise < HURT_POISE + 30) tension = Math.max(tension, 0.45); + const onRopes = Math.max(Math.abs(fighter.x), Math.abs(fighter.y)) > 400 || Math.abs(fighter.x) + Math.abs(fighter.y) > 640; + if (onRopes && Math.hypot(fighter.x - other.x, fighter.y - other.y) < 150) tension = Math.max(tension, 0.4); + } + return tension; +} + +interface Queued { + line: BroadcastLine; + at: number; + expires: number; + moment: string | null; + /** The phases in which the line still makes sense, or null for any. */ + phases: ReadonlySet | null; + /** What the ring announcer reads as the line comes up, if anything. */ + spoken: string | null; +} + +interface Showing { + line: BroadcastLine; + start: number; + end: number; + moment: string | null; + phases: ReadonlySet | null; +} + +interface RoundTally { + thrown: number; + landed: number; + damage: number; + knockdowns: number; + body: number; +} + +interface FighterNotes { + jabs: number[]; + window: number[]; + evasions: number[]; + run: number; + runTick: number; + unanswered: number; + stunnedAt: number; + knockedDownAt: number; + lowSince: number | null; + tiredRound: number; + takeoverRound: number; + payoffRound: number; + cutMarks: [number, number]; + eyeShut: [boolean, boolean]; + eyeSwelling: [boolean, boolean]; + rounds: number; +} + +const blankTally = (): RoundTally => ({ thrown: 0, landed: 0, damage: 0, knockdowns: 0, body: 0 }); +const blankNotes = (): FighterNotes => ({ jabs: [], window: [], evasions: [], run: 0, runTick: -1000, unanswered: 0, stunnedAt: -1000, knockedDownAt: -100_000, lowSince: null, tiredRound: 0, takeoverRound: 0, payoffRound: 0, cutMarks: [0, 0], eyeShut: [false, false], eyeSwelling: [false, false], rounds: 0 }); + +/** + * The broadcast team: turns the fight's events into a paced stream of lines from a play-by-play caller, + * a colour analyst and the ring announcer. Deterministic for the same events and clock. + */ +export class CommentaryDirector { + private readonly queue: Queued[] = []; + private showing: Showing | null = null; + private lastEnd = -Infinity; + private readonly lastChoice = new Map(); + private readonly cooldowns = new Map(); + private readonly notes = new Map(); + private readonly tallies = new Map(); + private players: Readonly> = {}; + private fighters: readonly [FighterSnapshot, FighterSnapshot] | null = null; + private tickRate = 30; + private introduced = false; + private lastPhase: string | null = null; + private round = 0; + private roundLength: { round: number; ticks: number } | null = null; + private fightRemaining = 0; + private struggleCalled = -1; + private lastKnockdown: { actor: string | null; target: string | null; tick: number } | null = null; + private final: FinalMessage | null = null; + private verdictGiven = false; + private resultCalled = false; + private readonly caption = { line: null as unknown as BroadcastLine, opacity: 0, entering: 0 }; + + constructor(private readonly hooks: CommentaryHooks = {}) {} + + /** Takes a snapshot and the events it brought that have not been seen before. */ + observe(snapshot: EngineSnapshot, events: readonly CombatEvent[], players: Readonly>, tickRate: number, now: number): void { + this.players = players; + this.fighters = snapshot.fighters; + this.tickRate = tickRate; + this.trackClock(snapshot); + if (!this.introduced) { + this.introduced = true; + if (snapshot.phase === "countdown" && snapshot.round_number === 1) this.introduce(snapshot, now); + } + const result = events.find((event) => event.kind === "result") ?? null; + for (const event of events) this.consider(event, snapshot, result, now); + this.inspect(snapshot, now); + this.lastPhase = snapshot.phase; + this.dropOutOfPhase(snapshot.phase, now); + } + + /** A line about a moment that has passed (a count already beaten, the introductions after the bell) goes unsaid. */ + private dropOutOfPhase(phase: MatchPhase, now: number): void { + for (let index = this.queue.length - 1; index >= 0; index -= 1) { + const phases = this.queue[index]!.phases; + if (phases !== null && !phases.has(phase)) this.queue.splice(index, 1); + } + const showing = this.showing; + if (showing !== null && showing.phases !== null && !showing.phases.has(phase)) showing.end = Math.min(showing.end, now + FADE_SECONDS); + } + + /** The bout's result has arrived. A decision opens with the announcer going to the scorecards. */ + finish(final: FinalMessage, players: Readonly>, ceremony: boolean, now: number): void { + this.players = players; + this.final = final; + if (ceremony) { + this.enqueue(this.announcement("Ladies and gentlemen, we go to the scorecards.", { kicker: "LADIES AND GENTLEMEN", title: "WE GO TO THE SCORECARDS", detail: "", corner: null }, SCORECARDS_HOLD_SECONDS, 99), now, null, 30); + this.hooks.speak?.(["Ladies and gentlemen, we go to the scorecards."]); + } + if (final.method === "forfeit" && !this.resultCalled && final.winner_id !== null) { + this.resultCalled = true; + this.say("forfeit", final.round, now, { a: this.nameOf(final.winner_id), b: this.nameOf(this.otherId(final.winner_id)) }, "result", 0); + } + } + + /** The referee raises the winner's arm (or both arms after a draw). */ + verdict(now: number): void { + const final = this.final; + if (final === null || this.verdictGiven) return; + this.verdictGiven = true; + const label = decisionLabel(final); + if (final.winner_id === null) { + const text = `After ${numberWord(final.round)} rounds, this bout is scored a ${label.toLowerCase()}.`; + this.enqueue(this.announcement(text, { kicker: `AFTER ${numberWord(final.round).toUpperCase()} ROUNDS`, title: label, detail: "", corner: null }, ANNOUNCER_HOLD_SECONDS, 100), now, "verdict", 30); + this.hooks.speak?.([text]); + return; + } + const name = this.nameOf(final.winner_id); + const method = label.toLowerCase(); + this.enqueue(this.announcement(`The winner, by ${method}: ${name}!`, { kicker: `THE WINNER, BY ${label}`, title: name.toUpperCase(), detail: "", corner: this.seatOf(final.winner_id) }, ANNOUNCER_HOLD_SECONDS, 100), now, "verdict", 30); + this.hooks.speak?.([`And the winner, by ${method}... ${name}!`]); + } + + /** The result card is on screen. A bout that did not go to the cards gets its announcement now. */ + resultShown(now: number): void { + const final = this.final; + if (final === null || this.verdictGiven) return; + if (final.method === "decision" || final.method === "draw") { + this.verdict(now); + return; + } + this.verdictGiven = true; + if (final.winner_id === null) return; + const name = this.nameOf(final.winner_id); + const how = ({ ko: "knockout", flash_ko: "knockout", tko: "technical knockout", doctor_stoppage: "doctor stoppage", disqualification: "disqualification", forfeit: "forfeit" } as Partial>)[final.method] ?? final.method.replaceAll("_", " "); + const clock = this.stoppageClock(final.round); + const stopped = final.method === "ko" || final.method === "flash_ko" || final.method === "tko" || final.method === "doctor_stoppage"; + const kicker = stopped && clock !== null ? `STOPPED AT ${clock} OF ROUND ${final.round}` : `ROUND ${final.round}`; + const text = stopped && clock !== null ? `${name} wins by ${how} at ${clock} of round ${final.round}.` : `${name} wins by ${how} in round ${final.round}.`; + this.enqueue(this.announcement(text, { kicker, title: name.toUpperCase(), detail: `WINS BY ${how.toUpperCase()}`, corner: this.seatOf(final.winner_id) }, ANNOUNCER_HOLD_SECONDS, 100), now, "verdict", 30); + const opening = stopped && clock !== null ? `The referee stops the contest at ${clock} of round ${numberWord(final.round)}. ` : ""; + this.hooks.speak?.([`${opening}Your winner, by ${how}... ${name}!`]); + } + + /** The knockout replay has started. */ + replay(now: number): void { + const knockdown = this.lastKnockdown; + this.say("replay", knockdown?.tick ?? 0, now, { a: this.nameOf(knockdown?.actor ?? null), b: this.nameOf(knockdown?.target ?? null) }, "replay", 0.6, 6); + } + + /** The line on screen at `now`, or null. Reuses one object, so read it before the next call. */ + current(now: number): Caption | null { + for (let index = this.queue.length - 1; index >= 0; index -= 1) if (this.queue[index]!.expires <= now) this.queue.splice(index, 1); + if (this.showing !== null && now >= this.showing.end) { + this.lastEnd = this.showing.end; + this.showing = null; + } + let best: Queued | null = null; + for (const entry of this.queue) { + if (entry.at > now) continue; + if (best === null || entry.line.priority > best.line.priority || (entry.line.priority === best.line.priority && entry.at < best.at)) best = entry; + } + if (best !== null) { + const showing = this.showing; + const start = showing === null + ? best.line.urgent || now >= this.lastEnd + GAP_SECONDS + : best.line.urgent && best.line.priority > showing.line.priority && now - showing.start >= PREEMPT_AFTER_SECONDS; + if (start) { + this.queue.splice(this.queue.indexOf(best), 1); + this.showing = { line: best.line, start: now, end: now + best.line.hold, moment: best.moment, phases: best.phases }; + // The ring announcer reads a corner's introduction as its card comes up. + if (best.spoken !== null) this.hooks.speak?.([best.spoken]); + } + } + const showing = this.showing; + if (showing === null) return null; + this.caption.line = showing.line; + this.caption.opacity = Math.max(0, Math.min(1, (now - showing.start) / FADE_SECONDS, (showing.end - now) / FADE_SECONDS)); + this.caption.entering = Math.max(0, 1 - (now - showing.start) / FADE_SECONDS); + return this.caption; + } + + private trackClock(snapshot: EngineSnapshot): void { + if (snapshot.phase === "fight") { + if (this.lastPhase === "countdown" || this.lastPhase === "rest") this.roundLength = { round: snapshot.round_number, ticks: snapshot.phase_ticks_remaining + 1 }; + this.fightRemaining = snapshot.phase_ticks_remaining; + } + if (snapshot.round_number !== this.round) { + this.round = snapshot.round_number; + this.tallies.clear(); + } + } + + /** "m:ss" into the round when the bout was stopped, when this client saw the round begin. */ + private stoppageClock(round: number): string | null { + const length = this.roundLength; + if (length === null || length.round !== round) return null; + const seconds = Math.max(0, Math.floor((length.ticks - this.fightRemaining) / this.tickRate)); + return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`; + } + + private introduce(snapshot: EngineSnapshot, now: number): void { + const seconds = snapshot.phase_ticks_remaining / this.tickRate; + if (seconds < 1.2) return; + const names = snapshot.fighters.map((fighter) => this.nameOf(fighter.player_id)); + const corners = snapshot.fighters.map((fighter, seat) => ({ name: names[seat]!, record: this.players[fighter.player_id]?.record, style: fighter.style })); + // Only what fits the countdown is read; the bell would cut the rest, names last. + const script = introductionScript([corners[0]!, corners[1]!], seconds); + // The red card comes up as the blue corner's line ends, so each line is read with its own card. + const blueHold = Math.max(1.4, script.length === 2 ? spokenSeconds(script[0]!) : seconds / 2); + const holds = [blueHold, Math.max(1.4, seconds - blueHold)] as const; + for (const seat of [0, 1] as const) { + const corner = seat === 0 ? "blue" : "red"; + const player = this.players[snapshot.fighters[seat].player_id]; + const style = styleTag(snapshot.fighters[seat].style); + const detail = player === undefined ? "" : `${style === null ? "" : `${style} · `}${player.record === undefined ? "" : `${recordCard(player.record)} · `}${player.cpu === true ? "COMPUTER" : `RATED ${player.rating}`}`; + this.enqueue(this.announcement(`In the ${corner} corner, ${names[seat]}.`, { kicker: `IN THE ${corner.toUpperCase()} CORNER`, title: names[seat]!.toUpperCase(), detail, corner: seat }, holds[seat], 97), now + seat * blueHold, null, 30, COUNTDOWN_ONLY, script[seat] ?? null); + } + } + + private consider(event: CombatEvent, snapshot: EngineSnapshot, result: CombatEvent | null, now: number): void { + const actor = event.actor_id; + const target = event.target_id; + const moment = `${event.tick}:${target ?? actor ?? ""}`; + const names = { a: this.nameOf(actor), b: this.nameOf(target) }; + switch (event.kind) { + case "punch_start": + if (actor !== null) this.tally(actor).thrown += 1; + return; + case "hit": + case "counter_hit": + if (actor !== null && target !== null && !wasBlocked(event, snapshot.events)) this.landed(event, actor, target, snapshot, now); + return; + case "stun": { + if (target === null) return; + this.notesFor(target).stunnedAt = event.tick; + const hurt = this.fighter(target); + const badly = hurt !== undefined && (hurt.poise < HURT_POISE || hurt.stunned_ticks >= ROCKED_MAX_TICKS - 6); + this.say(badly ? "hurtBadly" : "hurt", event.event_id, now, names, moment); + return; + } + case "guard_break": + this.say("guardBreak", event.event_id, now, names, moment); + return; + case "knockdown": { + if (target === null) return; + this.lastKnockdown = { actor, target, tick: event.tick }; + this.notesFor(target).knockedDownAt = event.tick; + if (actor !== null) this.tally(actor).knockdowns += 1; + if (result !== null) return; + const countered = snapshot.events.some((other) => other.kind === "counter_hit" && other.tick === event.tick && other.target_id === target); + this.say(event.amount >= 2 ? "knockdownAgain" : event.detail === "body" ? "knockdownBody" : countered ? "knockdownCounter" : "knockdown", event.event_id, now, names, moment); + return; + } + case "body_collapse": + this.say("bodyCollapse", event.event_id, now, names, moment); + return; + case "parry": + this.say("parried", event.event_id, now, names, moment, REACTION_SECONDS, undefined, actor); + return; + case "eye_shut": + if (target === null || (event.detail !== "left" && event.detail !== "right")) return; + this.notesFor(target).eyeShut[event.detail === "left" ? 0 : 1] = true; + this.say("eyeShut", event.event_id, now, { ...names, side: event.detail }, moment); + return; + case "blind_side": + if (event.detail !== "left" && event.detail !== "right") return; + this.say("blindSide", event.event_id, now, { ...names, side: event.detail }, null, REACTION_SECONDS, undefined, actor); + return; + case "corner": { + const key = event.detail === "cut" ? "cornerCut" : event.detail === "swelling" ? "cornerSwelling" : event.detail === "breath" ? "cornerBreath" : null; + if (key !== null) this.say(key, event.event_id, now, names, null, REACTION_SECONDS, undefined, actor); + return; + } + case "count": + // The referee counts on to the mandatory eight after the rise, when nobody is struggling to get up. + if (event.amount >= 6 && this.struggleCalled !== this.lastKnockdown?.tick && this.fighter(target)?.is_downed === true) { + this.struggleCalled = this.lastKnockdown?.tick ?? event.tick; + this.say("struggle", event.event_id, now, { a: this.nameOf(this.otherId(target)), b: this.nameOf(target) }, STRUGGLE_MOMENT); + } + return; + case "get_up": { + // He is up, and the phase is still the knockdown's: a "can he beat the count?" that is waiting or on + // screen is about a moment that has passed. + for (let index = this.queue.length - 1; index >= 0; index -= 1) if (this.queue[index]!.moment === STRUGGLE_MOMENT) this.queue.splice(index, 1); + if (this.showing?.moment === STRUGGLE_MOMENT) this.showing.end = Math.min(this.showing.end, now + FADE_SECONDS); + const up = { a: this.nameOf(this.otherId(actor)), b: this.nameOf(actor), n: numberWord(event.amount) }; + this.say(event.amount >= 8 ? "upLate" : "upEarly", event.event_id, now, up, moment); + this.say("finishCall", event.event_id, now, up, null, 3.4, 4); + return; + } + case "perfect_block": + this.say("parry", event.event_id, now, names, moment, REACTION_SECONDS, undefined, actor); + return; + case "evade": { + if (actor === null) return; + const notes = this.notesFor(actor); + notes.evasions = notes.evasions.filter((tick) => tick > event.tick - EVASION_WINDOW_SECONDS * this.tickRate); + notes.evasions.push(event.tick); + if (this.fighter(target)?.action_power === "power" || notes.evasions.length >= 3) { + if (this.say("evade", event.event_id, now, names, moment, REACTION_SECONDS, undefined, actor)) notes.evasions.length = 0; + } + return; + } + case "whiff": { + const puncher = this.fighter(actor); + if (puncher?.action_power !== "power" || puncher.action === null) return; + this.say("whiff", event.event_id, now, { ...names, punch: punchName(`${puncher.action}:${puncher.action_target ?? "head"}`, puncher) }, moment, REACTION_SECONDS, undefined, actor); + return; + } + case "clinch": { + if (actor === null) return; + const holder = this.fighter(actor); + const hurt = event.tick - this.notesFor(actor).stunnedAt <= 3 * this.tickRate || (holder !== undefined && holder.poise < HURT_POISE + 30); + this.say(hurt ? "holdingOn" : "clinch", event.event_id, now, names, moment, REACTION_SECONDS, undefined, actor); + return; + } + case "referee_break": + this.say("refBreak", event.event_id, now, names, null, REACTION_SECONDS, undefined, "referee"); + return; + case "taunt": + this.say("taunt", event.event_id, now, { a: names.a, b: this.nameOf(this.otherId(actor)) }, moment, REACTION_SECONDS, undefined, actor); + return; + case "foul": { + this.say(event.detail === "headbutt" ? "headbutt" : "lowBlow", event.event_id, now, names, moment); + if (this.fighter(actor)?.warnings === 1) this.say("warning", event.event_id, now, names, null, 3.6, 4); + return; + } + case "point_deduction": + this.say("deduction", event.event_id, now, names, `deduction:${event.tick}`); + return; + case "resume": + this.say("resume", event.event_id, now, names, null); + return; + case "box": + this.say("box", event.event_id, now, { a: this.nameOf(this.otherId(target)), b: this.nameOf(target) }, moment); + return; + case "stance": + this.say(event.detail === "southpaw" ? "southpaw" : "orthodox", event.event_id, now, names, null, REACTION_SECONDS, undefined, actor); + return; + case "exhausted": + if (actor !== null) this.tired(actor, event.event_id, now); + return; + case "bell": + if (event.detail === "round_start") { + this.tallies.clear(); + if (snapshot.round_number >= 2) this.say("roundStart", event.event_id, now, { r: snapshot.round_number }, null, REACTION_SECONDS, BELL_FRESH_SECONDS); + } else if (event.detail === "round_end") { + this.roundEnded(event, snapshot, result, now); + } + return; + case "result": + this.resulted(event, now); + return; + default: + return; + } + } + + private landed(event: CombatEvent, actor: string, target: string, snapshot: EngineSnapshot, now: number): void { + const [punchClass, area] = punchParts(event.detail); + const tally = this.tally(actor); + tally.landed += 1; + tally.damage += event.amount; + if (area === "body") tally.body += 1; + const notes = this.notesFor(actor); + const other = this.notesFor(target); + other.run = 0; + other.unanswered = 0; + notes.run = event.tick - notes.runTick <= COMBO_GAP_TICKS ? notes.run + 1 : 1; + notes.runTick = event.tick; + notes.unanswered += 1; + notes.window = notes.window.filter((tick) => tick > event.tick - TAKEOVER_WINDOW_SECONDS * this.tickRate); + notes.window.push(event.tick); + const moment = `${event.tick}:${target}`; + const puncher = snapshot.fighters.find((fighter) => fighter.player_id === actor); + const names = { a: this.nameOf(actor), b: this.nameOf(target), punch: punchName(event.detail, puncher) }; + const powered = puncher?.action_power === "power" && puncher.action === punchClass; + if (event.kind === "counter_hit" && event.amount >= 40) this.say("counter", event.event_id, now, names, moment, REACTION_SECONDS, undefined, actor); + else if (event.amount >= 85 || (powered && area === "head" && event.amount >= 60)) this.say("bigShot", event.event_id, now, names, moment, REACTION_SECONDS, undefined, actor); + if (notes.unanswered === 5) this.say("unanswered", event.event_id, now, names, moment, REACTION_SECONDS, undefined, actor); + else if (notes.run === 3) this.say("combo", event.event_id, now, names, moment, REACTION_SECONDS, undefined, actor); + if (punchClass === "jab") { + notes.jabs = notes.jabs.filter((tick) => tick > event.tick - JAB_WINDOW_SECONDS * this.tickRate); + notes.jabs.push(event.tick); + if (notes.jabs.length >= 3 && this.say("jab", event.event_id, now, names, null, REACTION_SECONDS, undefined, actor)) notes.jabs.length = 0; + } + if (area === "body") { + const struck = this.fighter(target); + if (struck !== undefined && tally.body >= 5 && struck.stamina < struck.maximum_stamina * 0.35 && notes.payoffRound !== snapshot.round_number) { + notes.payoffRound = snapshot.round_number; + this.say("payoff", event.event_id, now, names, null); + } else if (tally.body === 4 || tally.body === 8) { + this.say("body", event.event_id, now, names, null, REACTION_SECONDS, undefined, actor); + } + } + const theirs = other.window.filter((tick) => tick > event.tick - TAKEOVER_WINDOW_SECONDS * this.tickRate).length; + if (notes.window.length >= 7 && theirs <= 2 && notes.takeoverRound !== snapshot.round_number) { + notes.takeoverRound = snapshot.round_number; + if (this.say("takeover", event.event_id, now, names, null, REACTION_SECONDS, undefined, actor)) this.hooks.cue?.("chant"); + } + } + + private roundEnded(event: CombatEvent, snapshot: EngineSnapshot, result: CombatEvent | null, now: number): void { + const round = snapshot.round_number; + if (result !== null) { + if (result.detail === "decision" || result.detail === "draw") this.say("finalBell", event.event_id, now, {}, "bell", REACTION_SECONDS, BELL_FRESH_SECONDS); + return; + } + const saved = snapshot.fighters.find((fighter) => { + const notes = this.notesFor(fighter.player_id); + return fighter.stunned_ticks > 0 || fighter.poise < HURT_POISE || event.tick - notes.knockedDownAt <= 12 * this.tickRate; + }); + if (saved !== undefined) this.say("savedByBell", event.event_id, now, { b: this.nameOf(saved.player_id) }, "bell", REACTION_SECONDS, BELL_FRESH_SECONDS); + else this.say("roundEnd", event.event_id, now, { r: round }, "bell", REACTION_SECONDS, BELL_FRESH_SECONDS); + const [one, two] = snapshot.fighters.map((fighter) => ({ id: fighter.player_id, tally: this.tally(fighter.player_id) })) as [{ id: string; tally: RoundTally }, { id: string; tally: RoundTally }]; + const [lead, trail] = one.tally.landed >= two.tally.landed ? [one, two] : [two, one]; + if (lead.tally.thrown + trail.tally.thrown > 0) this.sayAt("summary", event.event_id, now + 2.4, { r: round, a: this.nameOf(lead.id), x: lead.tally.landed, y: lead.tally.thrown, b: this.nameOf(trail.id), z: trail.tally.landed, w: trail.tally.thrown }, 5); + const edge = (one.tally.knockdowns - two.tally.knockdowns) * 1000 + (one.tally.damage - two.tally.damage) + 6 * (one.tally.landed - two.tally.landed); + const close = Math.abs(edge) < Math.max(20, 0.12 * (one.tally.damage + two.tally.damage)); + const winner = close ? null : edge > 0 ? one : two; + if (winner !== null) this.notesFor(winner.id).rounds += 1; + const loser = winner === null ? null : winner === one ? two : one; + const margin = winner === null || loser === null ? 0 : this.notesFor(winner.id).rounds - this.notesFor(loser.id).rounds; + const opinion: LineKey = winner === null ? "roundClose" : one.tally.knockdowns !== two.tally.knockdowns ? "roundBig" : round >= 2 && margin >= 2 ? "pullingAway" : "roundFor"; + this.sayAt(opinion, event.event_id, now + 6.4, { a: this.nameOf(winner?.id ?? null), b: this.nameOf(loser?.id ?? null) }, 5); + const cut = snapshot.fighters.find((fighter) => Math.max(fighter.trauma.left_cut, fighter.trauma.right_cut) >= DOCTOR_LOOK.cut || fighter.trauma.swelling >= DOCTOR_LOOK.swelling); + if (cut !== undefined) this.sayAt("doctor", event.event_id, now + 10, { b: this.nameOf(cut.player_id) }, 4); + } + + private resulted(event: CombatEvent, now: number): void { + const key = RESULT_LINES[event.detail]; + if (key === undefined || this.resultCalled) return; + this.resultCalled = true; + const winner = event.actor_id; + this.say(key, event.event_id, now, { a: this.nameOf(winner), b: this.nameOf(this.otherId(winner)) }, "result", 0); + } + + /** Cuts, swelling and fatigue show in the fighters' state rather than in events. */ + private inspect(snapshot: EngineSnapshot, now: number): void { + if (snapshot.phase !== "fight") return; + for (const fighter of snapshot.fighters) { + const notes = this.notesFor(fighter.player_id); + const name = { a: this.nameOf(this.otherId(fighter.player_id)), b: this.nameOf(fighter.player_id) }; + for (const [eye, side] of [[0, "left"], [1, "right"]] as const) { + const cut = eye === 0 ? fighter.trauma.left_cut : fighter.trauma.right_cut; + const mark = notes.cutMarks[eye]; + if (mark < CUT_MARKS.length && cut >= CUT_MARKS[mark]!) { + notes.cutMarks[eye] = CUT_MARKS.filter((threshold) => cut >= threshold).length; + this.say(mark === 0 ? "cut" : mark === 1 ? "cutWorse" : "cutDoctor", snapshot.tick + eye, now, { ...name, side }, null); + } + const swelling = eye === 0 ? fighter.trauma.left_eye : fighter.trauma.right_eye; + if (!notes.eyeSwelling[eye] && !notes.eyeShut[eye] && swelling >= EYE_SWELLING) { + notes.eyeSwelling[eye] = true; + this.say("eyeSwelling", snapshot.tick + eye, now, { ...name, side }, null); + } + } + const ratio = fighter.stamina / Math.max(1, fighter.maximum_stamina); + if (ratio < TIRED_RATIO) { + notes.lowSince ??= snapshot.tick; + if (snapshot.tick - notes.lowSince >= 2 * this.tickRate) this.tired(fighter.player_id, snapshot.tick, now); + } else if (ratio >= RECOVERED_RATIO) { + notes.lowSince = null; + } + } + } + + private tired(playerId: string, seed: number, now: number): void { + const notes = this.notesFor(playerId); + if (notes.tiredRound === this.round) return; + notes.tiredRound = this.round; + this.say("tiring", seed, now, { a: this.nameOf(this.otherId(playerId)), b: this.nameOf(playerId) }, null); + } + + /** Queues a line unless it is cooling down; true when queued. */ + private say(key: LineKey, seed: number, now: number, vars: Vars, moment: string | null, delay = REACTION_SECONDS, expiresAfter?: number, subject?: string | null): boolean { + const cooldown = COOLDOWN[key]; + const coolKey = `${key}:${subject ?? ""}`; + if (cooldown !== undefined) { + if (now < (this.cooldowns.get(coolKey) ?? -Infinity)) return false; + this.cooldowns.set(coolKey, now + cooldown); + } + const text = fillLine(this.choose(key, seed), vars); + const line: BroadcastLine = { speaker: SPEAKER[key] ?? "play", text, priority: PRIORITY[key], urgent: URGENT.has(key), hold: holdFor(text), card: null }; + return this.enqueue(line, now + delay, moment, expiresAfter, PHASES[key] ?? null); + } + + private sayAt(key: LineKey, seed: number, at: number, vars: Vars, expiresAfter: number): void { + const text = fillLine(this.choose(key, seed), vars); + this.enqueue({ speaker: SPEAKER[key] ?? "play", text, priority: PRIORITY[key], urgent: URGENT.has(key), hold: holdFor(text), card: null }, at, null, expiresAfter); + } + + private announcement(text: string, card: AnnouncerCard, hold: number, priority: number): BroadcastLine { + return { speaker: "announcer", text, priority, urgent: true, hold, card }; + } + + /** A different phrasing each time, chosen from the event so every viewer hears the same call. */ + private choose(key: LineKey, seed: number): string { + const options: readonly string[] = LINES[key]; + let index = mix(seed, key.length * 31 + key.charCodeAt(0)) % options.length; + if (options.length > 1 && this.lastChoice.get(key) === index) index = (index + 1) % options.length; + this.lastChoice.set(key, index); + return options[index]!; + } + + private enqueue(line: BroadcastLine, at: number, moment: string | null, expiresAfter?: number, phases: ReadonlySet | null = null, spoken: string | null = null): boolean { + if (moment !== null) { + if (this.queue.some((entry) => entry.moment === moment && entry.line.priority >= line.priority)) return false; + if (this.showing?.moment === moment && this.showing.line.priority >= line.priority) return false; + for (let index = this.queue.length - 1; index >= 0; index -= 1) if (this.queue[index]!.moment === moment) this.queue.splice(index, 1); + } + this.queue.push({ line, at, expires: at + (expiresAfter ?? (line.urgent ? URGENT_STALE_SECONDS : STALE_SECONDS)), moment, phases, spoken }); + if (this.queue.length > MAX_QUEUE) { + let lowest = 0; + for (let index = 1; index < this.queue.length; index += 1) if (this.queue[index]!.line.priority < this.queue[lowest]!.line.priority) lowest = index; + this.queue.splice(lowest, 1); + } + return true; + } + + private tally(playerId: string): RoundTally { + let tally = this.tallies.get(playerId); + if (tally === undefined) { + tally = blankTally(); + this.tallies.set(playerId, tally); + } + return tally; + } + + private notesFor(playerId: string): FighterNotes { + let notes = this.notes.get(playerId); + if (notes === undefined) { + notes = blankNotes(); + this.notes.set(playerId, notes); + } + return notes; + } + + private fighter(playerId: string | null): FighterSnapshot | undefined { + return playerId === null ? undefined : this.fighters?.find((fighter) => fighter.player_id === playerId); + } + + private otherId(playerId: string | null): string | null { + const fighters = this.fighters; + if (fighters === null || playerId === null) return null; + return fighters[0].player_id === playerId ? fighters[1].player_id : fighters[1].player_id === playerId ? fighters[0].player_id : null; + } + + private seatOf(playerId: string): 0 | 1 | null { + const seat = this.fighters?.findIndex((fighter) => fighter.player_id === playerId) ?? -1; + return seat === 0 || seat === 1 ? seat : null; + } + + private nameOf(playerId: string | null): string { + if (playerId === null) return "the fighter"; + const name = this.players[playerId]?.name; + if (name !== undefined && name.trim().length > 0) return name.trim(); + const seat = this.seatOf(playerId); + return seat === 0 ? "the blue corner" : seat === 1 ? "the red corner" : "the fighter"; + } +} + +/** Long enough to read: a little over a second plus a beat per word. */ +export function holdFor(text: string): number { + return Math.min(5.2, Math.max(2.2, 1.4 + text.length * 0.05)); +} diff --git a/web/hands/src/render/crowd.test.ts b/web/hands/src/render/crowd.test.ts new file mode 100644 index 00000000..887dd7db --- /dev/null +++ b/web/hands/src/render/crowd.test.ts @@ -0,0 +1,394 @@ +import * as THREE from "three"; +import { describe, expect, it } from "vitest"; +import { buildArena } from "./arena"; +import { ceremonyShot } from "./camera"; +import { CAMERA_PLATFORM_HALF_ANGLE, CAMERA_PLATFORM_REACH, CROWD_TIERS, PARAPET_HEIGHT, PARAPET_SETBACK, buildArmGeometry, buildCrowd, buildHairGeometry, buildHeadGeometry, buildStandsGeometry, buildTorsoGeometry, cameraOnPlatform, seatSpectators, spectatorPose } from "./crowd"; + +const seeded = (seed: number): (() => number) => () => { + seed = Math.imul(seed ^ (seed >>> 15), 1 | seed); + seed ^= seed + Math.imul(seed ^ (seed >>> 7), 61 | seed); + return ((seed ^ (seed >>> 14)) >>> 0) / 4294967296; +}; + +const seats = CROWD_TIERS.reduce((sum, tier) => sum + tier.count, 0); +const total = seatSpectators(CROWD_TIERS, seeded(7)).length; + +describe("crowd seating", () => { + it("fills every seat in the stands, the same way every time", () => { + const first = seatSpectators(CROWD_TIERS, seeded(7)); + const second = seatSpectators(CROWD_TIERS, seeded(7)); + expect(first.length).toBe(seats); + expect(second).toEqual(first); + }); + + it("seats everyone in a row facing the ring", () => { + for (const spectator of seatSpectators(CROWD_TIERS, seeded(7))) { + const reach = Math.hypot(spectator.x, spectator.z); + const tier = CROWD_TIERS.find((candidate) => Math.abs(candidate.radius - reach) <= 0.25 + 1e-9)!; + expect(tier).toBeDefined(); + expect(Math.abs(spectator.y - tier.y)).toBeLessThanOrEqual(0.03 + 1e-9); + const toRing = Math.atan2(-spectator.x, -spectator.z); + const off = Math.atan2(Math.sin(spectator.yaw - toRing), Math.cos(spectator.yaw - toRing)); + expect(Math.abs(off)).toBeLessThanOrEqual(0.25 + 1e-9); + } + }); + + it("puts the rows in front of the broadcast camera on its platform, and the back row behind it off it", () => { + const seated = seatSpectators(CROWD_TIERS, seeded(7)); + const wedge = seated.filter((spectator) => spectator.z > 0 && Math.abs(Math.atan2(spectator.x, spectator.z)) < CAMERA_PLATFORM_HALF_ANGLE); + const onPlatform = seated.filter((spectator) => spectator.platform); + expect(onPlatform.length).toBeGreaterThan(30); + expect(onPlatform).toEqual(wedge.filter((spectator) => Math.hypot(spectator.x, spectator.z) < CAMERA_PLATFORM_REACH)); + expect(wedge.some((spectator) => !spectator.platform)).toBe(true); + const opposite = seated.filter((spectator) => spectator.z < 0 && Math.abs(Math.atan2(spectator.x, -spectator.z)) < CAMERA_PLATFORM_HALF_ANGLE); + expect(opposite.some((spectator) => Math.hypot(spectator.x, spectator.z) < 9)).toBe(true); + expect(opposite.every((spectator) => !spectator.platform)).toBe(true); + }); + + it("knows the broadcast camera from the shots taken inside the ring", () => { + // The broadcast camera works from 4.6 to 8.4 m out, pulled back to 14 m on a tall screen, swaying a little to the side. + for (const [x, z] of [[0, 6.5], [0.6, 8.4], [-1.3, 14.2]] as const) expect(cameraOnPlatform(x, z)).toBe(true); + // Close-ups, replays and the player's own camera stand inside the ring, or just behind a fighter on the ropes. + for (const [x, z] of [[0, 2.2], [1.4, -2.2], [0.3, 5.3], [0, -7], [6, 6]] as const) expect(cameraOnPlatform(x, z)).toBe(false); + }); +}); + +describe("the broadcast camera's platform", () => { + const meshesOf = (crowd: ReturnType): THREE.InstancedMesh[] => crowd.group.children.filter((child): child is THREE.InstancedMesh => child instanceof THREE.InstancedMesh); + const scaleOf = (mesh: THREE.InstancedMesh, index: number): number => { + const matrix = new THREE.Matrix4(); + mesh.getMatrixAt(index, matrix); + return new THREE.Vector3().setFromMatrixScale(matrix).length(); + }; + const broadcast = { x: 0.3, z: 12.5 }; + const inside = { x: 0.4, z: -1.8 }; + + it("clears the seats in front of the broadcast camera and fills them again for every other shot", () => { + const crowd = buildCrowd(seeded(3)); + const meshes = meshesOf(crowd); + const platform = crowd.spectators.flatMap((spectator, index) => (spectator.platform ? [index] : [])); + const others = crowd.spectators.flatMap((spectator, index) => (spectator.platform ? [] : [index])); + expect(platform.length).toBeGreaterThan(30); + // Seated and drawn to begin with, as every shot from inside the ring looks into that part of the stands. + for (const mesh of meshes) for (const index of platform) expect(scaleOf(mesh, index)).toBeGreaterThan(0.5); + const versions = meshes.map((mesh) => mesh.instanceMatrix.version); + crowd.makeRoomForCamera(broadcast); + for (const mesh of meshes) { + for (const index of platform) expect(scaleOf(mesh, index)).toBe(0); + for (const index of others) expect(scaleOf(mesh, index)).toBeGreaterThan(0.5); + expect(mesh.instanceMatrix.updateRanges).toEqual([]); + } + expect(meshes.map((mesh) => mesh.instanceMatrix.version)).toEqual(versions.map((version) => version + 1)); + // The crowd moving on keeps them out of the camera's way, at the low tier too. + crowd.update(2, 1); + crowd.update(2.02, 1); + crowd.setLowTier(true); + crowd.update(2.04, 1); + for (const index of platform) expect(scaleOf(meshes[0]!, index)).toBe(0); + crowd.makeRoomForCamera(inside); + for (const mesh of meshes) for (const index of platform) expect(scaleOf(mesh, index)).toBeGreaterThan(0.5); + crowd.dispose(); + }); + + it("clears them for every shot from among or behind them: the decision and the arm raised after a stoppage too", () => { + // Those shots stand in front of the result card's free space, centred on the fighters (clamped to 1.4 m + // across), at the distance that frames them, drifting by up to 6% of it; on a wide screen and a phone. + const firstSeats = CROWD_TIERS[0]!.radius - 0.25; + let among = 0; + for (const [aspect, fov] of [[16 / 9, 36], [390 / 844, 36 * Math.min(1.3, Math.sqrt(2.2))]] as const) { + for (const covered of [0.15, 0.3, 0.45, 0.6]) { + const shot = ceremonyShot(aspect, fov, covered); + for (const centreX of [-2.3, 0, 2.3]) { + for (const centreZ of [-2.3, 0, 1.2, 2.3]) { + for (const drift of [-0.06, 0, 0.06]) { + const x = THREE.MathUtils.clamp(centreX, -1.4, 1.4) + drift * shot.distance; + const z = centreZ + shot.distance; + // Any shot standing among the seats on the platform, or further out, has them cleared. + if (Math.hypot(x, z) < firstSeats) continue; + among += 1; + expect(cameraOnPlatform(x, z), `${x.toFixed(2)}, ${z.toFixed(2)}`).toBe(true); + } + } + } + } + } + expect(among).toBeGreaterThan(10); + }); + + it("keeps them clear while a shot hovers at the edge of the platform, rather than flickering", () => { + const crowd = buildCrowd(seeded(3)); + const [torsos] = meshesOf(crowd); + const platform = crowd.spectators.findIndex((spectator) => spectator.platform); + crowd.makeRoomForCamera({ x: 0, z: 6.3 }); + expect(scaleOf(torsos!, platform)).toBe(0); + const version = torsos!.instanceMatrix.version; + for (const z of [6.15, 6.25, 6.05, 6.2]) crowd.makeRoomForCamera({ x: 0, z }); + expect(torsos!.instanceMatrix.version).toBe(version); + expect(scaleOf(torsos!, platform)).toBe(0); + crowd.makeRoomForCamera({ x: 0, z: 5.5 }); + expect(scaleOf(torsos!, platform)).toBeGreaterThan(0.5); + crowd.dispose(); + }); + + it("puts them back seated when reduced motion has stopped the crowd", () => { + const crowd = buildCrowd(seeded(3)); + const [torsos, , , armsLeft] = meshesOf(crowd); + const platform = crowd.spectators.findIndex((spectator) => spectator.platform); + crowd.update(3, 1); + crowd.update(3, 1); + crowd.makeRoomForCamera(broadcast); + // Reduced motion seats everyone once and then stops updating the crowd. + crowd.update(0, 0, true); + expect(scaleOf(torsos!, platform)).toBe(0); + crowd.makeRoomForCamera(inside); + const seat = new THREE.Matrix4(); + torsos!.getMatrixAt(platform, seat); + const spectator = crowd.spectators[platform]!; + expect(new THREE.Vector3().setFromMatrixPosition(seat).y).toBeCloseTo(spectator.y + spectatorPose(spectator, 0, 0).rise * spectator.scale, 6); + const arm = new THREE.Matrix4(); + armsLeft!.getMatrixAt(platform, arm); + const turn = new THREE.Quaternion(); + arm.decompose(new THREE.Vector3(), turn, new THREE.Vector3()); + // The hand hangs below the shoulder, not up in the ovation it was cleared in. + expect(new THREE.Vector3(0, -0.53, 0).applyQuaternion(turn).y).toBeLessThan(0); + crowd.dispose(); + }); +}); + +describe("crowd reaction", () => { + const seated = seatSpectators(CROWD_TIERS, seeded(11)); + const standing = (excitement: number): number => seated.filter((spectator) => spectatorPose(spectator, 3.7, excitement).rise > 0.2).length; + + it("sits with its arms down while the bout is quiet", () => { + for (const spectator of seated) { + const pose = spectatorPose(spectator, 3.7, 0); + expect(pose.rise).toBeLessThan(0.03); + expect(pose.armLeft).toBeCloseTo(-0.35, 6); + expect(pose.armRight).toBeCloseTo(-0.35, 6); + } + }); + + it("gets to its feet with its arms up at the finish", () => { + for (const spectator of seated) { + const pose = spectatorPose(spectator, 3.7, 1); + expect(pose.rise).toBeGreaterThan(0.3); + expect(pose.armLeft).toBeLessThan(-2.2); + expect(pose.armRight).toBeLessThan(-2.2); + } + }); + + it("rises a few at a time as the excitement builds", () => { + expect(standing(0)).toBe(0); + const some = standing(0.45); + const more = standing(0.7); + expect(some).toBeGreaterThan(total * 0.15); + expect(some).toBeLessThan(total * 0.6); + expect(more).toBeGreaterThan(some); + expect(standing(1)).toBe(total); + }); +}); + +describe("crowd geometry", () => { + it("puts a parapet in front of every row, below shoulder height", () => { + const geometry = buildStandsGeometry(CROWD_TIERS); + const position = geometry.getAttribute("position"); + for (const tier of CROWD_TIERS) { + let top = -Infinity; + for (let index = 0; index < position.count; index += 1) { + const reach = Math.hypot(position.getX(index), position.getZ(index)); + if (Math.abs(reach - (tier.radius - PARAPET_SETBACK)) < 0.01) top = Math.max(top, position.getY(index)); + } + expect(top).toBeCloseTo(tier.y + PARAPET_HEIGHT, 5); + } + geometry.dispose(); + }); + + it("hides the lap behind the parapet and shows the chest and head above it", () => { + const torso = buildTorsoGeometry(); + torso.computeBoundingBox(); + const position = torso.getAttribute("position"); + let lap = -Infinity; + for (let index = 0; index < position.count; index += 1) if (position.getZ(index) > 0.25) lap = Math.max(lap, position.getY(index)); + for (const tier of CROWD_TIERS) { + expect(lap * tier.scale * 0.92).toBeLessThan(PARAPET_HEIGHT + 0.12); + expect(torso.boundingBox!.max.y * tier.scale * 0.92).toBeGreaterThan(PARAPET_HEIGHT + 0.35); + } + torso.dispose(); + }); + + it("draws a spectator in a few hundred triangles", () => { + const parts = [buildTorsoGeometry(), buildHeadGeometry(), buildHairGeometry(), buildArmGeometry(), buildArmGeometry()]; + let triangles = 0; + let vertices = 0; + for (const part of parts) { + expect(part.getIndex()).not.toBeNull(); + triangles += part.getIndex()!.count / 3; + vertices += part.getAttribute("position").count; + part.dispose(); + } + expect(triangles).toBeLessThan(500); + expect(vertices).toBeLessThan(450); + }); + + it("hangs the arm from the shoulder and sits the torso on the seat", () => { + const arm = buildArmGeometry(); + arm.computeBoundingBox(); + expect(arm.boundingBox!.max.y).toBeLessThan(0.06); + expect(arm.boundingBox!.min.y).toBeLessThan(-0.5); + const torso = buildTorsoGeometry(); + torso.computeBoundingBox(); + expect(torso.boundingBox!.min.y).toBeGreaterThan(0.2); + expect(torso.boundingBox!.max.y).toBeLessThan(1.05); + arm.dispose(); + torso.dispose(); + }); + + it("draws every spectator and moves their arms when they cheer", () => { + const crowd = buildCrowd(seeded(3)); + const drawn = crowd.group.children.filter((child): child is THREE.InstancedMesh => child instanceof THREE.InstancedMesh); + expect(drawn).toHaveLength(5); + for (const mesh of drawn) expect(mesh.count).toBe(total); + expect(crowd.group.getObjectByName("stands")).toBeInstanceOf(THREE.Mesh); + const arm = drawn[3]!; + const after = new THREE.Matrix4(); + // The first and the last spectator are in different halves of the crowd. + const watched = [0, total - 1]; + const seated = [new THREE.Matrix4(), new THREE.Matrix4()]; + for (const [at, matrix] of seated.entries()) arm.getMatrixAt(watched[at]!, matrix); + const moved = (): boolean[] => seated.map((matrix, at) => { + arm.getMatrixAt(watched[at]!, after); + return !after.equals(matrix); + }); + crowd.update(2, 1); + expect(moved().filter(Boolean)).toHaveLength(1); + crowd.update(2, 1); + expect(moved()).toEqual([true, true]); + crowd.dispose(); + }); +}); + +describe("crowd cost", () => { + const parts = (crowd: ReturnType): THREE.InstancedMesh[] => crowd.group.children.filter((child): child is THREE.InstancedMesh => child instanceof THREE.InstancedMesh); + + it("uploads only the half of the crowd that moved", () => { + const crowd = buildCrowd(seeded(3)); + const [torsos, , , armsLeft] = parts(crowd); + const half = Math.ceil(total / 2); + const ranges = [0, 1].map((frame) => { + crowd.update(1 + frame / 50, 0.5); + expect(armsLeft!.instanceMatrix.updateRanges).toEqual(torsos!.instanceMatrix.updateRanges); + return torsos!.instanceMatrix.updateRanges.map(({ start, count }) => ({ start, count })); + }); + expect(ranges.flat().sort((a, b) => a.start - b.start)).toEqual([{ start: 0, count: half * 16 }, { start: half * 16, count: (total - half) * 16 }]); + crowd.update(1.04, 0.5, true); + expect(torsos!.instanceMatrix.updateRanges).toEqual([]); + crowd.dispose(); + }); + + it("at the low tier moves a quarter at a time and puts its arms out of sight", () => { + const crowd = buildCrowd(seeded(3)); + const [torsos, , , armsLeft, armsRight] = parts(crowd); + crowd.setLowTier(true); + expect(armsLeft!.visible).toBe(false); + expect(armsRight!.visible).toBe(false); + const armsVersion = armsLeft!.instanceMatrix.version; + crowd.update(1, 0.5); + const [range] = torsos!.instanceMatrix.updateRanges; + expect(torsos!.instanceMatrix.updateRanges).toHaveLength(1); + expect(range!.count).toBeLessThanOrEqual(Math.ceil(total / 4) * 16); + expect(range!.start % (Math.ceil(total / 4) * 16)).toBe(0); + expect(armsLeft!.instanceMatrix.version).toBe(armsVersion); + crowd.setLowTier(false); + expect(armsLeft!.visible).toBe(true); + crowd.update(1.02, 0.5); + expect(armsLeft!.instanceMatrix.version).toBeGreaterThan(armsVersion); + expect(armsLeft!.instanceMatrix.updateRanges).toEqual([]); + crowd.dispose(); + }); +}); + +describe("crowd and reduced motion", () => { + const armsUp = (arena: ReturnType): number => { + const crowd = arena.group.getObjectByName("crowd")!; + const arms = crowd.children.filter((child): child is THREE.InstancedMesh => child instanceof THREE.InstancedMesh)[3]!; + const matrix = new THREE.Matrix4(); + const position = new THREE.Vector3(); + const turn = new THREE.Quaternion(); + const size = new THREE.Vector3(); + let raised = 0; + for (let index = 0; index < arms.count; index += 1) { + arms.getMatrixAt(index, matrix); + matrix.decompose(position, turn, size); + if (new THREE.Vector3(0, -0.53, 0).applyQuaternion(turn).y > 0.2) raised += 1; + } + return raised; + }; + + it("sits the crowd down when reduced motion is switched on during an ovation", () => { + const arena = buildArena(); + arena.excite(1); + arena.update(1, 0, false); + arena.update(1, 0, false); + expect(armsUp(arena)).toBe(total); + arena.update(1.02, 1 / 60, true); + expect(armsUp(arena)).toBe(0); + arena.dispose(); + }); + + it("seats the arms with the bodies when the low tier had frozen them, so they come back hanging from the shoulders", () => { + const arena = buildArena(); + arena.excite(1); + arena.update(1, 0, false); + arena.update(1, 0, false); + expect(armsUp(arena)).toBe(total); + // The scaler drops to the low tier, reduced motion seats the crowd, then quality recovers with motion still reduced. + arena.setLowTier(true); + arena.update(1.02, 1 / 60, true); + arena.setLowTier(false); + arena.update(1.04, 1 / 60, true); + expect(armsUp(arena)).toBe(0); + const crowd = arena.group.getObjectByName("crowd")!; + const [torsos, , , armsLeft, armsRight] = crowd.children.filter((child): child is THREE.InstancedMesh => child instanceof THREE.InstancedMesh); + const body = new THREE.Matrix4(); + const arm = new THREE.Matrix4(); + const shoulder = new THREE.Vector3(); + const root = new THREE.Vector3(); + let worst = 0; + for (let index = 0; index < torsos!.count; index += 1) { + torsos!.getMatrixAt(index, body); + for (const [mesh, side] of [[armsLeft!, 1], [armsRight!, -1]] as const) { + mesh.getMatrixAt(index, arm); + root.setFromMatrixPosition(arm); + worst = Math.max(worst, root.distanceTo(shoulder.set(side * 0.235, 0.9, 0).applyMatrix4(body))); + } + } + expect(worst).toBeLessThan(0.001); + arena.dispose(); + }); + + it("reaches everyone within two frames of motion coming back", () => { + const arena = buildArena(); + arena.update(0, 1 / 60, true); + arena.excite(1); + arena.update(1, 0, false); + arena.update(1, 0, false); + expect(armsUp(arena)).toBe(total); + arena.dispose(); + }); +}); + +describe("the advertising boards", () => { + it("read the right way round from inside the ring", () => { + const arena = buildArena(); + const boards: THREE.Texture[] = []; + arena.group.traverse((object) => { + if (!(object instanceof THREE.Mesh)) return; + const material = object.material as THREE.MeshBasicMaterial; + if (material.side === THREE.BackSide && material.map !== null && material.map.wrapS === THREE.RepeatWrapping) boards.push(material.map); + }); + expect(boards.length).toBeGreaterThan(0); + // The boards are the inside of a cylinder, so the picture is mirrored back by a negative repeat. + for (const map of boards) expect(map.repeat.x).toBeLessThan(0); + arena.dispose(); + }); +}); diff --git a/web/hands/src/render/crowd.ts b/web/hands/src/render/crowd.ts new file mode 100644 index 00000000..ad53204d --- /dev/null +++ b/web/hands/src/render/crowd.ts @@ -0,0 +1,392 @@ +import * as THREE from "three"; +import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js"; +import { ARENA_FLOOR } from "./world"; + +/** + * The arena crowd: seated spectators in stepped stands behind a parapet. Each is a torso with + * shoulders, a head with hair and a face, and two arms. They sway in their seats, and as the bout + * excites them the keener ones get to their feet and throw their arms up first. + */ + +export interface CrowdTier { + readonly radius: number; + /** Height of the tier's floor. */ + readonly y: number; + readonly count: number; + readonly scale: number; +} + +export const CROWD_TIERS: readonly CrowdTier[] = [ + { radius: 8.2, y: -0.55, count: 120, scale: 1 }, + { radius: 10.6, y: 0.35, count: 150, scale: 1.04 }, + { radius: 13.2, y: 1.35, count: 180, scale: 1.08 }, + { radius: 16.0, y: 2.45, count: 210, scale: 1.12 }, +]; + +/** How far in front of a row its parapet stands, and how high it rises above the row's floor. */ +export const PARAPET_SETBACK = 0.95; +export const PARAPET_HEIGHT = 0.42; +const SHIRT_COLORS = [0x3a4660, 0x5a4538, 0x35505c, 0x6a3f3a, 0x46603f, 0x544d63, 0x6e5f44, 0x8a8074, 0x47627a, 0x74486a, 0x9a3a3a, 0xb4b8c2, 0x2f5fb0, 0xb09a3e, 0x23262e, 0x23262e]; +const SKIN_COLORS = [0xc79b76, 0x8a5a3b, 0x6e4128, 0xe0b48f, 0x54301d, 0xa9744f]; +const HAIR_COLORS = [0x15100c, 0x2a1b12, 0x4a3220, 0x6b5a48, 0x8e8a84, 0xb08a4e]; + +const SHOULDER_HEIGHT = 0.9; +const SHOULDER_HALF_WIDTH = 0.235; +const ARM_REST = -0.35; +const ARM_RAISED = -2.7; +const STANDING_RISE = 0.32; +const HOUSE_LIGHT = 0.26; +/** The broadcast camera pulls back as far as 14 m on a tall screen, over the first three rows. */ +export const CAMERA_PLATFORM_REACH = 14.5; +export const CAMERA_PLATFORM_HALF_ANGLE = 0.3; +/** + * A camera this far out on the platform's side is up in the stands, in front of the first row's seats, and + * once there it has to come this much nearer to leave, so a shot hovering at the edge does not flicker them. + */ +export const CAMERA_PLATFORM_START = 6.2; +const CAMERA_PLATFORM_LEAVE = 0.3; + +/** + * Whether a camera at (x, z) stands on the broadcast platform, where the seats on it would be in its way: + * whatever the shot (the broadcast, the decision, the arm raised after a stoppage), by where it stands. + */ +export function cameraOnPlatform(x: number, z: number, from = CAMERA_PLATFORM_START): boolean { + return z > 0 && Math.hypot(x, z) > from && Math.abs(Math.atan2(x, z)) < CAMERA_PLATFORM_HALF_ANGLE; +} +const RIG_HEIGHT = 7.5; + +const SPILL_VERTEX = /* glsl */ ` +#ifdef USE_INSTANCING + vec4 crowdWorld = modelMatrix * instanceMatrix * vec4(transformed, 1.0); +#else + vec4 crowdWorld = modelMatrix * vec4(transformed, 1.0); +#endif +vCrowdLight = vec3(-crowdWorld.x, ${RIG_HEIGHT.toFixed(1)} - crowdWorld.y, -crowdWorld.z); +`; + +const SPILL_FRAGMENT = /* glsl */ ` +#ifdef USE_COLOR + totalEmissiveRadiance *= vColor.rgb * (0.22 + 0.78 * max(0.0, dot(inverseTransformDirection(normal, viewMatrix), normalize(vCrowdLight)))); +#endif +`; + +export interface Spectator { + readonly x: number; + readonly y: number; + readonly z: number; + readonly yaw: number; + readonly scale: number; + readonly phase: number; + /** 0..1: how little it takes to get this one out of the seat. */ + readonly keen: number; + /** How much of the ring's light reaches this row. */ + readonly light: number; + /** Sits on the broadcast camera's platform, and makes way while the camera stands there. */ + readonly platform: boolean; +} + +export interface SpectatorPose { + /** Metres above the seat. */ + rise: number; + /** Rotation of each arm about the shoulder, from hanging forward (rest) to overhead. */ + armLeft: number; + armRight: number; + sway: number; +} + +const smooth = (from: number, to: number, value: number): number => { + const t = Math.min(1, Math.max(0, (value - from) / (to - from))); + return t * t * (3 - 2 * t); +}; + +export function seatSpectators(tiers: readonly CrowdTier[], rand: () => number): Spectator[] { + const seated: Spectator[] = []; + for (const [row, tier] of tiers.entries()) { + for (let seat = 0; seat < tier.count; seat += 1) { + const angle = (seat / tier.count) * Math.PI * 2 + rand() * 0.03; + const reach = tier.radius + (rand() - 0.5) * 0.5; + const x = Math.sin(angle) * reach; + const z = Math.cos(angle) * reach; + const spectator = { keen: rand(), phase: rand() * Math.PI * 2, scale: tier.scale * (0.92 + rand() * 0.2), lean: (rand() - 0.5) * 0.5, lift: (rand() - 0.5) * 0.06 }; + seated.push({ + x, + y: tier.y + spectator.lift, + z, + yaw: Math.atan2(-x, -z) + spectator.lean, + scale: spectator.scale, + phase: spectator.phase, + keen: spectator.keen, + light: Math.max(0.45, 1 - row * 0.17), + // The broadcast camera works from a platform in the stands: whoever sits in front of it makes way for it. + platform: tier.radius < CAMERA_PLATFORM_REACH && z > 0 && Math.abs(Math.atan2(x, z)) < CAMERA_PLATFORM_HALF_ANGLE, + }); + } + } + return seated; +} + +export function spectatorPose(spectator: Spectator, time: number, excitement: number, out: SpectatorPose = { rise: 0, armLeft: 0, armRight: 0, sway: 0 }): SpectatorPose { + const threshold = 0.2 + 0.7 * (1 - spectator.keen); + const up = smooth(threshold - 0.12, threshold + 0.12, excitement); + const bounce = Math.abs(Math.sin(time * (2.3 + excitement * 3.2) + spectator.phase * 1.7)) * (0.02 + 0.1 * up); + const wave = Math.sin(time * 6.2 + spectator.phase) * 0.32 * up; + const arm = ARM_REST + (ARM_RAISED - ARM_REST) * up; + out.rise = up * STANDING_RISE + bounce; + out.armLeft = arm + wave; + out.armRight = arm - wave * 0.8; + out.sway = Math.sin(time * 1.6 + spectator.phase) * (0.04 + excitement * 0.05); + return out; +} + +/** The parts are merged as they are built, with their indices: a spectator is a few hundred triangles, six hundred times over. */ +function merged(parts: THREE.BufferGeometry[]): THREE.BufferGeometry { + const result = mergeGeometries(parts, false); + for (const part of parts) part.dispose(); + if (result === null) throw new Error("crowd geometry could not be merged"); + return result; +} + +export function buildTorsoGeometry(): THREE.BufferGeometry { + const chest = new THREE.CapsuleGeometry(0.16, 0.28, 2, 8); + chest.scale(1.35, 1, 0.8); + chest.translate(0, 0.64, 0); + const shoulders = new THREE.CapsuleGeometry(0.078, 0.3, 1, 5); + shoulders.rotateZ(Math.PI / 2); + shoulders.translate(0, SHOULDER_HEIGHT - 0.01, 0); + const lap = new THREE.CapsuleGeometry(0.11, 0.26, 1, 5); + lap.rotateX(Math.PI / 2); + lap.scale(1.7, 1, 1); + lap.translate(0, 0.4, 0.17); + return merged([chest, shoulders, lap]); +} + +export function buildHeadGeometry(): THREE.BufferGeometry { + const head = new THREE.SphereGeometry(0.105, 10, 6); + head.rotateY(-Math.PI / 2); + head.scale(0.92, 1.12, 1); + head.translate(0, 1.12, 0.01); + const neck = new THREE.CylinderGeometry(0.048, 0.056, 0.1, 5, 1, true); + neck.translate(0, 0.99, 0); + const neckUv = neck.getAttribute("uv"); + for (let index = 0; index < neckUv.count; index += 1) neckUv.setXY(index, 0.02, 0.02); + return merged([head, neck]); +} + +export function buildHairGeometry(): THREE.BufferGeometry { + const hair = new THREE.SphereGeometry(0.112, 8, 3, 0, Math.PI * 2, 0, Math.PI * 0.52); + hair.scale(0.95, 1.1, 1.04); + hair.rotateX(-0.42); + hair.translate(0, 1.128, -0.006); + return hair; +} + +/** One arm hanging from the shoulder at the origin. */ +export function buildArmGeometry(): THREE.BufferGeometry { + const arm = new THREE.CapsuleGeometry(0.046, 0.43, 2, 5); + arm.translate(0, -0.25, 0); + const hand = new THREE.SphereGeometry(0.048, 5, 3); + hand.translate(0, -0.53, 0); + return merged([arm, hand]); +} + +/** A face for a head mapped with the seam at the back: eyes, brows and a mouth on white, tinted per spectator. */ +export function faceTexture(): THREE.CanvasTexture { + const canvas = document.createElement("canvas"); + canvas.width = 64; + canvas.height = 32; + const ctx = canvas.getContext("2d"); + if (ctx !== null) { + ctx.fillStyle = "#ffffff"; + ctx.fillRect(0, 0, 64, 32); + ctx.fillStyle = "rgba(60,30,20,0.28)"; + ctx.fillRect(24, 9, 16, 10); + ctx.fillStyle = "#2a1a14"; + ctx.fillRect(26.5, 13, 3.4, 1.8); + ctx.fillRect(34.1, 13, 3.4, 1.8); + ctx.fillStyle = "rgba(40,20,14,0.7)"; + ctx.fillRect(26, 11, 4.4, 1); + ctx.fillRect(33.6, 11, 4.4, 1); + ctx.fillStyle = "rgba(90,30,28,0.75)"; + ctx.fillRect(29, 20.5, 6, 1.4); + ctx.fillStyle = "rgba(60,30,20,0.35)"; + ctx.fillRect(31.2, 14.5, 1.6, 4); + } + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + return texture; +} + +/** Stepped stands: a floor for every row and a parapet in front of it that hides the legs. */ +export function buildStandsGeometry(tiers: readonly CrowdTier[]): THREE.BufferGeometry { + const profile: THREE.Vector2[] = []; + let floor = ARENA_FLOOR; + for (const tier of tiers) { + const front = tier.radius - PARAPET_SETBACK; + profile.push(new THREE.Vector2(front, floor), new THREE.Vector2(front, tier.y + PARAPET_HEIGHT), new THREE.Vector2(front + 0.12, tier.y + PARAPET_HEIGHT), new THREE.Vector2(front + 0.12, tier.y)); + floor = tier.y; + } + const last = tiers.at(-1)!; + profile.push(new THREE.Vector2(last.radius + 1.4, last.y), new THREE.Vector2(last.radius + 1.4, last.y + 3)); + return new THREE.LatheGeometry(profile, 72); +} + +export interface BuiltCrowd { + readonly group: THREE.Group; + readonly spectators: readonly Spectator[]; + readonly update: (time: number, excitement: number, everyone?: boolean) => void; + /** At the low quality tier the crowd moves a quarter at a time and drops the arms, which are a third of its triangles. */ + readonly setLowTier: (low: boolean) => void; + /** + * Clears the seats on the broadcast camera's platform while the camera stands there (see `cameraOnPlatform`), + * and fills them again for every other shot, which may look into that part of the stands. + */ + readonly makeRoomForCamera: (camera: { readonly x: number; readonly z: number }) => void; + readonly dispose: () => void; +} + +export function buildCrowd(rand: () => number, tiers: readonly CrowdTier[] = CROWD_TIERS): BuiltCrowd { + const group = new THREE.Group(); + group.name = "crowd"; + const spectators = seatSpectators(tiers, rand); + const total = spectators.length; + const face = faceTexture(); + const geometries = [buildTorsoGeometry(), buildHeadGeometry(), buildHairGeometry(), buildArmGeometry(), buildStandsGeometry(tiers)] as const; + const cloth = new THREE.MeshStandardMaterial({ roughness: 0.9, metalness: 0, emissive: 0xffffff, emissiveIntensity: HOUSE_LIGHT }); + const skin = new THREE.MeshStandardMaterial({ map: face, emissiveMap: face, roughness: 0.75, metalness: 0, emissive: 0xffffff, emissiveIntensity: HOUSE_LIGHT }); + const hairMaterial = new THREE.MeshStandardMaterial({ roughness: 0.85, metalness: 0, emissive: 0xffffff, emissiveIntensity: HOUSE_LIGHT }); + for (const material of [cloth, skin, hairMaterial]) { + // The ring's lights fall off before the stands, so the spill from the rig over the ring is + // drawn as a glow in each spectator's own colours, shaded by which way the surface faces. + material.onBeforeCompile = (shader) => { + shader.vertexShader = shader.vertexShader + .replace("#include ", "#include \nvarying vec3 vCrowdLight;") + .replace("#include ", `#include \n${SPILL_VERTEX}`); + shader.fragmentShader = shader.fragmentShader + .replace("#include ", "#include \nvarying vec3 vCrowdLight;") + .replace("#include ", `#include \n${SPILL_FRAGMENT}`); + }; + material.customProgramCacheKey = () => "hands-crowd"; + } + const concrete = new THREE.MeshStandardMaterial({ color: 0x0b0e15, roughness: 0.96, metalness: 0, side: THREE.DoubleSide }); + const torsos = new THREE.InstancedMesh(geometries[0], cloth, total); + const heads = new THREE.InstancedMesh(geometries[1], skin, total); + const hair = new THREE.InstancedMesh(geometries[2], hairMaterial, total); + const armsLeft = new THREE.InstancedMesh(geometries[3], cloth, total); + const armsRight = new THREE.InstancedMesh(geometries[3], cloth, total); + const meshes = [torsos, heads, hair, armsLeft, armsRight]; + const bodies = [torsos, heads, hair]; + for (const mesh of meshes) { + mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage); + mesh.frustumCulled = false; + } + const color = new THREE.Color(); + for (let index = 0; index < total; index += 1) { + const light = spectators[index]!.light; + color.setHex(SHIRT_COLORS[Math.floor(rand() * SHIRT_COLORS.length)]!).multiplyScalar((0.75 + rand() * 0.5) * light); + torsos.setColorAt(index, color); + armsLeft.setColorAt(index, color); + armsRight.setColorAt(index, color); + color.setHex(SKIN_COLORS[Math.floor(rand() * SKIN_COLORS.length)]!).multiplyScalar((0.85 + rand() * 0.35) * light); + heads.setColorAt(index, color); + if (rand() > 0.12) color.setHex(HAIR_COLORS[Math.floor(rand() * HAIR_COLORS.length)]!).multiplyScalar((0.8 + rand() * 0.4) * light); + hair.setColorAt(index, color); + } + for (const mesh of meshes) mesh.instanceColor!.needsUpdate = true; + const stands = new THREE.Mesh(geometries[4], concrete); + stands.name = "stands"; + stands.receiveShadow = false; + group.add(stands, ...meshes); + + const up = new THREE.Vector3(0, 1, 0); + const position = new THREE.Vector3(); + const turn = new THREE.Quaternion(); + const size = new THREE.Vector3(); + const body = new THREE.Matrix4(); + const limb = new THREE.Matrix4(); + const shoulder = new THREE.Matrix4(); + const swing = new THREE.Matrix4(); + const pose: SpectatorPose = { rise: 0, armLeft: 0, armRight: 0, sway: 0 }; + const hang = (mesh: THREE.InstancedMesh, index: number, side: number, angle: number): void => { + shoulder.makeTranslation(side * SHOULDER_HALF_WIDTH, SHOULDER_HEIGHT, 0); + swing.makeRotationX(angle); + limb.multiplyMatrices(body, shoulder).multiply(swing); + mesh.setMatrixAt(index, limb); + }; + let pass = 0; + let lowTier = false; + let refreshAll = false; + let platformClear = false; + let poseTime = 0; + let poseExcitement = 0; + const platformSeats = spectators.flatMap((spectator, index) => (spectator.platform ? [index] : [])); + const hidden = new THREE.Matrix4().makeScale(0, 0, 0); + const place = (index: number, armed: boolean): void => { + const spectator = spectators[index]!; + if (platformClear && spectator.platform) { + for (const mesh of armed ? meshes : bodies) mesh.setMatrixAt(index, hidden); + return; + } + spectatorPose(spectator, poseTime, poseExcitement, pose); + turn.setFromAxisAngle(up, spectator.yaw + pose.sway); + body.compose(position.set(spectator.x, spectator.y + pose.rise * spectator.scale, spectator.z), turn, size.setScalar(spectator.scale)); + torsos.setMatrixAt(index, body); + heads.setMatrixAt(index, body); + hair.setMatrixAt(index, body); + if (armed) { + hang(armsLeft, index, 1, pose.armLeft); + hang(armsRight, index, -1, pose.armRight); + } + }; + // Half the crowd moves on each call (a quarter at the low tier), which cannot be seen, and only the + // block that moved is uploaded. + const update = (time: number, excitement: number, everyone = false): void => { + const all = everyone || refreshAll; + refreshAll = false; + const parts = all ? 1 : lowTier ? 4 : 2; + const block = Math.ceil(total / parts); + const first = (pass % parts) * block; + const last = Math.min(total, first + block); + pass += 1; + poseTime = time; + poseExcitement = excitement; + // A pass over everyone poses the arms too, even while the low tier hides them: reduced motion seats the + // crowd once and then stops updating it, so arms left raised would come back floating over seated bodies. + const armed = !lowTier || all; + for (let index = first; index < last; index += 1) place(index, armed); + for (const mesh of armed ? meshes : bodies) { + const matrices = mesh.instanceMatrix; + matrices.clearUpdateRanges(); + if (!all) matrices.addUpdateRange(first * 16, (last - first) * 16); + matrices.needsUpdate = true; + } + }; + const setLowTier = (low: boolean): void => { + if (low === lowTier) return; + lowTier = low; + armsLeft.visible = !low; + armsRight.visible = !low; + // Arms that were not moved while hidden catch up with everyone else at once. + if (!low) refreshAll = true; + }; + // Only at a cut: the seats on the platform are written at once, in the pose of the last update (reduced + // motion stops updating the crowd), and the whole crowd is uploaded with them. + const makeRoomForCamera = (camera: { readonly x: number; readonly z: number }): void => { + const clear = cameraOnPlatform(camera.x, camera.z, platformClear ? CAMERA_PLATFORM_START - CAMERA_PLATFORM_LEAVE : CAMERA_PLATFORM_START); + if (clear === platformClear) return; + platformClear = clear; + for (const index of platformSeats) place(index, true); + for (const mesh of meshes) { + mesh.instanceMatrix.clearUpdateRanges(); + mesh.instanceMatrix.needsUpdate = true; + } + }; + update(0, 0, true); + + const dispose = (): void => { + for (const geometry of geometries) geometry.dispose(); + for (const material of [cloth, skin, hairMaterial, concrete]) material.dispose(); + face.dispose(); + for (const mesh of meshes) mesh.dispose(); + }; + return { group, spectators, update, setLowTier, makeRoomForCamera, dispose }; +} diff --git a/web/hands/src/render/effects.ts b/web/hands/src/render/effects.ts index 41991e20..16335a7a 100644 --- a/web/hands/src/render/effects.ts +++ b/web/hands/src/render/effects.ts @@ -1,17 +1,51 @@ import * as THREE from "three"; import type { BloodLevel } from "../settings"; import type { CombatEvent, Hand } from "../types"; -import { CANVAS_TOP, RING_FIGHT_HALF } from "./world"; +import { CanvasBlood, type RegionUploader } from "./canvas-blood"; +import { wearCornerColour } from "./gear"; +import { BIG_SHOT, HARD_SHOT, ROCKING_COUNTER, bloodDropsFor, bloodShade, buildChunkGeometry, buildDropletGeometry, buildShardGeometry, buildWoundGeometry, closeCut, dropletShape, eyeTexture, jawWoundTexture, woundTexture, wristWoundTexture } from "./gore"; +import { EYE_LIDS, HEAD_SITES, HEAD_SWELL_CORE, InjuryShading } from "./injury"; +import { LookShading, SCANNED_LOOK, lookShape, type FighterLook } from "./looks"; +import { SHIELD_RADIUS, buildMouthpieceGeometry, idleShield, stepShield, type ShieldState } from "./mouthpiece"; +import { CANVAS_TOP, RING_FIGHT_HALF, ROPE_LINE } from "./world"; const MAX_DROPLETS = 900; const MAX_MIST = 90; -const MAX_DECALS = 48; -const MAX_GIBS = 48; +const MAX_GIBS = 96; +const MAX_SHARDS = 24; +const SHARDS_PER_BURST = 18; +const BRAIN_PER_BURST = 14; +const TEETH_PER_BURST = 5; const MAX_HEADS = 2; const MAX_HANDS = 4; const GIBS_PER_DECAPITATION = 24; const GIBS_PER_HAND = 16; const HEAD_RADIUS = 0.12; +const HEAD_SETTLE_SECONDS = 0.35; +const SEVERED_PART_MARGIN = 0.04; +/** Inward speed of a part beyond the ropes, per metre it is beyond them. */ +const ROPE_RETURN_RATE = 4; +const DROPLET_GRAVITY = 9.81; +/** Of every `STRAND_PERIOD` drops of blood thrown by a blow, one leads a strand and `STRAND_LINKS` trail it. */ +const STRAND_PERIOD = 5; +const STRAND_LINKS = 2; +const STRAND_LAG = 0.09; +const STRAND_THINNING = 0.22; +/** + * How dark a stain soaks into the canvas: a drop leaves a dark spot, the spray off a cut a lighter + * splash, and the canvas goes solid only where blood lands again and again, so it gets bloodier round + * by round instead of the first round painting it solid. A knockdown and a finisher soak in darker. + */ +const DROP_STAIN = 0.7; +const SPLASH_STAIN = 0.45; +const KNOCKDOWN_STAIN = 0.6; +const FINISHER_STAIN = 0.9; +/** A cut's share of a punch's blood (the engine's bleeding over eight) once it is open: bleeding of about 60. */ +const OPEN_WOUND = 8; +/** A drop the size of a gob of blood (this radius, in metres) always stains where it lands; fine spray does one time in four. */ +const GOB = 0.015; +/** The neck is close to round where it is cut. */ +export const NECK_WOUND_RADIUS = 0.068; const HAND_RADIUS = 0.085; const MAX_STEP = 0.05; const SIMULATION_STEP = 1 / 60; @@ -28,9 +62,18 @@ interface Droplet { } export interface BakedPart { + /** Carries the mesh's bind positions in a `bindPosition` attribute. */ readonly geometry: THREE.BufferGeometry; readonly map: THREE.Texture | null; readonly color: number; + /** The cut that freed the part, in the part's own frame: its middle, and the flesh that closes it, measured from there. */ + readonly cut?: { readonly position: THREE.Vector3; readonly flesh: THREE.BufferGeometry }; + /** Whose head it is, so it keeps its hair and beard once it is off. */ + readonly look?: FighterLook; + /** How soaked a severed glove was with the opponent's blood (0..1), so it stays that way. */ + readonly gloveBlood?: number; + /** The injuries of the head it was cut from, which it keeps once it is off. */ + readonly injury?: InjuryShading; } interface Mist { @@ -47,19 +90,44 @@ interface Gib { vx: number; vy: number; vz: number; rx: number; ry: number; rz: number; vrx: number; vry: number; vrz: number; - life: number; scale: number; + /** Lengthens the chunk along one axis so no two look alike. */ + stretch: number; bounces: number; stained: boolean; - tooth: boolean; + kind: DebrisKind; + /** Lying still on the canvas, where it stays for the bout. */ + resting: boolean; + /** When it was thrown, so the oldest piece gives way to new ones. */ + born: number; + /** The fighter whose finisher threw it, or -1: it goes when his head or hand goes back on for a replay. */ + owner: number; + /** Seconds left as it shrinks away to make room, or -1. */ + fading: number; } +/** + * Flesh, teeth, brain and skull lie where they land for the rest of the bout, as the blood under them + * does. Once a pool is all but full the oldest piece on the canvas shrinks away to make room. + */ +type DebrisKind = "flesh" | "tooth" | "brain" | "shard"; +const DEBRIS_BOUNCES: Readonly> = { flesh: 1, tooth: 3, brain: 1, shard: 2 }; +const DEBRIS_FADE_SECONDS = 0.3; + interface SeveredHead { readonly mesh: THREE.Mesh; readonly defaultGeometry: THREE.BufferGeometry; readonly defaultScale: THREE.Vector3; readonly cap: THREE.Mesh; + readonly defaultCap: THREE.BufferGeometry; + /** Hair and beard of a severed head; null for a hand. */ + readonly look: LookShading | null; + /** The bruises, swelling, cuts and blood a severed head keeps; null for a hand. */ + readonly injury: InjuryShading | null; + /** The blood soaked into a severed glove; null for a head. */ + readonly blood: { value: number } | null; baked: THREE.BufferGeometry | null; + bakedFlesh: THREE.BufferGeometry | null; readonly radius: number; active: boolean; moving: boolean; @@ -68,10 +136,21 @@ interface SeveredHead { vrx: number; vry: number; vrz: number; bounces: number; stained: boolean; + /** A head rolls onto an ear once it stops: 0 to 1 through the roll, -1 when it is not rolling. */ + settle: number; + readonly settleFrom: THREE.Quaternion; + readonly settleTo: THREE.Quaternion; + settleFromY: number; + settleToY: number; + /** How far the part reaches to either side of its pivot, along its own side-to-side axis. */ + sideLow: number; + sideHigh: number; } interface Stump { readonly mesh: THREE.Mesh; + /** The wound shaped to the cut, once the renderer has measured it. */ + readonly flesh: THREE.BufferGeometry; active: boolean; fountainLife: number; accumulator: number; @@ -79,6 +158,57 @@ interface Stump { direction: SprayDirection; } +/** + * How long the artery of each stump pumps: a severed neck, the neck a burst head leaves and a wrist. Over the + * last FOUNTAIN_FADE_SECONDS the pressure falls away. + */ +export const NECK_FOUNTAIN_SECONDS = 6.5; +const BURST_FOUNTAIN_SECONDS = 7.5; +const WRIST_FOUNTAIN_SECONDS = 5; +const FOUNTAIN_FADE_SECONDS = 4; +/** Beats a second of a heart going flat out, and how much of each beat (a share of it) the spurt lasts. */ +const HEART_RATE = 1.7; +const SPURT_WIDTH = 0.14; +/** Drops a second at the top of a beat at full pressure, and the share of that still running between beats. */ +const FOUNTAIN_PEAK_RATE = 110; +const FOUNTAIN_BETWEEN_BEATS = 0.12; +/** Arterial blood leaves at 4 to 6 m/s at the top of a beat, within this angle (radians) of the wound's axis. */ +const FOUNTAIN_SPEED_LOW = 4; +const FOUNTAIN_SPEED_HIGH = 6; +const FOUNTAIN_CONE = 0.35; +const fountainAxis = new THREE.Vector3(); +const fountainSide = new THREE.Vector3(); +const fountainUp = new THREE.Vector3(); +const fountainVelocity = new THREE.Vector3(); + +/** + * The way a stump's wound faces, into `out`: the raised middle of the flesh closing its cut faces out of it, and + * a disc standing in for a cut that was not measured faces up its own frame. + */ +function stumpAxis(stump: Stump, out: THREE.Vector3): THREE.Vector3 { + const normal = stump.mesh.geometry === stump.flesh ? stump.flesh.getAttribute("normal") : undefined; + if (normal !== undefined && normal.count > 0) out.fromBufferAttribute(normal, 0); + else out.set(0, 1, 0).applyQuaternion(stump.mesh.quaternion); + return out.lengthSq() > 1e-8 ? out.normalize() : out.set(0, 1, 0); +} + +/** An eye hanging out of its socket on its nerve, swinging as a weight on a cord. */ +interface HangingEye { + active: boolean; + eventId: number | null; + readonly position: THREE.Vector3; + readonly previous: THREE.Vector3; + readonly socket: THREE.Vector3; + /** The way the face points, which the pupil half turns to as it hangs. */ + readonly facing: THREE.Vector3; + /** The middle of the skull and how the head is turned: the eye lies against the head rather than hanging through it. */ + readonly skull: THREE.Vector3; + readonly skullTurn: THREE.Quaternion; + hasSkull: boolean; + bleeding: number; + accumulator: number; +} + interface DripEmitter { readonly position: THREE.Vector3; active: boolean; @@ -93,6 +223,17 @@ const seeded = (seed: number): (() => number) => () => { }; const finite = (value: number, fallback = 0): number => Number.isFinite(value) ? value : fallback; +const idleGib = (): Gib => ({ alive: false, x: 0, y: -50, z: 0, vx: 0, vy: 0, vz: 0, rx: 0, ry: 0, rz: 0, vrx: 0, vry: 0, vrz: 0, scale: 0, stretch: 1, bounces: 0, stained: false, kind: "flesh", resting: false, born: 0, owner: -1, fading: -1 }); +const SHIELD_WHITE = new THREE.Color(0xf4f7fb); +const EYE_RADIUS = 0.012; +/** How far an eye forced out hangs below its socket on the nerve. */ +export const EYE_NERVE_LENGTH = 0.055; +const EYE_DAMPING = 0.985; +/** Half the width and height of the head about the middle of the skull, plus the eye, for a hanging eye. */ +const SKULL_RADII = new THREE.Vector2(0.085 + EYE_RADIUS, 0.12 + EYE_RADIUS); +/** How far in front of the socket's plane a hanging eye lies on the face, for the cheekbone and the lids. */ +const FACE_CLEARANCE = 0.012; +const SALIVA = { r: 0.82, g: 0.86, b: 0.9 } as const; /** Horizontal world direction (x and z) a spray flies in. */ export interface SprayDirection { @@ -108,9 +249,22 @@ function unitSpray(direction: number | SprayDirection): SprayDirection { } const unitY = new THREE.Vector3(0, 1, 0); +const UNIT_Z = new THREE.Vector3(0, 0, 1); const safeStep = (dt: number): number => Number.isFinite(dt) ? THREE.MathUtils.clamp(dt, 0, MAX_STEP) : 0; const IMPACT_KINDS = new Set(["hit", "counter_hit", "block", "perfect_block", "guard_break", "knockdown", "bleed"]); +/** + * One axis of a severed part against the ropes. A part that reaches them from inside bounces off. + * One cut off beyond them, on a fighter leaning into the ropes, is drawn back in rather than moved. + */ +export function confineToRopes(position: number, velocity: number, from: number, limit: number): { position: number; velocity: number } { + const excess = Math.abs(position) - limit; + if (excess <= 0) return { position, velocity }; + const side = Math.sign(position); + if (Math.abs(from) <= limit) return { position: side * limit, velocity: velocity * -0.42 }; + return { position, velocity: -side * Math.max(-side * velocity, excess * ROPE_RETURN_RATE) }; +} + export type BloodPattern = "jet" | "fan" | "plume" | "body_burst" | "ooze" | "impact"; export function bloodPatternFor(event: CombatEvent): BloodPattern { @@ -130,37 +284,55 @@ function copyFiniteQuaternion(target: THREE.Quaternion, source: THREE.Quaternion else target.normalize(); } -function splatTexture(seed: number): THREE.CanvasTexture { - const canvas = document.createElement("canvas"); - canvas.width = 128; - canvas.height = 128; - const ctx = canvas.getContext("2d"); - if (ctx !== null) { - const rand = seeded(seed); - ctx.clearRect(0, 0, 128, 128); - ctx.fillStyle = "rgba(255,255,255,1)"; - const blobs = 7 + Math.floor(rand() * 6); - for (let i = 0; i < blobs; i += 1) { - const angle = rand() * Math.PI * 2; - const distance = i === 0 ? 0 : 8 + rand() * 40; - const radius = i === 0 ? 24 + rand() * 12 : 3 + rand() * 12; - ctx.globalAlpha = i === 0 ? 1 : 0.7 + rand() * 0.3; - ctx.beginPath(); - ctx.ellipse(64 + Math.cos(angle) * distance, 64 + Math.sin(angle) * distance, radius, radius * (0.55 + rand() * 0.5), rand() * Math.PI, 0, Math.PI * 2); - ctx.fill(); - } - ctx.globalAlpha = 1; - for (let i = 0; i < 12; i += 1) { - const angle = rand() * Math.PI * 2; - const distance = 30 + rand() * 30; - ctx.beginPath(); - ctx.arc(64 + Math.cos(angle) * distance, 64 + Math.sin(angle) * distance, 1 + rand() * 3, 0, Math.PI * 2); - ctx.fill(); +/** A severed head's skin, as the live head's is made (graph.ts applyFighterSkin): it is drawn by the same shader once baked. */ +function severedHeadMaterial(): THREE.MeshPhysicalMaterial { + return new THREE.MeshPhysicalMaterial({ color: 0x8a4d32, roughness: 0.58, metalness: 0.02, clearcoat: 0.25, clearcoatRoughness: 0.6 }); +} + +/** + * The turn and scale that carry bind space (centimetres) into a baked part's own frame, fitted to the + * baked positions: the bind positions reshaped by the look, then posed. A part baked rigid fits exactly. + */ +function bindToPart(geometry: THREE.BufferGeometry, look: FighterLook, out: THREE.Matrix3): THREE.Matrix3 { + out.identity(); + const position = geometry.getAttribute("position"); + const bind = geometry.getAttribute("bindPosition"); + const index = geometry.getIndex(); + if (bind === undefined || bind.count !== position.count) return out; + const used = new Uint8Array(position.count); + if (index === null) used.fill(1); + else for (let corner = 0; corner < index.count; corner += 1) used[index.getX(corner)] = 1; + const posed = new THREE.Vector3(); + const shaped = new THREE.Vector3(); + const posedMean = new THREE.Vector3(); + const shapedMean = new THREE.Vector3(); + let count = 0; + for (let vertex = 0; vertex < position.count; vertex += 1) { + if (used[vertex] === 0) continue; + posedMean.add(posed.fromBufferAttribute(position, vertex)); + shapedMean.add(lookShape(shaped.fromBufferAttribute(bind, vertex), look, shaped)); + count += 1; + } + if (count < 4) return out; + posedMean.divideScalar(count); + shapedMean.divideScalar(count); + // Least squares: the map M with posed = M * shaped (both about their means) is across * spread^-1. + const across = new Array(9).fill(0); + const spread = new Array(9).fill(0); + for (let vertex = 0; vertex < position.count; vertex += 1) { + if (used[vertex] === 0) continue; + posed.fromBufferAttribute(position, vertex).sub(posedMean); + lookShape(shaped.fromBufferAttribute(bind, vertex), look, shaped).sub(shapedMean); + for (let row = 0; row < 3; row += 1) { + for (let column = 0; column < 3; column += 1) { + across[row * 3 + column]! += posed.getComponent(row) * shaped.getComponent(column); + spread[row * 3 + column]! += shaped.getComponent(row) * shaped.getComponent(column); + } } } - const texture = new THREE.CanvasTexture(canvas); - texture.colorSpace = THREE.NoColorSpace; - return texture; + const spreadMatrix = new THREE.Matrix3().set(...(spread as [number, number, number, number, number, number, number, number, number])); + if (Math.abs(spreadMatrix.determinant()) < 1e-9) return out; + return out.set(...(across as [number, number, number, number, number, number, number, number, number])).multiply(spreadMatrix.invert()); } function mistTexture(): THREE.CanvasTexture { @@ -185,14 +357,14 @@ export class Effects3D { private readonly dropletPositions: Float32Array; private readonly dropletColors: Float32Array; readonly dropletBuffers: { readonly position: THREE.BufferAttribute; readonly color: THREE.BufferAttribute }; - private readonly dropletGeometry: THREE.SphereGeometry; + private readonly dropletGeometry: THREE.BufferGeometry; + private readonly dropletShape = { width: 0, length: 0 }; private readonly dropletMaterial: THREE.MeshStandardMaterial; private readonly dropletMatrix = new THREE.Matrix4(); private readonly dropletQuaternion = new THREE.Quaternion(); private readonly dropletScale = new THREE.Vector3(); private readonly dropletVelocity = new THREE.Vector3(); private readonly dropletColor = new THREE.Color(); - private readonly splatMaps: THREE.CanvasTexture[] = []; private dropletIndex = 0; /** Live droplets fill the front of the pool, and only they are drawn and uploaded. */ private liveDroplets = 0; @@ -206,16 +378,20 @@ export class Effects3D { private readonly mistMap: THREE.CanvasTexture; private mistIndex = 0; - private readonly decals: THREE.Mesh[] = []; - private readonly decalGeometry: THREE.PlaneGeometry; - private decalIndex = 0; + private readonly canvasBlood: CanvasBlood; - private readonly gibGeometry: THREE.IcosahedronGeometry; + private readonly gibGeometry: THREE.BufferGeometry; private readonly gibMaterial: THREE.MeshStandardMaterial; private readonly gibMesh: THREE.InstancedMesh; + private readonly shardGeometry: THREE.BufferGeometry; + private readonly shardMaterial: THREE.MeshStandardMaterial; + private readonly shardMesh: THREE.InstancedMesh; + private readonly shards: Gib[] = []; + private shardIndex = 0; private readonly gibColor = new THREE.Color(); private readonly gibs: Gib[] = []; private gibIndex = 0; + private debrisThrown = 0; private readonly gibMatrix = new THREE.Matrix4(); private readonly gibPosition = new THREE.Vector3(); private readonly gibQuaternion = new THREE.Quaternion(); @@ -228,14 +404,45 @@ export class Effects3D { private readonly handGeometry: THREE.CapsuleGeometry; private readonly handMaterials: THREE.MeshStandardMaterial[] = []; private readonly hands: SeveredHead[] = []; - private readonly stumpGeometry: THREE.CylinderGeometry; - private readonly wristStumpGeometry: THREE.CylinderGeometry; + private readonly stumpGeometry: THREE.BufferGeometry; + private readonly wristStumpGeometry: THREE.BufferGeometry; private readonly stumpMaterial: THREE.MeshStandardMaterial; + private readonly stumpMap: THREE.CanvasTexture; + private readonly wristMaterial: THREE.MeshStandardMaterial; + private readonly wristMap: THREE.CanvasTexture; + private readonly jawMaterial: THREE.MeshStandardMaterial; + private readonly jawMap: THREE.CanvasTexture; + private readonly stumpAcross = new THREE.Vector3(); + private readonly bindToPartMatrix = new THREE.Matrix3(); + private readonly settleAxis = new THREE.Vector3(); + private readonly settleUp = new THREE.Vector3(); + private dropletCloseness = 1; + private readonly stumpOutward = new THREE.Vector3(); private readonly stumps: Stump[] = []; private readonly handStumps: Stump[] = []; private readonly lastDecapitationEvent = [null, null] as Array; + private readonly lastBurstEvent = [null, null] as Array; private readonly lastDismembermentEvent = Array(MAX_HANDS).fill(null); + private readonly eyeGeometry: THREE.SphereGeometry; + private readonly eyeMap: THREE.CanvasTexture; + private readonly eyeMaterial: THREE.MeshStandardMaterial; + private readonly nerveGeometry: THREE.CylinderGeometry; + private readonly nerveMaterial: THREE.MeshStandardMaterial; + private readonly eyeMeshes: THREE.Mesh[] = []; + private readonly nerveMeshes: THREE.Mesh[] = []; + private readonly eyes: HangingEye[] = []; + private readonly eyeDirection = new THREE.Vector3(); + private readonly eyeTurn = new THREE.Quaternion(); + private readonly eyeSocketLocal = new THREE.Vector3(); + private readonly shieldGeometry: THREE.BufferGeometry; + private readonly shieldMaterials: THREE.MeshStandardMaterial[] = []; + private readonly shieldMeshes: THREE.Mesh[] = []; + private readonly shields: ShieldState[] = [idleShield(), idleShield()]; + private readonly lastShieldEvent = [null, null] as Array; + /** Materials only for the shader pre-compile, made on the first call for it. */ + private bakedStandIns: { readonly map: THREE.Texture; readonly materials: readonly THREE.MeshStandardMaterial[] } | null = null; + private readonly dripEmitters: [DripEmitter, DripEmitter] = [ { position: new THREE.Vector3(), active: false, rate: 0, accumulator: 0 }, { position: new THREE.Vector3(), active: false, rate: 0, accumulator: 0 }, @@ -245,15 +452,16 @@ export class Effects3D { private shake = 0; private bloodLevel: BloodLevel = "full"; - constructor(private readonly scene: THREE.Scene) { + /** `bloodCanvasSize` is the resolution blood on the canvas is painted at, in pixels across the ring. */ + constructor(private readonly scene: THREE.Scene, bloodCanvasSize = 1024) { this.dropletPositions = new Float32Array(MAX_DROPLETS * 3); this.dropletColors = new Float32Array(MAX_DROPLETS * 3); this.dropletBuffers = { position: new THREE.BufferAttribute(this.dropletPositions, 3), color: new THREE.BufferAttribute(this.dropletColors, 3), }; - this.dropletGeometry = new THREE.SphereGeometry(1, 7, 5); - this.dropletMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.22, metalness: 0.02 }); + this.dropletGeometry = buildDropletGeometry(); + this.dropletMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.3, metalness: 0.02 }); this.dropletMesh = new THREE.InstancedMesh(this.dropletGeometry, this.dropletMaterial, MAX_DROPLETS); this.dropletMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage); this.dropletMesh.frustumCulled = false; @@ -268,15 +476,14 @@ export class Effects3D { this.dropletMesh.instanceColor?.setUsage(THREE.DynamicDrawUsage); this.dropletMesh.instanceMatrix.needsUpdate = true; this.dropletMesh.count = 0; - for (let i = 0; i < 4; i += 1) this.splatMaps.push(splatTexture(0x3a1f_00d1 + i * 977)); this.mistPositions = new Float32Array(MAX_MIST * 3); - this.mistColors = new Float32Array(MAX_MIST * 3); + this.mistColors = new Float32Array(MAX_MIST * 4).fill(1); this.mistGeometry = new THREE.BufferGeometry(); this.mistGeometry.setAttribute("position", new THREE.BufferAttribute(this.mistPositions, 3)); - this.mistGeometry.setAttribute("color", new THREE.BufferAttribute(this.mistColors, 3)); + this.mistGeometry.setAttribute("color", new THREE.BufferAttribute(this.mistColors, 4)); this.mistMap = mistTexture(); - this.mistMaterial = new THREE.PointsMaterial({ size: 0.42, map: this.mistMap, transparent: true, opacity: 0.68, depthWrite: false, sizeAttenuation: true }); + this.mistMaterial = new THREE.PointsMaterial({ size: 0.36, map: this.mistMap, transparent: true, opacity: 0.6, depthWrite: false, sizeAttenuation: true, vertexColors: true }); this.mistPoints = new THREE.Points(this.mistGeometry, this.mistMaterial); this.mistPoints.frustumCulled = false; scene.add(this.mistPoints); @@ -285,84 +492,159 @@ export class Effects3D { this.mistPositions[i * 3 + 1] = -50; } - this.decalGeometry = new THREE.PlaneGeometry(0.2, 0.2); - for (let i = 0; i < MAX_DECALS; i += 1) { - const decal = new THREE.Mesh(this.decalGeometry, new THREE.MeshStandardMaterial({ color: 0x6e0d13, map: this.splatMaps[i % this.splatMaps.length]!, alphaMap: this.splatMaps[i % this.splatMaps.length]!, transparent: true, opacity: 0.42, depthWrite: false, roughness: 0.25, metalness: 0 })); - decal.rotation.x = -Math.PI / 2; - decal.position.y = CANVAS_TOP + 0.004 + i * 0.00015; - decal.visible = false; - this.decals.push(decal); - scene.add(decal); - } + this.canvasBlood = new CanvasBlood(scene, bloodCanvasSize); - this.gibGeometry = new THREE.IcosahedronGeometry(0.04, 0); - this.gibMaterial = new THREE.MeshStandardMaterial({ color: 0x650a10, roughness: 0.82, metalness: 0 }); + this.gibGeometry = buildChunkGeometry(); + // Each piece carries its whole colour, so teeth are white and brain is grey beside the flesh. + this.gibMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.34, metalness: 0 }); this.gibMesh = new THREE.InstancedMesh(this.gibGeometry, this.gibMaterial, MAX_GIBS); this.gibMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage); this.gibMesh.frustumCulled = false; scene.add(this.gibMesh); for (let i = 0; i < MAX_GIBS; i += 1) { - const gib: Gib = { alive: false, x: 0, y: -50, z: 0, vx: 0, vy: 0, vz: 0, rx: 0, ry: 0, rz: 0, vrx: 0, vry: 0, vrz: 0, life: 0, scale: 0, bounces: 0, stained: false, tooth: false }; + const gib = idleGib(); this.gibs.push(gib); - this.gibMesh.setColorAt(i, this.gibColor.setHex(0xffffff)); - this.writeGibMatrix(i, gib); + this.gibMesh.setColorAt(i, this.gibColor.setHex(0x5c0a10)); + this.writeDebrisMatrix(this.gibMesh, i, gib); } this.gibMesh.instanceMatrix.needsUpdate = true; + this.shardGeometry = buildShardGeometry(); + this.shardMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.5, metalness: 0, vertexColors: true, side: THREE.DoubleSide }); + this.shardMesh = new THREE.InstancedMesh(this.shardGeometry, this.shardMaterial, MAX_SHARDS); + this.shardMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage); + this.shardMesh.frustumCulled = false; + this.shardMesh.castShadow = true; + scene.add(this.shardMesh); + for (let i = 0; i < MAX_SHARDS; i += 1) { + const shard = idleGib(); + this.shards.push(shard); + this.shardMesh.setColorAt(i, this.gibColor.setHex(0xffffff)); + this.writeDebrisMatrix(this.shardMesh, i, shard); + } + this.shardMesh.instanceMatrix.needsUpdate = true; this.headGeometry = new THREE.SphereGeometry(HEAD_RADIUS, 18, 14); this.handGeometry = new THREE.CapsuleGeometry(0.055, 0.09, 6, 12); - this.stumpGeometry = new THREE.CylinderGeometry(0.072, 0.09, 0.035, 12); - this.wristStumpGeometry = new THREE.CylinderGeometry(0.035, 0.05, 0.03, 10); - this.stumpMaterial = new THREE.MeshStandardMaterial({ color: 0x56070c, roughness: 0.88, metalness: 0 }); + this.stumpGeometry = buildWoundGeometry(NECK_WOUND_RADIUS, NECK_WOUND_RADIUS); + this.wristStumpGeometry = buildWoundGeometry(0.038, 0.042, 0.006); + this.stumpMap = woundTexture(); + this.stumpMaterial = new THREE.MeshStandardMaterial({ map: this.stumpMap, roughness: 0.3, metalness: 0 }); + this.wristMap = wristWoundTexture(); + this.wristMaterial = new THREE.MeshStandardMaterial({ map: this.wristMap, roughness: 0.3, metalness: 0 }); + this.jawMap = jawWoundTexture(); + this.jawMaterial = new THREE.MeshStandardMaterial({ map: this.jawMap, roughness: 0.28, metalness: 0 }); for (let i = 0; i < MAX_HEADS; i += 1) { - const headMaterial = new THREE.MeshStandardMaterial({ color: 0x8a4d32, roughness: 0.76, metalness: 0 }); + // The skin of the live head: clear-coated for sweat, beaten as it was, and in its owner's look. + const headMaterial = severedHeadMaterial(); this.headMaterials.push(headMaterial); + const headInjury = new InjuryShading(headMaterial, HEAD_SITES, { core: HEAD_SWELL_CORE, lids: EYE_LIDS, baked: true }); + const headLook = new LookShading(headMaterial, true); const headMesh = new THREE.Mesh(this.headGeometry, headMaterial); headMesh.scale.set(0.82, 1.08, 0.9); headMesh.castShadow = true; headMesh.visible = false; scene.add(headMesh); - const cap = new THREE.Mesh(new THREE.CircleGeometry(0.058, 14), this.stumpMaterial); - cap.rotation.x = Math.PI / 2; - cap.position.y = -0.1; + const cap = new THREE.Mesh(this.stumpGeometry, this.stumpMaterial); cap.visible = false; headMesh.add(cap); - this.heads.push({ mesh: headMesh, defaultGeometry: this.headGeometry, defaultScale: headMesh.scale.clone(), cap, baked: null, radius: HEAD_RADIUS, active: false, moving: false, eventId: null, vx: 0, vy: 0, vz: 0, vrx: 0, vry: 0, vrz: 0, bounces: 0, stained: false }); + this.heads.push({ mesh: headMesh, defaultGeometry: this.headGeometry, defaultScale: headMesh.scale.clone(), cap, defaultCap: this.stumpGeometry, look: headLook, injury: headInjury, blood: null, baked: null, bakedFlesh: null, radius: HEAD_RADIUS, active: false, moving: false, eventId: null, vx: 0, vy: 0, vz: 0, vrx: 0, vry: 0, vrz: 0, bounces: 0, stained: false, settle: -1, settleFrom: new THREE.Quaternion(), settleTo: new THREE.Quaternion(), settleFromY: 0, settleToY: 0, sideLow: -HEAD_RADIUS, sideHigh: HEAD_RADIUS }); const stumpMesh = new THREE.Mesh(this.stumpGeometry, this.stumpMaterial); stumpMesh.visible = false; scene.add(stumpMesh); - this.stumps.push({ mesh: stumpMesh, active: false, fountainLife: 0, accumulator: 0, seed: 1, direction: { x: 1, z: 0 } }); + this.stumps.push({ mesh: stumpMesh, flesh: new THREE.BufferGeometry(), active: false, fountainLife: 0, accumulator: 0, seed: 1, direction: { x: 1, z: 0 } }); + } + this.eyeGeometry = new THREE.SphereGeometry(EYE_RADIUS, 16, 12); + this.eyeMap = eyeTexture(); + this.eyeMaterial = new THREE.MeshStandardMaterial({ map: this.eyeMap, roughness: 0.18, metalness: 0 }); + this.nerveGeometry = new THREE.CylinderGeometry(0.0034, 0.0045, 1, 8, 1, true); + this.nerveGeometry.translate(0, 0.5, 0); + this.nerveMaterial = new THREE.MeshStandardMaterial({ color: 0xb04048, roughness: 0.25, metalness: 0, side: THREE.DoubleSide }); + for (let index = 0; index < MAX_HEADS; index += 1) { + const eye = new THREE.Mesh(this.eyeGeometry, this.eyeMaterial); + const nerve = new THREE.Mesh(this.nerveGeometry, this.nerveMaterial); + eye.visible = false; + nerve.visible = false; + eye.castShadow = true; + scene.add(eye, nerve); + this.eyeMeshes.push(eye); + this.nerveMeshes.push(nerve); + this.eyes.push({ active: false, eventId: null, position: new THREE.Vector3(), previous: new THREE.Vector3(), socket: new THREE.Vector3(), facing: new THREE.Vector3(0, 0, 1), skull: new THREE.Vector3(), skullTurn: new THREE.Quaternion(), hasSkull: false, bleeding: 0, accumulator: 0 }); + } + this.shieldGeometry = buildMouthpieceGeometry(); + for (let index = 0; index < MAX_HEADS; index += 1) { + const material = new THREE.MeshStandardMaterial({ color: 0xe8eef5, roughness: 0.22, metalness: 0, side: THREE.DoubleSide }); + this.shieldMaterials.push(material); + const mesh = new THREE.Mesh(this.shieldGeometry, material); + mesh.castShadow = true; + mesh.visible = false; + scene.add(mesh); + this.shieldMeshes.push(mesh); } for (let index = 0; index < MAX_HANDS; index += 1) { const material = new THREE.MeshStandardMaterial({ color: 0x1d4ed8, roughness: 0.38, metalness: 0.03 }); + const blood = wearCornerColour(material, true); this.handMaterials.push(material); const handMesh = new THREE.Mesh(this.handGeometry, material); handMesh.scale.set(1.15, 1, 1.35); handMesh.castShadow = true; handMesh.visible = false; scene.add(handMesh); - const cap = new THREE.Mesh(new THREE.CircleGeometry(0.04, 12), this.stumpMaterial); - cap.rotation.x = Math.PI / 2; + const cap = new THREE.Mesh(this.wristStumpGeometry, this.wristMaterial); + cap.rotation.x = Math.PI; cap.position.y = 0.02; cap.visible = false; handMesh.add(cap); - this.hands.push({ mesh: handMesh, defaultGeometry: this.handGeometry, defaultScale: handMesh.scale.clone(), cap, baked: null, radius: HAND_RADIUS, active: false, moving: false, eventId: null, vx: 0, vy: 0, vz: 0, vrx: 0, vry: 0, vrz: 0, bounces: 0, stained: false }); + this.hands.push({ mesh: handMesh, defaultGeometry: this.handGeometry, defaultScale: handMesh.scale.clone(), cap, defaultCap: this.wristStumpGeometry, look: null, injury: null, blood, baked: null, bakedFlesh: null, radius: HAND_RADIUS, active: false, moving: false, eventId: null, vx: 0, vy: 0, vz: 0, vrx: 0, vry: 0, vrz: 0, bounces: 0, stained: false, settle: -1, settleFrom: new THREE.Quaternion(), settleTo: new THREE.Quaternion(), settleFromY: 0, settleToY: 0, sideLow: -HAND_RADIUS, sideHigh: HAND_RADIUS }); - const stumpMesh = new THREE.Mesh(this.wristStumpGeometry, this.stumpMaterial); + const stumpMesh = new THREE.Mesh(this.wristStumpGeometry, this.wristMaterial); stumpMesh.visible = false; scene.add(stumpMesh); - this.handStumps.push({ mesh: stumpMesh, active: false, fountainLife: 0, accumulator: 0, seed: 1, direction: { x: 1, z: 0 } }); + this.handStumps.push({ mesh: stumpMesh, flesh: new THREE.BufferGeometry(), active: false, fountainLife: 0, accumulator: 0, seed: 1, direction: { x: 1, z: 0 } }); } } /** - * Stand-ins sharing the hidden pools' geometry and materials (blood on the canvas, severed parts and - * their stumps), so their shaders can be compiled ahead of the first bloody hit instead of inside it. + * Droplets are drawn oversized so they read from the broadcast camera; a close camera shrinks + * them toward their real size. `distance` is from the camera to what it is looking at. + */ + setViewDistance(distance: number): void { + this.dropletCloseness = THREE.MathUtils.clamp(finite(distance, 5) / 5, 0.4, 1); + } + + /** + * Stand-ins sharing the hidden pools' geometry and materials (blood on the canvas, severed heads and + * gloves and their wounds, the jaw a burst head leaves, a forced-out eye on its nerve, a gum shield), + * so their shaders can be compiled ahead of the first bloody hit instead of inside it. */ compileStandIns(): THREE.Mesh[] { - const hidden = [this.decals[0]!, this.heads[0]!.mesh, this.hands[0]!.mesh, this.stumps[0]!.mesh, this.handStumps[0]!.mesh]; - return hidden.map((mesh) => new THREE.Mesh(mesh.geometry, mesh.material)); + const hidden = [ + this.canvasBlood.mesh, + this.heads[0]!.mesh, + this.hands[0]!.mesh, + this.stumps[0]!.mesh, + this.handStumps[0]!.mesh, + this.eyeMeshes[0]!, + this.nerveMeshes[0]!, + this.shieldMeshes[0]!, + ]; + // Each casts a shadow as its pool does, so the key light's shadow pass can be compiled for it too. + const standIns = hidden.map((mesh) => Object.assign(new THREE.Mesh(mesh.geometry, mesh.material), { castShadow: mesh.castShadow })); + standIns.push(Object.assign(new THREE.Mesh(this.stumpGeometry, this.jawMaterial), { castShadow: this.stumps[0]!.mesh.castShadow })); + // Baked from a fighter, a severed head or glove is drawn with its owner's texture: a variant of its + // shader of its own, compiled here on materials in that state, which share its program. + if (this.bakedStandIns === null) { + const map = new THREE.Texture(); + const head = severedHeadMaterial(); + head.map = map; + new InjuryShading(head, HEAD_SITES, { core: HEAD_SWELL_CORE, lids: EYE_LIDS, baked: true }); + new LookShading(head, true); + const glove = new THREE.MeshStandardMaterial({ color: 0x1d4ed8, roughness: 0.38, metalness: 0.03, map }); + wearCornerColour(glove, true); + this.bakedStandIns = { map, materials: [head, glove] }; + } + for (const material of this.bakedStandIns.materials) standIns.push(Object.assign(new THREE.Mesh(this.headGeometry, material), { castShadow: true })); + return standIns; } setBloodLevel(level: BloodLevel): void { @@ -388,18 +670,43 @@ export class Effects3D { return this.mists.filter((mist) => mist.alive).length; } - get visibleDecals(): number { - return this.decals.filter((decal) => decal.visible).length; + /** Stains of blood painted on the canvas since it was last cleaned. */ + get canvasStains(): number { + return this.canvasBlood.stains; + } + + /** Sends only the newly painted part of the canvas blood to the GPU from now on. */ + useUploader(uploader: RegionUploader): void { + this.canvasBlood.useUploader(uploader); + } + + setLowTier(low: boolean): void { + this.canvasBlood.setLowTier(low); } get liveGibs(): number { return this.gibs.filter((gib) => gib.alive).length; } + get liveShards(): number { + return this.shards.filter((shard) => shard.alive).length; + } + + /** How many teeth, pieces of brain and pieces of skull are out, by kind. */ + debrisCount(kind: DebrisKind): number { + return [...this.gibs, ...this.shards].filter((gib) => gib.alive && gib.kind === kind).length; + } + get activeHeads(): number { return this.heads.filter((head) => head.active).length; } + /** Whether a fighter's head has burst, leaving the jaw on his neck. */ + headBurst(fighterIndex: number): boolean { + const stump = this.stumps[Math.trunc(fighterIndex)]; + return stump !== undefined && stump.active && stump.mesh.material === this.jawMaterial; + } + /** Copies the world position of a fighter's severed head into `out`; false when the head is still on. */ severedHeadPosition(fighterIndex: number, out: THREE.Vector3): boolean { const head = this.heads[Math.trunc(fighterIndex)]; @@ -408,6 +715,259 @@ export class Effects3D { return true; } + /** Copies the direction a severed head's face points into `out`; false when the head is still on. */ + severedHeadFacing(fighterIndex: number, out: THREE.Vector3): boolean { + const head = this.heads[Math.trunc(fighterIndex)]; + if (head === undefined || !head.active) return false; + out.set(0, 0, 1).applyQuaternion(head.mesh.quaternion); + return true; + } + + /** Whether a fighter's gum shield is out of his mouth. */ + mouthpieceOut(fighterIndex: number): boolean { + return this.shields[Math.trunc(fighterIndex)]?.out === true; + } + + /** Copies where a fighter's gum shield is into `out`; false while it is still in his mouth. */ + mouthpiecePosition(fighterIndex: number, out: THREE.Vector3): boolean { + const shield = this.shields[Math.trunc(fighterIndex)]; + if (shield === undefined || !shield.out) return false; + out.set(shield.x, shield.y, shield.z); + return true; + } + + /** + * Knocks a fighter's gum shield out of his mouth along the punch: it tumbles, bounces and slides on + * the canvas and lies there until the round ends. Once out it stays out, unless `again` replays the + * blow. A spray of spit follows it, two thirds of it blood with full blood and a third with reduced. + */ + ejectMouthpiece(fighterIndex: number, mouth: THREE.Vector3, quaternion: THREE.Quaternion, direction: number | SprayDirection, eventId: number, color: number, again = false): boolean { + if (fighterIndex < 0 || fighterIndex >= MAX_HEADS) return false; + const index = Math.trunc(fighterIndex); + const shield = this.shields[index]!; + const safeEventId = Number.isSafeInteger(eventId) ? eventId : 0; + if ((shield.out && !again) || this.lastShieldEvent[index] === safeEventId) return false; + this.lastShieldEvent[index] = safeEventId; + const rand = seeded(safeEventId * 48271 + index * 7907 + 5); + const launch = unitSpray(direction); + const along = 0.9 + rand() * 0.9; + const vy = 0.9 + rand() * 0.9; + const across = (rand() - 0.5) * 1.1; + Object.assign(shield, idleShield(), { + out: true, + moving: true, + x: finite(mouth.x), y: finite(mouth.y, 1.45), z: finite(mouth.z), + vx: along * launch.x - across * launch.z, + vy, + vz: along * launch.z + across * launch.x, + wx: (rand() - 0.5) * 26, + wy: (rand() - 0.5) * 18, + wz: (rand() - 0.5) * 26, + restYaw: rand() * Math.PI * 2, + } satisfies Partial); + const mesh = this.shieldMeshes[index]!; + mesh.position.set(shield.x, shield.y, shield.z); + copyFiniteQuaternion(mesh.quaternion, quaternion); + mesh.visible = true; + const material = this.shieldMaterials[index]!; + material.color.setHex(color).lerp(SHIELD_WHITE, 0.55); + const full = this.bloodLevel === "full"; + for (let drop = 0; drop < (full ? 22 : 12); drop += 1) { + const angle = rand() * Math.PI * 2; + const blood = full ? drop % 3 !== 0 : this.bloodLevel === "reduced" && drop % 3 === 0; + this.spawnDroplet( + shield.x + (rand() - 0.5) * 0.03, + shield.y + (rand() - 0.5) * 0.03, + shield.z + (rand() - 0.5) * 0.03, + shield.vx * (0.5 + rand() * 0.6) + Math.sin(angle) * 0.4, + shield.vy * (0.4 + rand() * 0.6), + shield.vz * 0.5 + Math.cos(angle) * 0.4, + blood ? bloodShade(rand()) : SALIVA, + 0.5 + rand() * 0.5, + blood, + ); + } + return true; + } + + /** + * Forces a fighter's eye out of its socket at `socket`, along `forward` (the way the face points): + * it springs out, then hangs and swings on its nerve, bleeding from the empty socket. + */ + gougeEye(fighterIndex: number, socket: THREE.Vector3, forward: THREE.Vector3, direction: number | SprayDirection, eventId: number): void { + if (this.bloodLevel !== "full" || fighterIndex < 0 || fighterIndex >= MAX_HEADS) return; + const index = Math.trunc(fighterIndex); + const eye = this.eyes[index]!; + const safeEventId = Number.isSafeInteger(eventId) ? eventId : 0; + if (eye.active || eye.eventId === safeEventId) return; + eye.active = true; + eye.eventId = safeEventId; + eye.socket.set(finite(socket.x), finite(socket.y, 1.6), finite(socket.z)); + eye.facing.copy(forward); + eye.hasSkull = false; + eye.position.copy(eye.socket).addScaledVector(forward, EYE_RADIUS * 1.5); + // It leaves at about a metre and a half a second, out of the face and away from the punch. + this.eyeDirection.copy(forward).multiplyScalar(1.3); + const launch = unitSpray(direction); + this.eyeDirection.x += launch.x * 0.5; + this.eyeDirection.z += launch.z * 0.5; + eye.previous.copy(eye.position).addScaledVector(this.eyeDirection, -SIMULATION_STEP); + eye.bleeding = 2; + eye.accumulator = 0; + this.eyeMeshes[index]!.visible = true; + this.nerveMeshes[index]!.visible = true; + const rand = seeded(safeEventId * 52_711 + index * 3571 + 13); + for (let drop = 0; drop < 36; drop += 1) { + const angle = rand() * Math.PI * 2; + this.spawnDroplet(eye.socket.x, eye.socket.y, eye.socket.z, forward.x * (0.8 + rand()) + Math.sin(angle) * 0.6, 0.4 + rand() * 1.2, forward.z * (0.8 + rand()) + Math.cos(angle) * 0.6, bloodShade(rand()), 0.6 + rand() * 0.6, true); + } + for (let puff = 0; puff < 4; puff += 1) this.spawnMist(eye.socket.x + (rand() - 0.5) * 0.1, eye.socket.y + (rand() - 0.5) * 0.08, eye.socket.z + (rand() - 0.5) * 0.1, 0.7 + rand() * 0.6, 0.5 + rand() * 0.4); + this.writeEye(index); + } + + /** + * Keeps the nerve of a hanging eye in its socket as the head moves; `facing` is the way the face + * points and `skull` the middle of the head, which the eye lies against. + */ + anchorEye(fighterIndex: number, socket: THREE.Vector3, facing?: THREE.Vector3, skull?: THREE.Vector3, skullTurn?: THREE.Quaternion): void { + const eye = this.eyes[Math.trunc(fighterIndex)]; + if (eye === undefined || !eye.active) return; + eye.socket.set(finite(socket.x), finite(socket.y, 1.6), finite(socket.z)); + if (facing !== undefined && facing.lengthSq() > 1e-8) eye.facing.copy(facing).normalize(); + if (skull !== undefined && skullTurn !== undefined) { + eye.skull.set(finite(skull.x), finite(skull.y, 1.6), finite(skull.z)); + copyFiniteQuaternion(eye.skullTurn, skullTurn); + eye.hasSkull = true; + } + } + + eyeOut(fighterIndex: number): boolean { + return this.eyes[Math.trunc(fighterIndex)]?.active === true; + } + + /** Copies where a fighter's hanging eye is into `out`; false while it is in his head. */ + eyePosition(fighterIndex: number, out: THREE.Vector3): boolean { + const eye = this.eyes[Math.trunc(fighterIndex)]; + if (eye === undefined || !eye.active) return false; + out.copy(eye.position); + return true; + } + + private updateEyes(step: number): void { + for (const [index, eye] of this.eyes.entries()) { + if (!eye.active) continue; + // Verlet: what it moved last step, a little damped, plus gravity; then the nerve holds it. + this.eyeDirection.copy(eye.position).sub(eye.previous).multiplyScalar(EYE_DAMPING); + eye.previous.copy(eye.position); + eye.position.add(this.eyeDirection); + eye.position.y -= DROPLET_GRAVITY * step * step; + this.eyeDirection.copy(eye.position).sub(eye.socket); + const stretch = this.eyeDirection.length(); + if (stretch > EYE_NERVE_LENGTH) eye.position.copy(eye.socket).addScaledVector(this.eyeDirection, EYE_NERVE_LENGTH / stretch); + if (eye.hasSkull && this.restOnFace(eye)) { + // It rests where it touches rather than sliding round the head, and the nerve still holds it. + eye.previous.lerp(eye.position, 0.6); + this.eyeDirection.copy(eye.position).sub(eye.socket); + const held = this.eyeDirection.length(); + if (held > EYE_NERVE_LENGTH) eye.position.copy(eye.socket).addScaledVector(this.eyeDirection, EYE_NERVE_LENGTH / held); + } + if (eye.position.y < CANVAS_TOP + EYE_RADIUS) eye.position.y = CANVAS_TOP + EYE_RADIUS; + if (this.bloodLevel === "full" && eye.bleeding > 0) { + eye.bleeding = Math.max(0, eye.bleeding - step); + eye.accumulator += step * 14; + while (eye.accumulator >= 1) { + eye.accumulator -= 1; + this.spawnDroplet(eye.socket.x, eye.socket.y - 0.005, eye.socket.z, (this.ambientRandom() - 0.5) * 0.2, -0.2 - this.ambientRandom() * 0.3, (this.ambientRandom() - 0.5) * 0.2, bloodShade(this.ambientRandom()), 0.9, true); + } + } + this.writeEye(index); + } + } + + /** + * Keeps a hanging eye out of the head. Seen in the head's own frame the head behind the face is a + * column as wide and tall as the skull, ending in the plane of the face just in front of the socket; + * an eye inside it goes out the shorter way, to the face or to the side. True when it was moved. + */ + private restOnFace(eye: HangingEye): boolean { + this.eyeTurn.copy(eye.skullTurn).invert(); + const local = this.eyeDirection.copy(eye.position).sub(eye.skull).applyQuaternion(this.eyeTurn); + const face = this.eyeSocketLocal.copy(eye.socket).sub(eye.skull).applyQuaternion(this.eyeTurn).z + EYE_RADIUS + FACE_CLEARANCE; + if (local.z >= face) return false; + const across = Math.hypot(local.x / SKULL_RADII.x, local.y / SKULL_RADII.y); + if (across >= 1) return false; + const toFace = face - local.z; + const toSide = (1 - across) * Math.min(SKULL_RADII.x, SKULL_RADII.y); + if (toFace <= toSide || across < 1e-6) local.z = face; + else { + local.x /= across; + local.y /= across; + } + eye.position.copy(eye.skull).add(local.applyQuaternion(eye.skullTurn)); + return true; + } + + private writeEye(index: number): void { + const eye = this.eyes[index]!; + const ball = this.eyeMeshes[index]!; + const nerve = this.nerveMeshes[index]!; + ball.position.copy(eye.position); + // The nerve leaves the back of the eye, so the pupil looks away from the socket. + this.eyeDirection.copy(eye.position).sub(eye.socket); + const length = this.eyeDirection.length(); + if (length > 1e-5) { + this.eyeDirection.multiplyScalar(1 / length); + nerve.quaternion.setFromUnitVectors(unitY, this.eyeDirection); + this.eyeDirection.addScaledVector(eye.facing, 0.9).normalize(); + ball.quaternion.setFromUnitVectors(UNIT_Z, this.eyeDirection); + } + nerve.position.copy(eye.socket); + nerve.scale.set(1, Math.max(0.001, length - EYE_RADIUS * 0.8), 1); + } + + /** Puts one fighter's gum shield back in his mouth, for a replay that knocks it out again. */ + returnMouthpiece(fighterIndex: number): void { + const index = Math.trunc(fighterIndex); + const shield = this.shields[index]; + if (shield === undefined) return; + Object.assign(shield, idleShield()); + this.shieldMeshes[index]!.visible = false; + this.lastShieldEvent[index] = null; + } + + /** The corner puts the gum shield back in between rounds. */ + clearMouthpieces(): void { + for (const [index, shield] of this.shields.entries()) { + Object.assign(shield, idleShield()); + this.shieldMeshes[index]!.visible = false; + this.lastShieldEvent[index] = null; + } + } + + private updateShields(step: number): void { + for (const [index, shield] of this.shields.entries()) { + if (!shield.out || !shield.moving) continue; + const mesh = this.shieldMeshes[index]!; + const flying = shield.age < 0.3 && !shield.sliding; + const fromX = shield.x; + const fromZ = shield.z; + stepShield(shield, mesh.quaternion, step, CANVAS_TOP); + const limit = ROPE_LINE - SHIELD_RADIUS - SEVERED_PART_MARGIN; + const x = confineToRopes(shield.x, shield.vx, fromX, limit); + const z = confineToRopes(shield.z, shield.vz, fromZ, limit); + shield.x = x.position; + shield.vx = x.velocity; + shield.z = z.position; + shield.vz = z.velocity; + mesh.position.set(shield.x, shield.y, shield.z); + // A thread of spit and blood trails it for the first moments of its flight. + if (flying && this.ambientRandom() < step * 40) { + const blood = this.ambientRandom() < (this.bloodLevel === "full" ? 0.6 : this.bloodLevel === "reduced" ? 0.15 : 0); + this.spawnDroplet(shield.x, shield.y, shield.z, shield.vx * 0.6, shield.vy * 0.6, shield.vz * 0.6, blood ? bloodShade(this.ambientRandom()) : SALIVA, 0.45, blood, 0.003); + } + } + } + get activeHands(): number { return this.hands.filter((hand) => hand.active).length; } @@ -416,7 +976,7 @@ export class Effects3D { return [...this.stumps, ...this.handStumps].filter((stump) => stump.active).length; } - private spawnDroplet(x: number, y: number, z: number, vx: number, vy: number, vz: number, color: { r: number; g: number; b: number }, life: number, blood: boolean, radius = blood ? 0.006 + this.ambientRandom() * 0.009 : 0.004 + this.ambientRandom() * 0.004): void { + private spawnDroplet(x: number, y: number, z: number, vx: number, vy: number, vz: number, color: { r: number; g: number; b: number }, life: number, blood: boolean, radius = blood ? 0.0035 + this.ambientRandom() ** 3 * 0.0125 : 0.0025 + this.ambientRandom() ** 2 * 0.004): void { // A new droplet joins the live ones at the front; a full pool reuses its slots in turn. const index = this.liveDroplets < MAX_DROPLETS ? this.liveDroplets++ : this.dropletIndex++ % MAX_DROPLETS; const droplet = this.droplets[index]!; @@ -477,8 +1037,8 @@ export class Effects3D { } /** Spawns a droplet whose horizontal velocity is given along the spray and across it. */ - private sprayDroplet(spray: SprayDirection, x: number, y: number, z: number, along: number, vy: number, across: number, color: { r: number; g: number; b: number }, life: number, blood: boolean): void { - this.spawnDroplet(x, y, z, along * spray.x - across * spray.z, vy, along * spray.z + across * spray.x, color, life, blood); + private sprayDroplet(spray: SprayDirection, x: number, y: number, z: number, along: number, vy: number, across: number, color: { r: number; g: number; b: number }, life: number, blood: boolean, radius?: number): void { + this.spawnDroplet(x, y, z, along * spray.x - across * spray.z, vy, along * spray.z + across * spray.x, color, life, blood, radius); } private writeDropletMatrix(index: number, droplet: Droplet): void { @@ -490,15 +1050,15 @@ export class Effects3D { return; } const speed = Math.hypot(droplet.vx, droplet.vy, droplet.vz); - const stretch = 1 + Math.min(3.2, speed * 0.55); if (speed > 1e-4) { this.dropletVelocity.set(droplet.vx / speed, droplet.vy / speed, droplet.vz / speed); this.dropletQuaternion.setFromUnitVectors(unitY, this.dropletVelocity); } else { this.dropletQuaternion.identity(); } - const fade = Math.min(1, droplet.life / (droplet.maxLife * 0.3)); - this.dropletScale.set(droplet.radius * fade, droplet.radius * stretch * fade, droplet.radius * fade); + const fade = Math.min(1, droplet.life / (droplet.maxLife * 0.3)) * this.dropletCloseness; + const shape = dropletShape(droplet.radius * fade, speed, this.dropletShape); + this.dropletScale.set(shape.width, shape.length, shape.width); this.dropletMatrix.compose(this.dropletVelocity.set(droplet.x, droplet.y, droplet.z), this.dropletQuaternion, this.dropletScale); this.dropletMesh.setMatrixAt(index, this.dropletMatrix); } @@ -517,24 +1077,21 @@ export class Effects3D { this.mistPositions[index * 3] = mist.x; this.mistPositions[index * 3 + 1] = mist.y; this.mistPositions[index * 3 + 2] = mist.z; - this.mistColors[index * 3] = 0.6 * mist.scale; - this.mistColors[index * 3 + 1] = 0.07 * mist.scale; - this.mistColors[index * 3 + 2] = 0.08 * mist.scale; + this.mistColors[index * 4 + 3] = Math.min(1, mist.scale); this.mistGeometry.attributes.position!.needsUpdate = true; this.mistGeometry.attributes.color!.needsUpdate = true; } private placeDecal(x: number, z: number, scaleX: number, scaleZ: number, rotation: number, opacity: number, color: number): void { - const decal = this.decals[this.decalIndex % MAX_DECALS]!; - this.decalIndex += 1; - decal.visible = true; - decal.position.x = finite(x); - decal.position.z = finite(z); - decal.rotation.z = finite(rotation); - decal.scale.set(Math.max(0.01, finite(scaleX, 0.2)) * 1.4, Math.max(0.01, finite(scaleZ, 0.15)) * 1.4, 1); - const material = decal.material as THREE.MeshStandardMaterial; - material.opacity = THREE.MathUtils.clamp(finite(opacity, 0.4) * 1.6, 0, 1); - material.color.setHex(color); + this.canvasBlood.stain( + finite(x), + finite(z), + Math.max(0.01, finite(scaleX, 0.2)) * 0.28, + Math.max(0.01, finite(scaleZ, 0.15)) * 0.28, + finite(rotation), + THREE.MathUtils.clamp(finite(opacity, 0.4), 0, 0.95), + color, + ); } private ambientRandom(): number { @@ -543,10 +1100,15 @@ export class Effects3D { return ((this.ambientSeed ^ (this.ambientSeed >>> 14)) >>> 0) / 4294967296; } - splatter(x: number, z: number, scale: number, rand: () => number = () => this.ambientRandom()): void { + /** + * Blood thrown onto the canvas round `x`, `z`: `stamps` blots of about `opacity` each (a quarter as + * many, smaller and fainter, with reduced blood). One splash is a stain the canvas shows through, and + * where splashes land on each other it darkens, so the canvas gets bloodier as the bout goes on. + */ + splatter(x: number, z: number, scale: number, rand: () => number = () => this.ambientRandom(), stamps = 12, opacity = FINISHER_STAIN): void { if (this.bloodLevel === "off") return; const reduced = this.bloodLevel === "reduced"; - const drops = reduced ? 3 : 12; + const drops = reduced ? Math.ceil(stamps / 4) : stamps; const modeScale = reduced ? 0.35 : 1; for (let i = 0; i < drops; i += 1) { const angle = rand() * Math.PI * 2; @@ -557,23 +1119,17 @@ export class Effects3D { (0.35 + rand() * 0.9) * scale * modeScale, (0.2 + rand() * 0.65) * scale * modeScale, rand() * Math.PI, - (0.34 + rand() * 0.24) * modeScale, + opacity * (0.75 + rand() * 0.5) * (reduced ? 0.6 : 1), i === 0 ? 0x4c070b : 0x6e0d13, ); } } - pool(x: number, z: number, scale: number, rand: () => number = () => this.ambientRandom()): void { + /** Blood pooling on the canvas, `radius` metres from `x`, `z`; `stamp` picks the shape of its edge. */ + pool(x: number, z: number, radius: number, stamp: number): void { if (this.bloodLevel === "off") return; const reduced = this.bloodLevel === "reduced"; - const modeScale = reduced ? 0.35 : 1; - this.placeDecal(x, z, 2.8 * scale * modeScale, 2.0 * scale * modeScale, rand() * Math.PI, 0.56 * modeScale, 0x450609); - if (reduced) return; - for (let i = 0; i < 3; i += 1) { - const angle = rand() * Math.PI * 2; - const radius = 0.18 + rand() * 0.32; - this.placeDecal(x + Math.sin(angle) * radius, z + Math.cos(angle) * radius, 0.45 + rand() * 0.55, 0.22 + rand() * 0.38, rand() * Math.PI, 0.38 + rand() * 0.18, 0x620a10); - } + this.canvasBlood.pool(finite(x), finite(z), Math.max(0.02, finite(radius, 0.1)) * (reduced ? 0.5 : 1), reduced ? 0.45 : 0.92, stamp); } /** @@ -589,13 +1145,25 @@ export class Effects3D { // Sweat and chunks are pushed along the punch as hard as the event's sign says (a block may carry none). const push = spray === undefined ? Math.abs(finite(event.direction)) : 1; const origin = { x: finite(targetWorld.x), y: event.detail.endsWith(":body") ? 1.05 : 1.58, z: finite(targetWorld.z) }; - const sweatCount = reducedMotion ? 0 : Math.round((blocked ? 6 : 16) + Math.min(20, Math.max(0, event.amount) / 20)); + // A cut's beat throws no sweat and shakes nothing: no punch landed. + const beat = event.kind === "bleed"; + const sweatCount = reducedMotion || beat ? 0 : Math.round((blocked ? 6 : 16) + Math.min(20, Math.max(0, event.amount) / 20)); + const strand = { x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, life: 0, radius: 0, color: bloodShade(0) }; + // The engine's blood is the cut's bleeding over eight plus the damage over four: the first part is + // what an open wound throws. A block's amount is the guard's damage and a knockdown's its count, + // neither the weight of the punch. + const landed = event.kind === "hit" || event.kind === "counter_hit"; + const wound = landed ? Math.max(0, event.blood - Math.floor(Math.max(0, event.amount) / 4)) : 0; const bloodCount = reducedMotion || event.blood <= 0 || this.bloodLevel === "off" ? 0 - : this.bloodLevel === "reduced" - ? Math.min(24, Math.round(event.blood * 0.24)) - : Math.min(120, Math.round(event.blood * 1.1)); - this.shake = Math.min(0.09, this.shake + (blocked ? 0.008 : Math.max(0.012, event.amount / 2600))); + : beat + // The drip from the face carries the bleeding, and the beat adds a few drops. + ? (this.bloodLevel === "reduced" ? 1 : Math.min(5, 2 + Math.floor(Math.max(0, event.amount) / 12))) + : this.bloodLevel === "reduced" + ? Math.min(24, Math.round(event.blood * (blocked ? 0.12 : 0.24))) + // The guard takes the punch, and half the blood it would have thrown. + : blocked ? Math.round(bloodDropsFor(event.blood, 0) / 2) : bloodDropsFor(event.blood, landed ? event.amount : 0); + if (!beat) this.shake = Math.min(0.09, this.shake + (blocked ? 0.008 : Math.max(0.012, event.amount / 2600))); if (event.kind === "knockdown") this.shake = Math.min(0.14, this.shake + 0.06); for (let i = 0; i < sweatCount; i += 1) { @@ -615,7 +1183,13 @@ export class Effects3D { ); } for (let i = 0; i < bloodCount; i += 1) { - const arterial = i % 4 === 0; + const link = i % STRAND_PERIOD; + if (link > 0 && link <= STRAND_LINKS) { + const slower = 1 - STRAND_LAG * link; + this.spawnDroplet(strand.x, strand.y, strand.z, strand.vx * slower, strand.vy * slower, strand.vz * slower, strand.color, strand.life, true, strand.radius * (1 - STRAND_THINNING * link)); + continue; + } + const arterial = link === 0; const angle = (rand() - 0.5) * Math.PI; const speed = arterial ? 2.2 + rand() * 1.8 : 0.9 + rand() * 1.5; let along = speed; @@ -647,37 +1221,50 @@ export class Effects3D { vy = arterial ? 1.3 + rand() * 1.6 : 0.5 + rand() * 1.25; across = (rand() - 0.5) * speed * spread; } - const shade = rand(); - this.sprayDroplet( - launch, - origin.x + (rand() - 0.5) * 0.12, - origin.y + (rand() - 0.5) * 0.14, - origin.z + (rand() - 0.5) * 0.12, - along, - vy, - across, - shade < 0.3 ? { r: 0.72, g: 0.055, b: 0.08 } : shade < 0.7 ? { r: 0.5, g: 0.025, b: 0.045 } : { r: 0.3, g: 0.012, b: 0.025 }, - 0.65 + rand() * 0.85, - true, - ); + // Authored along and across the punch, turned onto the line it travelled; a strand's links follow its lead. + const vx = along * launch.x - across * launch.z; + const vz = along * launch.z + across * launch.x; + const color = bloodShade(rand()); + const x = origin.x + (rand() - 0.5) * 0.12; + const y = origin.y + (rand() - 0.5) * 0.14; + const z = origin.z + (rand() - 0.5) * 0.12; + const life = 0.65 + rand() * 0.85; + if (!arterial) { + this.spawnDroplet(x, y, z, vx, vy, vz, color, life, true); + continue; + } + strand.x = x; + strand.y = y; + strand.z = z; + strand.vx = vx; + strand.vy = vy; + strand.vz = vz; + strand.color = color; + strand.life = life; + strand.radius = 0.008 + rand() * 0.008; + this.spawnDroplet(x, y, z, vx, vy, vz, color, life, true, strand.radius); } - if (!reducedMotion && event.blood > 0 && this.bloodLevel !== "off" && (event.amount > 190 || event.kind === "knockdown" || event.kind === "counter_hit")) { - const puffs = this.bloodLevel === "reduced" ? (event.kind === "knockdown" ? 3 : 2) : (event.kind === "knockdown" ? 14 : 10); + const misty = (event.kind === "hit" && event.amount >= HARD_SHOT) || event.kind === "knockdown" || event.kind === "counter_hit" || wound >= OPEN_WOUND * 2; + if (!reducedMotion && event.blood > 0 && this.bloodLevel !== "off" && misty) { + // The worse the cut, the thicker the mist off every punch, so the blood in the air grows round by round. + const puffs = this.bloodLevel === "reduced" ? (event.kind === "knockdown" ? 3 : 2) : (event.kind === "knockdown" ? 14 : 10 + Math.min(8, Math.floor(wound / 4))); const scale = this.bloodLevel === "reduced" ? 0.35 : 1; for (let i = 0; i < puffs; i += 1) { this.spawnMist(origin.x + (rand() - 0.5) * 0.34, origin.y + (rand() - 0.5) * 0.26, origin.z + (rand() - 0.5) * 0.34, (0.9 + rand() * 1.2) * scale, 0.55 + rand() * 0.55); } } - if (event.blood > 6 && this.bloodLevel !== "off") { - this.splatter(origin.x + (rand() - 0.5) * 0.6, origin.z + (rand() - 0.5) * 0.6, 1 + Math.min(2, event.blood / 55), rand); + // The canvas is splashed by what an open wound throws and by a man going down, not by every punch + // that draws a little blood: the drops that land paint the rest. + const stamps = event.kind === "knockdown" ? 12 : Math.min(8, Math.round(wound / 4)); + if (stamps > 0 && this.bloodLevel !== "off") { + const knockdown = event.kind === "knockdown"; + this.splatter(origin.x + (rand() - 0.5) * 0.6, origin.z + (rand() - 0.5) * 0.6, knockdown ? 1 + Math.min(2, event.blood / 55) : 0.8 + Math.min(1.2, wound / 30), rand, stamps, knockdown ? KNOCKDOWN_STAIN : SPLASH_STAIN); } - if ( - this.bloodLevel === "full" - && !reducedMotion - && event.blood >= 40 - && ["hit", "counter_hit", "knockdown"].includes(event.kind) - ) { - const chunks = Math.min(12, Math.max(4, Math.round(event.blood / 9))); + // A punch that rocks him tears flesh from an open cut, and a big counter tears it from any face. + const rocking = event.amount >= HARD_SHOT || (event.kind === "counter_hit" && event.amount >= ROCKING_COUNTER); + const tearing = landed && ((rocking && wound >= OPEN_WOUND) || (event.kind === "counter_hit" && event.amount >= BIG_SHOT)); + if (this.bloodLevel === "full" && !reducedMotion && tearing && !event.detail.endsWith(":body")) { + const chunks = Math.min(8, Math.max(2, Math.round((event.amount - 30) / 10))); for (let index = 0; index < chunks; index += 1) { const angle = rand() * Math.PI * 2; const speed = 0.5 + rand() * 1.8; @@ -696,26 +1283,62 @@ export class Effects3D { } private applyBakedPart(part: SeveredHead, baked: BakedPart | undefined, fallbackColor: number): void { - if (part.baked !== null) { - part.baked.dispose(); - part.baked = null; - } + part.baked?.dispose(); + part.bakedFlesh?.dispose(); + part.baked = null; + part.bakedFlesh = null; + part.cap.geometry = part.defaultCap; + part.cap.quaternion.identity(); const material = part.mesh.material as THREE.MeshStandardMaterial; + part.sideLow = -part.radius; + part.sideHigh = part.radius; if (baked !== undefined) { part.mesh.geometry = baked.geometry; part.baked = baked.geometry; part.mesh.scale.setScalar(1); + const position = baked.geometry.getAttribute("position"); + const used = baked.geometry.getIndex(); + if (used !== null && used.count > 0) { + part.sideLow = Infinity; + part.sideHigh = -Infinity; + for (let at = 0; at < used.count; at += 1) { + const x = position.getX(used.getX(at)); + part.sideLow = Math.min(part.sideLow, x); + part.sideHigh = Math.max(part.sideHigh, x); + } + } material.map = baked.map; + part.look?.set(baked.look ?? SCANNED_LOOK); + if (part.injury !== null) { + // A severed head keeps the face it was beaten into, and the sheen of its sweat. + if (baked.injury === undefined) part.injury.clear(); + else part.injury.copyFrom(baked.injury); + const live = baked.injury?.material; + if (live instanceof THREE.MeshPhysicalMaterial && material instanceof THREE.MeshPhysicalMaterial) material.clearcoat = live.clearcoat; + part.injury.setBindToPart(bindToPart(baked.geometry, baked.look ?? SCANNED_LOOK, this.bindToPartMatrix)); + } + if (part.blood !== null) part.blood.value = baked.gloveBlood ?? 0; material.color.setHex(baked.color); - baked.geometry.computeBoundingBox(); - const box = baked.geometry.boundingBox; - if (box !== null) part.cap.position.y = box.min.y + 0.004; + if (baked.cut !== undefined) { + part.cap.geometry = baked.cut.flesh; + part.bakedFlesh = baked.cut.flesh; + part.cap.position.copy(baked.cut.position); + } else { + // A glove is a mesh of its own, open at the wrist: the wound closes its lowest point. + baked.geometry.computeBoundingBox(); + const box = baked.geometry.boundingBox; + part.cap.position.set(0, (box?.min.y ?? 0) + 0.004, 0); + part.cap.rotation.x = Math.PI; + } part.cap.visible = true; } else { part.mesh.geometry = part.defaultGeometry; part.mesh.scale.copy(part.defaultScale); material.map = null; material.color.setHex(fallbackColor); + part.injury?.clear(); + part.injury?.setBindToPart(this.bindToPartMatrix.identity()); + if (part.blood !== null) part.blood.value = 0; part.cap.visible = false; } material.needsUpdate = true; @@ -753,13 +1376,15 @@ export class Effects3D { head.vrz = (rand() - 0.5) * 12; head.bounces = 0; head.stained = false; + head.settle = -1; head.mesh.position.set(finite(position.x), finite(position.y, 1.55), finite(position.z)); copyFiniteQuaternion(head.mesh.quaternion, quaternion); head.mesh.visible = true; const stump = this.stumps[index]!; + stump.mesh.material = this.stumpMaterial; stump.active = true; - stump.fountainLife = 1.25; + stump.fountainLife = NECK_FOUNTAIN_SECONDS; stump.accumulator = 0; stump.seed = safeEventId | 1; stump.direction = launch; @@ -779,6 +1404,8 @@ export class Effects3D { 0.8 + rand() * 2.4, Math.cos(angle) * speed, rand, + "flesh", + index, ); } for (let i = 0; i < 120; i += 1) { @@ -793,7 +1420,7 @@ export class Effects3D { 1.2 + rand() * 2.2 + Math.sin(angle) * speed * 0.35, 0.7 + rand() * 2.7, Math.cos(angle) * speed, - shade < 0.5 ? { r: 0.64, g: 0.035, b: 0.055 } : { r: 0.36, g: 0.015, b: 0.03 }, + bloodShade(shade), 0.75 + rand() * 1.1, true, ); @@ -801,7 +1428,78 @@ export class Effects3D { for (let i = 0; i < 14; i += 1) { this.spawnMist(head.mesh.position.x + (rand() - 0.5) * 0.35, head.mesh.position.y + (rand() - 0.5) * 0.25, head.mesh.position.z + (rand() - 0.5) * 0.35, 1 + rand() * 1.4, 0.65 + rand() * 0.55); } - this.splatter(head.mesh.position.x, head.mesh.position.z, 1.5, rand); + this.splatter(head.mesh.position.x, head.mesh.position.z, 1.5, rand, 18); + } + + /** + * The head bursts above the mouth: pieces of skull and brain, teeth and a great spray of blood go out + * from the middle of the skull at `position`, and the lower jaw is left on the neck. + */ + burstHead(fighterIndex: number, position: THREE.Vector3, direction: number | SprayDirection, eventId: number): void { + if (this.bloodLevel !== "full" || fighterIndex < 0 || fighterIndex >= MAX_HEADS) return; + const index = Math.trunc(fighterIndex); + const safeEventId = Number.isSafeInteger(eventId) ? eventId : 0; + const stump = this.stumps[index]!; + if (stump.active || this.lastBurstEvent[index] === safeEventId) return; + this.lastBurstEvent[index] = safeEventId; + const rand = seeded(safeEventId * 91_813 + index * 6151 + 41); + const launch = unitSpray(direction); + const x = finite(position.x); + const y = finite(position.y, 1.6); + const z = finite(position.z); + const scatter = (count: number, kind: DebrisKind, speed: number, spread: number, lift: number): void => { + for (let piece = 0; piece < count; piece += 1) { + const around = rand() * Math.PI * 2; + const up = rand() * 1.2 - 0.2; + const flat = Math.sqrt(Math.max(0, 1 - up * up)); + const pace = speed + rand() * spread; + this.spawnGib( + x + Math.cos(around) * flat * 0.06, + y + up * 0.05 - (kind === "tooth" ? 0.07 : 0), + z + Math.sin(around) * flat * 0.06, + Math.cos(around) * flat * pace + launch.x * pace * 0.45, + up * pace + lift, + Math.sin(around) * flat * pace + launch.z * pace * 0.45, + rand, + kind, + index, + ); + } + }; + scatter(SHARDS_PER_BURST, "shard", 2.2, 2.4, 1); + scatter(BRAIN_PER_BURST, "brain", 1.2, 1.8, 0.8); + scatter(TEETH_PER_BURST, "tooth", 1, 1.4, 0.6); + for (let drop = 0; drop < 180; drop += 1) { + const around = rand() * Math.PI * 2; + const up = rand() * 1.3 - 0.3; + const flat = Math.sqrt(Math.max(0, 1 - Math.min(1, up * up))); + const pace = 1.4 + rand() * 3.2; + this.spawnDroplet( + x + (rand() - 0.5) * 0.1, + y + (rand() - 0.5) * 0.1, + z + (rand() - 0.5) * 0.1, + Math.cos(around) * flat * pace + launch.x * pace * 0.35, + up * pace + 0.8, + Math.sin(around) * flat * pace + launch.z * pace * 0.35, + bloodShade(rand()), + 0.8 + rand() * 1.1, + true, + 0.004 + rand() ** 2 * 0.014, + ); + } + for (let puff = 0; puff < 24; puff += 1) { + this.spawnMist(x + (rand() - 0.5) * 0.5, y + (rand() - 0.3) * 0.4, z + (rand() - 0.5) * 0.5, 1.2 + rand() * 1.6, 0.8 + rand() * 0.7); + } + this.splatter(x + launch.x * 0.3, z + launch.z * 0.3, 2.2, rand, 18); + this.shake = Math.min(0.16, this.shake + 0.1); + stump.mesh.material = this.jawMaterial; + stump.active = true; + stump.fountainLife = BURST_FOUNTAIN_SECONDS; + stump.accumulator = 0; + stump.seed = safeEventId | 1; + stump.direction = launch; + stump.mesh.position.set(x, y - 0.08, z); + stump.mesh.visible = true; } dismemberHand( @@ -842,7 +1540,7 @@ export class Effects3D { const stump = this.handStumps[index]!; stump.active = true; - stump.fountainLife = 0.9; + stump.fountainLife = WRIST_FOUNTAIN_SECONDS; stump.accumulator = 0; stump.seed = safeEventId | 1; stump.direction = launch; @@ -862,6 +1560,8 @@ export class Effects3D { 0.5 + rand() * 1.9, Math.cos(angle) * speed, rand, + "flesh", + Math.trunc(fighterIndex), ); } for (let drop = 0; drop < 80; drop += 1) { @@ -875,7 +1575,7 @@ export class Effects3D { 0.8 + rand() * 1.7 + Math.sin(angle) * speed * 0.35, 0.5 + rand() * 2.1, Math.cos(angle) * speed, - { r: 0.56, g: 0.025, b: 0.045 }, + bloodShade(rand()), 0.7 + rand() * 0.9, true, ); @@ -906,12 +1606,23 @@ export class Effects3D { copyFiniteQuaternion(stump.mesh.quaternion, quaternion); } - anchorStump(fighterIndex: number, position: THREE.Vector3, quaternion: THREE.Quaternion): void { + /** + * Keeps the wound on the neck. `rim` is the edge of the cut as the skin is posed, both ends of each + * edge in turn: with it the wound closes the opening exactly, and without it a disc stands in. + */ + anchorStump(fighterIndex: number, position: THREE.Vector3, quaternion: THREE.Quaternion, rim?: ArrayLike, across?: THREE.Vector3): void { if (this.bloodLevel !== "full" || fighterIndex < 0 || fighterIndex >= MAX_HEADS) return; const stump = this.stumps[Math.trunc(fighterIndex)]!; if (!stump.active) return; stump.mesh.position.set(finite(position.x), finite(position.y, 1.5), finite(position.z)); - copyFiniteQuaternion(stump.mesh.quaternion, quaternion); + if (rim === undefined || rim.length < 18) { + stump.mesh.geometry = this.stumpGeometry; + copyFiniteQuaternion(stump.mesh.quaternion, quaternion); + return; + } + closeCut(stump.flesh, rim, stump.mesh.position, this.stumpOutward.set(0, 1, 0).applyQuaternion(quaternion), 0.01, across); + stump.mesh.geometry = stump.flesh; + stump.mesh.quaternion.identity(); } restoreFighter(fighterIndex: number): void { @@ -929,6 +1640,14 @@ export class Effects3D { stump.accumulator = 0; stump.mesh.visible = false; stump.mesh.position.y = -50; + stump.mesh.material = this.stumpMaterial; + // What his finisher threw goes with it, to be thrown again if a replay brings the finisher back. + this.clearGibs(index); + const eye = this.eyes[index]!; + eye.active = false; + eye.bleeding = 0; + this.eyeMeshes[index]!.visible = false; + this.nerveMeshes[index]!.visible = false; for (const handIndex of [index * 2, index * 2 + 1]) { const hand = this.hands[handIndex]!; hand.active = false; @@ -977,7 +1696,7 @@ export class Effects3D { (this.ambientRandom() - 0.5) * 0.08, -0.25 - this.ambientRandom() * 0.4, (this.ambientRandom() - 0.5) * 0.08, - { r: 0.5, g: 0.04, b: 0.06 }, + bloodShade(this.ambientRandom()), 1.1, true, ); @@ -985,7 +1704,7 @@ export class Effects3D { } } - /** Ejects teeth from the mouth on heavy head contact. */ + /** Ejects teeth from the mouth on heavy head contact, in a spray of blood that reduced blood cuts to a few drops. */ spawnTeeth(mouthWorld: THREE.Vector3, direction: number | SprayDirection, count: number, eventId: number): void { if (this.bloodLevel === "off") return; const rand = seeded((Number.isSafeInteger(eventId) ? eventId : 0) * 7331 + 91); @@ -1002,23 +1721,37 @@ export class Effects3D { 1.1 + rand() * 1.4, Math.cos(angle) * 0.7, rand, - true, + "tooth", ); } - for (let i = 0; i < 18; i += 1) { + for (let i = 0; i < (this.bloodLevel === "reduced" ? 4 : 18); i += 1) { const angle = rand() * Math.PI * 2; - this.sprayDroplet(launch, finite(mouthWorld.x), finite(mouthWorld.y, 1.4), finite(mouthWorld.z), 0.8 + rand() * 1.4 + Math.sin(angle) * 0.5, 0.6 + rand() * 1.3, Math.cos(angle) * 0.5, { r: 0.55, g: 0.02, b: 0.04 }, 0.6 + rand() * 0.6, true); + this.sprayDroplet(launch, finite(mouthWorld.x), finite(mouthWorld.y, 1.4), finite(mouthWorld.z), 0.8 + rand() * 1.4 + Math.sin(angle) * 0.5, 0.6 + rand() * 1.3, Math.cos(angle) * 0.5, bloodShade(rand()), 0.6 + rand() * 0.6, true); } } - private spawnGib(x: number, y: number, z: number, vx: number, vy: number, vz: number, rand: () => number, tooth = false): void { - const index = this.gibIndex % MAX_GIBS; - this.gibIndex += 1; - const gib = this.gibs[index]!; + private spawnGib(x: number, y: number, z: number, vx: number, vy: number, vz: number, rand: () => number, kind: DebrisKind = "flesh", owner = -1): void { + const shard = kind === "shard"; + const pool = shard ? this.shards : this.gibs; + const mesh = shard ? this.shardMesh : this.gibMesh; + const index = this.debrisSlot(pool, shard); + const gib = pool[index]!; gib.alive = true; - gib.tooth = tooth; - this.gibMesh.setColorAt(index, this.gibColor.setHex(tooth ? 0xf3ead6 : 0xffffff)); - if (this.gibMesh.instanceColor !== null) this.gibMesh.instanceColor.needsUpdate = true; + gib.kind = kind; + gib.owner = owner; + gib.born = this.debrisThrown++; + gib.resting = false; + gib.fading = -1; + const shade = 0.55 + rand() * 0.45; + if (kind === "tooth") this.gibColor.setHex(0xf3ead6); + else if (kind === "brain") this.gibColor.setHex(rand() < 0.3 ? 0x8a3a3c : 0xb7918e).multiplyScalar(0.8 + shade * 0.25); + else if (shard) this.gibColor.setScalar(0.85 + shade * 0.15); + else { + this.gibColor.setHex(0x5c0a10).multiplyScalar(shade); + this.gibColor.g *= 0.75 + rand() * 0.25; + } + mesh.setColorAt(index, this.gibColor); + if (mesh.instanceColor !== null) mesh.instanceColor.needsUpdate = true; gib.x = finite(x); gib.y = finite(y, 1.5); gib.z = finite(z); @@ -1031,20 +1764,49 @@ export class Effects3D { gib.vrx = (rand() - 0.5) * 18; gib.vry = (rand() - 0.5) * 18; gib.vrz = (rand() - 0.5) * 18; - gib.life = tooth ? 6 : 1.8 + rand() * 1.2; - gib.scale = tooth ? 0.22 + rand() * 0.12 : 0.32 + rand() * 0.7; + gib.scale = kind === "tooth" ? 0.22 + rand() * 0.12 : shard ? 0.6 + rand() * 0.6 : kind === "brain" ? 0.45 + rand() * 0.55 : 0.3 + rand() * 0.6; + gib.stretch = kind === "tooth" ? 1 : 0.7 + rand() * 0.9; gib.bounces = 0; gib.stained = false; - this.writeGibMatrix(index, gib); - this.gibMesh.instanceMatrix.needsUpdate = true; + this.writeDebrisMatrix(mesh, index, gib); + mesh.instanceMatrix.needsUpdate = true; + this.makeDebrisRoom(pool); } - /** Spawns a gib whose horizontal velocity is given along the spray and across it. */ - private sprayGib(spray: SprayDirection, x: number, y: number, z: number, along: number, vy: number, across: number, rand: () => number, tooth = false): void { - this.spawnGib(x, y, z, along * spray.x - across * spray.z, vy, along * spray.z + across * spray.x, rand, tooth); + /** A free place in a pool, looking from the next in turn; with none free, the oldest piece's. */ + private debrisSlot(pool: readonly Gib[], shard: boolean): number { + const start = shard ? this.shardIndex : this.gibIndex; + let slot = start % pool.length; + for (let step = 0; step < pool.length; step += 1) { + const index = (start + step) % pool.length; + if (!pool[index]!.alive) { + slot = index; + break; + } + if (pool[index]!.born < pool[slot]!.born) slot = index; + } + if (shard) this.shardIndex = slot + 1; + else this.gibIndex = slot + 1; + return slot; } - private writeGibMatrix(index: number, gib: Gib): void { + /** Keeps an eighth of a pool free: once it is nearly full, the oldest piece lying on the canvas shrinks away. */ + private makeDebrisRoom(pool: readonly Gib[]): void { + let free = 0; + let oldest: Gib | null = null; + for (const gib of pool) { + if (!gib.alive) free += 1; + else if (gib.resting && gib.fading < 0 && (oldest === null || gib.born < oldest.born)) oldest = gib; + } + if (free < Math.max(3, pool.length >> 3) && oldest !== null) oldest.fading = DEBRIS_FADE_SECONDS; + } + + /** Spawns a piece of debris whose horizontal velocity is given along the spray and across it. */ + private sprayGib(spray: SprayDirection, x: number, y: number, z: number, along: number, vy: number, across: number, rand: () => number, kind: DebrisKind = "flesh", owner = -1): void { + this.spawnGib(x, y, z, along * spray.x - across * spray.z, vy, along * spray.z + across * spray.x, rand, kind, owner); + } + + private writeDebrisMatrix(mesh: THREE.InstancedMesh, index: number, gib: Gib): void { if (!gib.alive) { this.gibPosition.set(0, -50, 0); this.gibQuaternion.identity(); @@ -1053,21 +1815,23 @@ export class Effects3D { this.gibPosition.set(gib.x, gib.y, gib.z); this.gibEuler.set(gib.rx, gib.ry, gib.rz); this.gibQuaternion.setFromEuler(this.gibEuler); - this.gibScale.setScalar(gib.scale); + const size = gib.fading >= 0 ? (gib.scale * Math.max(0, gib.fading)) / DEBRIS_FADE_SECONDS : gib.scale; + this.gibScale.set(size * gib.stretch, size, size / Math.sqrt(gib.stretch)); } this.gibMatrix.compose(this.gibPosition, this.gibQuaternion, this.gibScale); - this.gibMesh.setMatrixAt(index, this.gibMatrix); + mesh.setMatrixAt(index, this.gibMatrix); } - private updateGibs(dt: number): void { + private updateDebris(pool: readonly Gib[], mesh: THREE.InstancedMesh, dt: number): void { let changed = false; - for (const [index, gib] of this.gibs.entries()) { - if (!gib.alive) continue; + for (const [index, gib] of pool.entries()) { + // A piece lying still costs nothing until it is made to give way. + if (!gib.alive || (gib.resting && gib.fading < 0)) continue; changed = true; - gib.life -= dt; - if (gib.life <= 0) { - gib.alive = false; - this.writeGibMatrix(index, gib); + if (gib.fading >= 0) { + gib.fading -= dt; + if (gib.fading <= 0) gib.alive = false; + this.writeDebrisMatrix(mesh, index, gib); continue; } gib.vy -= 6.4 * dt; @@ -1080,11 +1844,11 @@ export class Effects3D { this.confineGib(gib); if (gib.y <= CANVAS_TOP + 0.015) { gib.y = CANVAS_TOP + 0.015; - if (!gib.stained && this.bloodLevel === "full" && !gib.tooth) { + if (!gib.stained && this.bloodLevel === "full" && gib.kind !== "tooth") { gib.stained = true; - this.placeDecal(gib.x, gib.z, 0.24 + gib.scale * 0.16, 0.14 + gib.scale * 0.1, gib.rz, 0.42, 0x620a10); + this.placeDecal(gib.x, gib.z, 0.24 + gib.scale * 0.16, 0.14 + gib.scale * 0.1, gib.rz, 0.55, 0x620a10); } - if (gib.bounces < (gib.tooth ? 3 : 1) && Math.abs(gib.vy) > 0.35) { + if (gib.bounces < DEBRIS_BOUNCES[gib.kind] && Math.abs(gib.vy) > 0.35) { gib.bounces += 1; gib.vy = Math.abs(gib.vy) * 0.3; gib.vx *= 0.68; @@ -1092,18 +1856,19 @@ export class Effects3D { gib.vrx *= 0.7; gib.vry *= 0.7; gib.vrz *= 0.7; - } else if (gib.tooth) { + } else { + // It lies where it came down for the rest of the bout; a piece of skull settles on its back or its face. gib.vx = 0; gib.vy = 0; gib.vz = 0; gib.vrx = gib.vry = gib.vrz = 0; - } else { - gib.alive = false; + if (gib.kind === "shard") gib.rx = Math.round(gib.rx / Math.PI) * Math.PI; + gib.resting = true; } } - this.writeGibMatrix(index, gib); + this.writeDebrisMatrix(mesh, index, gib); } - if (changed) this.gibMesh.instanceMatrix.needsUpdate = true; + if (changed) mesh.instanceMatrix.needsUpdate = true; } private confineGib(gib: Gib): void { @@ -1119,7 +1884,15 @@ export class Effects3D { private updateDetachedParts(parts: readonly SeveredHead[], dt: number): void { for (const part of parts) { + if (part.active && part.settle >= 0 && part.settle < 1) { + part.settle = Math.min(1, part.settle + dt / HEAD_SETTLE_SECONDS); + const eased = part.settle * part.settle * (3 - 2 * part.settle); + part.mesh.quaternion.slerpQuaternions(part.settleFrom, part.settleTo, eased); + part.mesh.position.y = part.settleFromY + (part.settleToY - part.settleFromY) * eased; + } if (!part.active || !part.moving) continue; + const fromX = part.mesh.position.x; + const fromZ = part.mesh.position.z; part.vy -= 5.8 * dt; part.mesh.position.x += part.vx * dt; part.mesh.position.y += part.vy * dt; @@ -1127,20 +1900,20 @@ export class Effects3D { part.mesh.rotation.x += part.vrx * dt; part.mesh.rotation.y += part.vry * dt; part.mesh.rotation.z += part.vrz * dt; - if (Math.abs(part.mesh.position.x) > RING_FIGHT_HALF) { - part.mesh.position.x = Math.sign(part.mesh.position.x) * RING_FIGHT_HALF; - part.vx *= -0.42; - } - if (Math.abs(part.mesh.position.z) > RING_FIGHT_HALF) { - part.mesh.position.z = Math.sign(part.mesh.position.z) * RING_FIGHT_HALF; - part.vz *= -0.42; - } + // The ropes keep a severed part in the ring, where the cameras can reach it. + const limit = ROPE_LINE - part.radius - SEVERED_PART_MARGIN; + const x = confineToRopes(part.mesh.position.x, part.vx, fromX, limit); + const z = confineToRopes(part.mesh.position.z, part.vz, fromZ, limit); + part.mesh.position.x = x.position; + part.mesh.position.z = z.position; + part.vx = x.velocity; + part.vz = z.velocity; if (part.mesh.position.y <= CANVAS_TOP + part.radius) { part.mesh.position.y = CANVAS_TOP + part.radius; if (!part.stained && this.bloodLevel === "full") { part.stained = true; const stainScale = part.radius === HEAD_RADIUS ? 1 : 0.55; - this.placeDecal(part.mesh.position.x, part.mesh.position.z, 1.3 * stainScale, 0.9 * stainScale, part.mesh.rotation.z, 0.58, 0x430507); + this.placeDecal(part.mesh.position.x, part.mesh.position.z, 1.3 * stainScale, 0.9 * stainScale, part.mesh.rotation.z, 0.85, 0x430507); } if (part.bounces < 2 && Math.abs(part.vy) > 0.42) { part.bounces += 1; @@ -1158,6 +1931,7 @@ export class Effects3D { part.vrx = 0; part.vry = 0; part.vrz = 0; + if (part.look !== null) this.rollOntoEar(part); } } if (!Number.isFinite(part.mesh.position.x) || !Number.isFinite(part.mesh.position.y) || !Number.isFinite(part.mesh.position.z) || !Number.isFinite(part.mesh.quaternion.x) || !Number.isFinite(part.mesh.quaternion.y) || !Number.isFinite(part.mesh.quaternion.z) || !Number.isFinite(part.mesh.quaternion.w)) { @@ -1171,6 +1945,20 @@ export class Effects3D { } } + /** + * A head left on the canvas rolls the short way onto an ear, as a head does: its face turns to the + * side, where a camera can find it, and it lies on the canvas rather than on the ball it bounced as. + */ + private rollOntoEar(part: SeveredHead): void { + const side = this.settleAxis.set(1, 0, 0).applyQuaternion(part.mesh.quaternion); + const up = side.y >= 0 ? 1 : -1; + part.settleFrom.copy(part.mesh.quaternion); + part.settleTo.setFromUnitVectors(side, this.settleUp.set(0, up, 0)).multiply(part.mesh.quaternion); + part.settleFromY = part.mesh.position.y; + part.settleToY = CANVAS_TOP + (up > 0 ? -part.sideLow : part.sideHigh); + part.settle = 0; + } + private stumpRandom(stump: Stump): number { let seed = stump.seed | 0; seed = Math.imul(seed ^ (seed >>> 15), 1 | seed); @@ -1179,34 +1967,54 @@ export class Effects3D { return ((seed ^ (seed >>> 14)) >>> 0) / 4294967296; } + /** + * The severed artery pumps with the heart: a spurt with every beat, thrown at arterial speed along the way + * the wound faces (up from a standing neck, across the canvas from one lying there), weakening as he bleeds + * out over the fountain's last FOUNTAIN_FADE_SECONDS rather than stopping dead. + */ private updateStumpFountains(dt: number): void { if (this.bloodLevel !== "full") return; for (const stump of [...this.stumps, ...this.handStumps]) { if (!stump.active || stump.fountainLife <= 0) continue; stump.fountainLife = Math.max(0, stump.fountainLife - dt); - stump.accumulator += dt * 36; + const pressure = Math.min(1, stump.fountainLife / FOUNTAIN_FADE_SECONDS); + const beat = (stump.fountainLife * HEART_RATE) % 1; + const spurt = Math.exp(-(((beat - 0.5) / SPURT_WIDTH) ** 2)); + stump.accumulator += dt * FOUNTAIN_PEAK_RATE * pressure * (FOUNTAIN_BETWEEN_BEATS + (1 - FOUNTAIN_BETWEEN_BEATS) * spurt); + if (stump.accumulator < 1) continue; + const axis = stumpAxis(stump, fountainAxis); + fountainSide.set(Math.abs(axis.y) < 0.9 ? 0 : 1, Math.abs(axis.y) < 0.9 ? 1 : 0, 0).cross(axis).normalize(); + fountainUp.crossVectors(axis, fountainSide); while (stump.accumulator >= 1) { stump.accumulator -= 1; const first = this.stumpRandom(stump); const second = this.stumpRandom(stump); const third = this.stumpRandom(stump); - this.sprayDroplet( - stump.direction, - stump.mesh.position.x + (first - 0.5) * 0.08, - stump.mesh.position.y + 0.015, - stump.mesh.position.z + (second - 0.5) * 0.08, - 0.35 + first * 0.55, - 1.3 + second * 1.45, - (third - 0.5) * 0.85, - { r: 0.58, g: 0.025, b: 0.045 }, - 0.65 + third * 0.5, + // Within a cone round the wound's axis, faster at the top of a beat and as the pressure holds. + const tilt = Math.sqrt(first) * FOUNTAIN_CONE; + const around = second * Math.PI * 2; + fountainVelocity.copy(axis).multiplyScalar(Math.cos(tilt)) + .addScaledVector(fountainSide, Math.sin(tilt) * Math.cos(around)) + .addScaledVector(fountainUp, Math.sin(tilt) * Math.sin(around)) + .multiplyScalar(THREE.MathUtils.lerp(FOUNTAIN_SPEED_LOW, FOUNTAIN_SPEED_HIGH, third) * (0.7 + 0.3 * spurt) * (0.55 + 0.45 * pressure)); + this.spawnDroplet( + stump.mesh.position.x + axis.x * 0.015 + (first - 0.5) * 0.03, + stump.mesh.position.y + axis.y * 0.015, + stump.mesh.position.z + axis.z * 0.015 + (second - 0.5) * 0.03, + fountainVelocity.x, + fountainVelocity.y, + fountainVelocity.z, + bloodShade(first), + 1.2 + third * 0.6, true, + 0.004 + third * third * 0.01, ); } } } update(dt: number): void { + this.canvasBlood.update(safeStep(dt)); this.simulationRemainder += safeStep(dt); while (this.simulationRemainder + 1e-9 >= SIMULATION_STEP) { this.simulationRemainder = Math.max(0, this.simulationRemainder - SIMULATION_STEP); @@ -1220,7 +2028,10 @@ export class Effects3D { this.updateStumpFountains(step); this.updateDetachedParts(this.heads, step); this.updateDetachedParts(this.hands, step); - this.updateGibs(step); + this.updateShields(step); + this.updateEyes(step); + this.updateDebris(this.gibs, this.gibMesh, step); + this.updateDebris(this.shards, this.shardMesh, step); const live = this.liveDroplets; let retired = false; @@ -1228,16 +2039,19 @@ export class Effects3D { const droplet = this.droplets[i]!; droplet.life -= step; if (droplet.life <= 0 || droplet.y < CANVAS_TOP) { - if (droplet.y < CANVAS_TOP && droplet.blood && this.bloodLevel !== "off" && this.ambientRandom() < (this.bloodLevel === "full" ? 0.48 : 0.18)) { - const scale = this.bloodLevel === "full" ? 1 : 0.35; - this.placeDecal(droplet.x, droplet.z, (0.22 + this.ambientRandom() * 0.38) * scale, (0.14 + this.ambientRandom() * 0.24) * scale, this.ambientRandom() * Math.PI, 0.38 * scale, 0x6e0d13); + // A drop leaves a spot the size of the drop: fine spray soaks in unseen, a gob of blood splashes. + const lands = THREE.MathUtils.clamp(droplet.radius / GOB, 0.25, 1) * (this.bloodLevel === "full" ? 1 : 0.4); + if (droplet.y < CANVAS_TOP && droplet.blood && this.bloodLevel !== "off" && this.ambientRandom() < lands) { + const scale = this.bloodLevel === "full" ? 1 : 0.6; + const size = droplet.radius * (40 + this.ambientRandom() * 20) * scale; + this.placeDecal(droplet.x, droplet.z, size, size * (0.55 + this.ambientRandom() * 0.35), this.ambientRandom() * Math.PI, this.bloodLevel === "full" ? DROP_STAIN : DROP_STAIN * 0.6, 0x6e0d13); } // The last live droplet takes this slot and is stepped next. this.retireDroplet(i); retired = true; continue; } - droplet.vy -= 4.6 * step; + droplet.vy -= DROPLET_GRAVITY * step; droplet.x += droplet.vx * step; droplet.y += droplet.vy * step; droplet.z += droplet.vz * step; @@ -1268,13 +2082,10 @@ export class Effects3D { continue; } const progress = 1 - mist.life / mist.maxLife; - const fade = Math.max(0, 1 - progress) * 0.7 * mist.scale; this.mistPositions[i * 3] = mist.x; this.mistPositions[i * 3 + 1] = mist.y + progress * 0.14; this.mistPositions[i * 3 + 2] = mist.z; - this.mistColors[i * 3] = fade; - this.mistColors[i * 3 + 1] = fade * 0.1; - this.mistColors[i * 3 + 2] = fade * 0.12; + this.mistColors[i * 4 + 3] = Math.max(0, 1 - progress) * Math.min(1, mist.scale); } if (mistChanged) { this.mistGeometry.attributes.position!.needsUpdate = true; @@ -1308,21 +2119,25 @@ export class Effects3D { } } - private clearGibs(): void { - let changed = false; - for (const [index, gib] of this.gibs.entries()) { - if (!gib.alive) continue; - gib.alive = false; - this.writeGibMatrix(index, gib); - changed = true; + /** Clears the debris on the canvas, or only what `owner`'s finisher threw. */ + private clearGibs(owner?: number): void { + for (const [pool, mesh] of [[this.gibs, this.gibMesh], [this.shards, this.shardMesh]] as const) { + let changed = false; + for (const [index, gib] of pool.entries()) { + if (!gib.alive || (owner !== undefined && gib.owner !== owner)) continue; + gib.alive = false; + this.writeDebrisMatrix(mesh, index, gib); + changed = true; + } + if (changed) mesh.instanceMatrix.needsUpdate = true; } - if (changed) this.gibMesh.instanceMatrix.needsUpdate = true; } clearDynamic(): void { this.clearDroplets(false); this.clearMists(); this.clearGibs(); + this.clearMouthpieces(); for (let index = 0; index < MAX_HEADS; index += 1) this.restoreFighter(index); this.shake = 0; this.stopDrip(0); @@ -1343,42 +2158,60 @@ export class Effects3D { } clearDecals(): void { - for (const decal of this.decals) decal.visible = false; - this.decalIndex = 0; + this.canvasBlood.clear(); } dispose(): void { this.scene.remove(this.dropletMesh); this.scene.remove(this.mistPoints); this.scene.remove(this.gibMesh); + this.scene.remove(this.shardMesh); this.dropletMesh.dispose(); this.dropletGeometry.dispose(); this.dropletMaterial.dispose(); - for (const map of this.splatMaps) map.dispose(); + this.canvasBlood.dispose(); for (const part of [...this.heads, ...this.hands]) { part.baked?.dispose(); - part.cap.geometry.dispose(); + part.bakedFlesh?.dispose(); } + for (const stump of [...this.stumps, ...this.handStumps]) stump.flesh.dispose(); this.mistGeometry.dispose(); this.mistMaterial.dispose(); this.mistMap.dispose(); this.gibMesh.dispose(); this.gibGeometry.dispose(); this.gibMaterial.dispose(); + this.shardMesh.dispose(); + this.shardGeometry.dispose(); + this.shardMaterial.dispose(); + this.jawMaterial.dispose(); + this.jawMap.dispose(); this.headGeometry.dispose(); for (const material of this.headMaterials) material.dispose(); this.handGeometry.dispose(); for (const material of this.handMaterials) material.dispose(); this.stumpGeometry.dispose(); this.wristStumpGeometry.dispose(); + this.shieldGeometry.dispose(); + for (const material of this.shieldMaterials) material.dispose(); + for (const mesh of this.shieldMeshes) this.scene.remove(mesh); + this.eyeGeometry.dispose(); + this.eyeMap.dispose(); + this.eyeMaterial.dispose(); + this.nerveGeometry.dispose(); + this.nerveMaterial.dispose(); + for (const mesh of [...this.eyeMeshes, ...this.nerveMeshes]) this.scene.remove(mesh); this.stumpMaterial.dispose(); + this.stumpMap.dispose(); + this.wristMaterial.dispose(); + this.wristMap.dispose(); + if (this.bakedStandIns !== null) { + this.bakedStandIns.map.dispose(); + for (const material of this.bakedStandIns.materials) material.dispose(); + this.bakedStandIns = null; + } for (const head of this.heads) this.scene.remove(head.mesh); for (const hand of this.hands) this.scene.remove(hand.mesh); for (const stump of [...this.stumps, ...this.handStumps]) this.scene.remove(stump.mesh); - this.decalGeometry.dispose(); - for (const decal of this.decals) { - this.scene.remove(decal); - (decal.material as THREE.Material).dispose(); - } } } diff --git a/web/hands/src/render/eye.test.ts b/web/hands/src/render/eye.test.ts new file mode 100644 index 00000000..fe6a3ef6 --- /dev/null +++ b/web/hands/src/render/eye.test.ts @@ -0,0 +1,154 @@ +import * as THREE from "three"; +import { describe, expect, it } from "vitest"; +import { fighter } from "../test/fixtures"; +import type { CombatEvent } from "../types"; +import { EYE_NERVE_LENGTH, Effects3D } from "./effects"; +import { SkinnedBoxer, loadBoxerGlb } from "./graph"; +import { BODY_SITES, EYE_LIDS, HEAD_SITES, InjuryShading } from "./injury"; +import { FightRenderer, arcadeInjuryFor, eyeSocket, replayReattaches } from "./renderer"; +import { CANVAS_TOP } from "./world"; + +const gltf = await loadBoxerGlb(); +const downed = { ...fighter("two"), is_downed: true }; +const ko = { finish_method: "ko" as const, winner_id: "one" }; +const hit = (detail: string, eventId: number): CombatEvent => ({ event_id: eventId, tick: 10, kind: "hit", actor_id: "one", target_id: "two", amount: 80, detail, blood: 20, direction: 1, action_id: null }); +const puncher = (hand: string) => ({ ...fighter("one"), action_key: `hook:${hand}:head:normal` }); + +describe("an eye forced out", () => { + it("is what a hard finishing hook earns, on the side it lands; other punches keep their finishers", () => { + expect(arcadeInjuryFor(hit("hook:head", 1), downed, ko, puncher("left"))).toBe("eye_right"); + expect(arcadeInjuryFor(hit("hook:head", 3), downed, ko, puncher("right"))).toBe("eye_left"); + expect(arcadeInjuryFor(hit("hook:head", 2), downed, ko, puncher("left"))).toBe("eye_right"); + expect(arcadeInjuryFor({ ...hit("hook:head", 2), amount: 50 }, downed, ko, puncher("left"))).toBe("jaw_dislocation"); + expect(arcadeInjuryFor(hit("straight:head", 1), downed, ko, puncher("left"))).toBe("decapitation"); + expect(["eye_left", "eye_right"]).toContain(arcadeInjuryFor(hit("hook:head", 5), downed, ko)); + expect(replayReattaches("eye_left")).toBe(true); + expect(replayReattaches("eye_right")).toBe(true); + }); + + it("leaves a hollow, bleeding socket where the face has an eye, and nothing on the body", () => { + const head = new InjuryShading(new THREE.MeshStandardMaterial(), HEAD_SITES, { core: [0, 119, -3.5, 0], lids: EYE_LIDS }); + head.setEyeOut("right"); + expect(head.eyeOut).toBe("right"); + expect(head.uniforms.uInjuryEyeOut.value.toArray()).toEqual([0, 1]); + expect(head.level("rightEye").blood).toBeGreaterThan(1); + expect(head.level("leftEye").blood).toBe(0); + head.setEyeOut(null); + expect(head.eyeOut).toBeNull(); + expect(head.level("rightEye").blood).toBe(0); + head.setEyeOut("left"); + head.clear(); + expect(head.eyeOut).toBeNull(); + const body = new InjuryShading(new THREE.MeshStandardMaterial(), BODY_SITES, { core: [0, 0, -2, 1] }); + body.setEyeOut("left"); + expect(body.eyeOut).toBeNull(); + const shader = { uniforms: {} as Record, vertexShader: "#include \n#include ", fragmentShader: "#include \n#include \n#include " }; + head.material!.onBeforeCompile(shader as unknown as THREE.WebGLProgramParametersWithUniforms, null as unknown as THREE.WebGLRenderer); + expect(shader.uniforms.uInjuryEyeOut).toBe(head.uniforms.uInjuryEyeOut); + expect(shader.vertexShader).toContain("uniform vec2 uInjuryEyeOut;"); + expect(shader.fragmentShader).toContain("uniform vec2 uInjuryEyeOut;"); + // `out` is a reserved word in GLSL ES 3.0 and would stop the skin compiling. + expect(shader.vertexShader).not.toMatch(/\bfloat out\b/); + expect(shader.fragmentShader).not.toMatch(/\bfloat out\b/); + }); + + it("sits in its socket on the face as it is posed, the left and the right on their own sides", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + boxer.root.rotation.y = 0.7; + const left = new THREE.Vector3(); + const right = new THREE.Vector3(); + const forward = new THREE.Vector3(); + const turn = new THREE.Quaternion(); + expect(eyeSocket(boxer, "left", left, forward, turn)).toBe(true); + expect(eyeSocket(boxer, "right", right, forward, turn)).toBe(true); + const head = boxer.bone("head")!.getWorldPosition(new THREE.Vector3()); + expect(left.distanceTo(head)).toBeLessThan(0.15); + expect(left.distanceTo(right)).toBeGreaterThan(0.05); + expect(left.distanceTo(right)).toBeLessThan(0.11); + const across = new THREE.Vector3(1, 0, 0).applyQuaternion(turn); + expect(left.clone().sub(right).dot(across)).toBeGreaterThan(0.04); + expect(forward.length()).toBeCloseTo(1, 6); + boxer.dispose(); + }); + + it("springs out and hangs on its nerve in front of the face, never through the canvas, and only with full blood", () => { + const socket = new THREE.Vector3(0, 0.3, 0); + const forward = new THREE.Vector3(1, 0, 0); + // The head lies on its side, face toward +X: the middle of the skull is ten centimetres behind the eye. + const skull = new THREE.Vector3(-0.1, 0.3, 0); + const turn = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), forward); + const effects = new Effects3D(new THREE.Scene(), 256); + effects.gougeEye(1, socket, forward, 1, 31); + expect(effects.eyeOut(1)).toBe(true); + const at = new THREE.Vector3(); + for (let frame = 0; frame < 240; frame += 1) { + effects.anchorEye(1, socket, forward, skull, turn); + effects.update(1 / 60); + expect(effects.eyePosition(1, at)).toBe(true); + expect(at.distanceTo(socket)).toBeLessThanOrEqual(EYE_NERVE_LENGTH + 1e-6); + expect(at.y).toBeGreaterThan(CANVAS_TOP); + // In front of the face plane through the socket. + expect(at.x).toBeGreaterThan(socket.x + 0.01); + } + // At rest it hangs below the socket. + expect(at.y).toBeLessThan(socket.y - 0.02); + effects.gougeEye(1, socket, forward, 1, 31); + effects.restoreFighter(1); + expect(effects.eyeOut(1)).toBe(false); + effects.dispose(); + const reduced = new Effects3D(new THREE.Scene(), 256); + reduced.setBloodLevel("reduced"); + reduced.gougeEye(0, socket, forward, 1, 32); + expect(reduced.eyeOut(0)).toBe(false); + reduced.dispose(); + }); + + it("is shot close in the finish, aimed at the eye from the side it hangs on", () => { + const effects = new Effects3D(new THREE.Scene(), 256); + const socket = new THREE.Vector3(0.6, 0.3, 0.2); + effects.gougeEye(1, socket, new THREE.Vector3(1, 0, 0), 1, 40); + effects.anchorEye(1, socket, new THREE.Vector3(1, 0, 0), new THREE.Vector3(0.5, 0.3, 0.2), new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), new THREE.Vector3(1, 0, 0))); + for (let frame = 0; frame < 60; frame += 1) effects.update(1 / 60); + const eye = new THREE.Vector3(); + effects.eyePosition(1, eye); + const stub = { + finishCloseUpUntil: 101.7, finishCloseUpIndex: 1, finishCloseUpBearing: null as number | null, headCacheValid: [true, true], + headCache: [new THREE.Vector3(), new THREE.Vector3(0.5, 0.35, 0.2)], arcadeInjuries: [null, "eye_right"], effects, + closeUpTarget: new THREE.Vector3(), closeUpFacing: new THREE.Vector3(), closeUpPosition: new THREE.Vector3(), replayLookAt: new THREE.Vector3(), + tmpA: new THREE.Vector3(-1.6, 0, -1.2), tmpB: new THREE.Vector3(-1.2, 0, 0.1), refereePosition: new THREE.Vector3(-0.6, 0, -1.9), + headWorldPose: () => ({ position: new THREE.Vector3(0.5, 0.3, 0.2), quaternion: new THREE.Quaternion() }), + eyeInSight: (FightRenderer.prototype as unknown as { eyeInSight: unknown }).eyeInSight, + }; + const shot = (FightRenderer.prototype as unknown as { closeUpFrame(this: unknown, seconds: number): { position: THREE.Vector3; lookAt: THREE.Vector3 } }).closeUpFrame.call(stub, 100.2); + expect(shot.lookAt.distanceTo(eye)).toBeLessThan(1e-6); + expect(Math.hypot(shot.position.x - eye.x, shot.position.z - eye.z)).toBeLessThan(0.5); + // The eye hangs on the +X side of the head, and the camera is there with it. + expect(shot.position.x).toBeGreaterThan(eye.x); + effects.dispose(); + }); + + it("shoots the head instead when the eye hangs under a face that lies on the canvas", () => { + const effects = new Effects3D(new THREE.Scene(), 256); + const socket = new THREE.Vector3(0.5, 0.16, 0.2); + const down = new THREE.Vector3(0, -1, 0); + effects.gougeEye(1, socket, down, 1, 40); + effects.anchorEye(1, socket, down, new THREE.Vector3(0.5, 0.26, 0.2), new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), down)); + for (let frame = 0; frame < 60; frame += 1) effects.update(1 / 60); + const eye = new THREE.Vector3(); + effects.eyePosition(1, eye); + const head = new THREE.Vector3(0.5, 0.3, 0.2); + const stub = { + finishCloseUpUntil: 101.7, finishCloseUpIndex: 1, finishCloseUpBearing: null as number | null, headCacheValid: [true, true], + headCache: [new THREE.Vector3(), head], arcadeInjuries: [null, "eye_right"], effects, graphs: null, + closeUpTarget: new THREE.Vector3(), closeUpFacing: new THREE.Vector3(), closeUpPosition: new THREE.Vector3(), replayLookAt: new THREE.Vector3(), + tmpA: new THREE.Vector3(-1.6, 0, -1.2), tmpB: new THREE.Vector3(-1.2, 0, 0.1), refereePosition: new THREE.Vector3(-0.6, 0, -1.9), + headWorldPose: () => ({ position: new THREE.Vector3(0.5, 0.26, 0.2), quaternion: new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), down) }), + eyeInSight: (FightRenderer.prototype as unknown as { eyeInSight: unknown }).eyeInSight, + }; + const shot = (FightRenderer.prototype as unknown as { closeUpFrame(this: unknown, seconds: number): { position: THREE.Vector3; lookAt: THREE.Vector3 } }).closeUpFrame.call(stub, 100.2); + expect(eye.y).toBeLessThan(0.2); + expect(shot.lookAt.distanceTo(head)).toBeLessThan(1e-6); + expect(shot.position.y).toBeGreaterThan(head.y + 0.6); + effects.dispose(); + }); +}); diff --git a/web/hands/src/render/gear.test.ts b/web/hands/src/render/gear.test.ts new file mode 100644 index 00000000..6341a41a --- /dev/null +++ b/web/hands/src/render/gear.test.ts @@ -0,0 +1,77 @@ +import * as THREE from "three"; +import { describe, expect, it } from "vitest"; +import { fighter as baseFighter } from "../test/fixtures"; +import type { FighterSnapshot } from "../types"; +import { wearCornerColour } from "./gear"; +import { BoxingGraph, SkinnedBoxer, loadBoxerGlb } from "./graph"; +import { worldMapping } from "./world"; + +const gltf = await loadBoxerGlb(); +const mapping = worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }); + +const SHADER = { + vertexShader: "#include \n#include ", + fragmentShader: "#include \n#include \n#include ", +}; + +function compile(material: THREE.Material): { uniforms: Record; vertexShader: string; fragmentShader: string } { + const shader = { uniforms: {} as Record, ...SHADER }; + material.onBeforeCompile(shader as unknown as THREE.WebGLProgramParametersWithUniforms, null as unknown as THREE.WebGLRenderer); + return shader; +} + +function gloveMaterial(boxer: SkinnedBoxer): THREE.MeshStandardMaterial { + return boxer.gloveMesh("left").material as THREE.MeshStandardMaterial; +} + +describe("corner colour", () => { + it("hands the shader the blood level it returns", () => { + const material = new THREE.MeshStandardMaterial(); + const blood = wearCornerColour(material); + const shader = compile(material); + expect(shader.uniforms.uGearBlood).toBe(blood); + expect(shader.vertexShader).toContain("vGearPos = position;"); + expect(shader.fragmentShader).toContain("uGearBlood"); + expect(material.customProgramCacheKey()).toBe("hands-gear-live"); + }); + + it("reads bind positions from an attribute on a baked part", () => { + const material = new THREE.MeshStandardMaterial(); + wearCornerColour(material, true); + const shader = compile(material); + expect(shader.vertexShader).toContain("attribute vec3 bindPosition;"); + expect(shader.vertexShader).toContain("vGearPos = bindPosition;"); + expect(material.customProgramCacheKey()).toBe("hands-gear-baked"); + }); +}); + +describe("blood on the gloves", () => { + const bleeding = (id: string): FighterSnapshot => ({ + ...baseFighter(id), x: 0, y: -150, facing_x: 0, facing_y: 1000, + trauma: { head: 600, body: 0, left_eye: 0, right_eye: 0, left_cut: 300, right_cut: 200, swelling: 0, bleeding: 400 }, + }); + const fighter = (): FighterSnapshot => ({ ...baseFighter("one"), x: 0, y: 0, facing_x: 0, facing_y: -1000 }); + + it("soaks the gloves as the opponent bleeds and leaves them in the corner's colour", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + const graph = new BoxingGraph(boxer, mapping); + graph.update(fighter(), baseFighter("two"), 1 / 60, 0, false, "full", 1, undefined); + expect(boxer.gloveBloodLevel).toBe(0); + graph.update(fighter(), bleeding("two"), 1 / 60, 0.02, false, "full", 2, undefined); + expect(boxer.gloveBloodLevel).toBe(1); + expect(gloveMaterial(boxer).color.getHex()).toBe(0x1d4ed8); + expect(compile(gloveMaterial(boxer)).uniforms.uGearBlood).toEqual({ value: 1 }); + graph.dispose(); + }); + + it("is lighter with reduced blood and absent with blood off", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0xb91c1c }); + const graph = new BoxingGraph(boxer, mapping); + graph.update(fighter(), bleeding("two"), 1 / 60, 0, false, "reduced", 1, undefined); + expect(boxer.gloveBloodLevel).toBeGreaterThan(0.2); + expect(boxer.gloveBloodLevel).toBeLessThan(0.6); + graph.update(fighter(), bleeding("two"), 1 / 60, 0.02, false, "off", 2, undefined); + expect(boxer.gloveBloodLevel).toBe(0); + graph.dispose(); + }); +}); diff --git a/web/hands/src/render/gear.ts b/web/hands/src/render/gear.ts new file mode 100644 index 00000000..69403e87 --- /dev/null +++ b/web/hands/src/render/gear.ts @@ -0,0 +1,63 @@ +import * as THREE from "three"; + +/** + * The scanned gloves and trunks are black with pale trim, so tinting them left both fighters in + * black. The dark cloth is redrawn in the corner's colour with the scan's folds, and the trim in + * white. Blood soaks a glove from the knuckles back, so the cuff keeps the corner's colour. + */ + +const DECLARATIONS = /* glsl */ ` +uniform float uGearBlood; +varying vec3 vGearPos; +`; + +const FRAGMENT = /* glsl */ ` +#ifdef USE_MAP +{ + float scanned = dot(sampledDiffuseColor.rgb, vec3(0.2126, 0.7152, 0.0722)); + vec3 cloth = diffuse * clamp(scanned / 0.035, 0.45, 1.5) * 0.85; + vec3 piping = vec3(0.68) * clamp(scanned / 0.22, 0.6, 1.15); + gearTrim = smoothstep(0.07, 0.12, scanned); + diffuseColor.rgb = mix(cloth, piping, gearTrim); + float ragged = 0.08 * sin(vGearPos.y * 1.9 + vGearPos.z * 2.3) * sin(vGearPos.z * 1.3 - vGearPos.x * 0.7); +#ifdef GEAR_TRUNKS + // Blood runs down the chest into the front of the waistband and soaks down from there in runs. + float runs = 0.5 + 0.5 * sin(vGearPos.x * 0.9 + 1.3) * sin(vGearPos.x * 2.3); + float depth = mix(2.0, 22.0, uGearBlood) * (0.45 + 0.55 * runs) + ragged * 20.0; + float front = smoothstep(-1.0, 3.0, vGearPos.z) * (1.0 - smoothstep(9.0, 15.0, abs(vGearPos.x))); + gearSoaked = (1.0 - smoothstep(depth, depth + 3.0, 80.0 - vGearPos.y)) * front * min(1.0, uGearBlood * 2.5); +#else + float knuckles = smoothstep(46.0, 55.0, abs(vGearPos.x)); + float reach = mix(1.05, 0.5, uGearBlood) + ragged; + gearSoaked = smoothstep(reach, reach + 0.22, knuckles) * min(1.0, uGearBlood * 3.0); +#endif + diffuseColor.rgb = mix(diffuseColor.rgb, vec3(0.11, 0.003, 0.005), gearSoaked * 0.9); +} +#endif +`; + +const ROUGHNESS = /* glsl */ ` +roughnessFactor = mix(mix(roughnessFactor, 0.8, gearTrim), 0.2, gearSoaked); +`; + +/** + * Returns the uniform that holds how bloodied the gear is, 0 to 1. A `baked` material draws geometry + * that is no longer in bind space, which carries its bind positions in a `bindPosition` attribute. + */ +export function wearCornerColour(material: THREE.MeshStandardMaterial, baked = false, part: "gloves" | "trunks" = "gloves"): { value: number } { + const blood = { value: 0 }; + if (part === "trunks") material.defines = { ...material.defines, GEAR_TRUNKS: "" }; + material.onBeforeCompile = (shader) => { + shader.uniforms.uGearBlood = blood; + shader.vertexShader = shader.vertexShader + .replace("#include ", `#include \n${baked ? "attribute vec3 bindPosition;" : ""}\n${DECLARATIONS}`) + .replace("#include ", `#include \nvGearPos = ${baked ? "bindPosition" : "position"};`); + shader.fragmentShader = shader.fragmentShader + .replace("#include ", `#include \n${DECLARATIONS}\nfloat gearSoaked = 0.0;\nfloat gearTrim = 0.0;`) + .replace("#include ", `#include \n${FRAGMENT}`) + .replace("#include ", `#include \n${ROUGHNESS}`); + }; + material.customProgramCacheKey = () => `hands-gear-${baked ? "baked" : "live"}`; + material.needsUpdate = true; + return blood; +} diff --git a/web/hands/src/render/gore-geometry.test.ts b/web/hands/src/render/gore-geometry.test.ts index 8cf2ae5c..04beadd3 100644 --- a/web/hands/src/render/gore-geometry.test.ts +++ b/web/hands/src/render/gore-geometry.test.ts @@ -34,7 +34,7 @@ function shaderJawMask(): (y: number, z: number) => number { const t = THREE.MathUtils.clamp((x - edge0) / (edge1 - edge0), 0, 1); return t * t * (3 - 2 * t); }; - const mask = new Function("smoothstep", "transformed", "uInjuryJawLevel", `return ${expression};`) as (step: typeof smoothstep, transformed: { y: number; z: number }, level: number) => number; + const mask = new Function("smoothstep", "injuryMoved", "uInjuryJawLevel", `return ${expression};`) as (step: typeof smoothstep, moved: { y: number; z: number }, level: number) => number; material.dispose(); return (y, z) => mask(smoothstep, { y, z }, shading.uniforms.uInjuryJawLevel.value); } diff --git a/web/hands/src/render/gore.test.ts b/web/hands/src/render/gore.test.ts new file mode 100644 index 00000000..f26f1aea --- /dev/null +++ b/web/hands/src/render/gore.test.ts @@ -0,0 +1,1134 @@ +import * as THREE from "three"; +import { describe, expect, it } from "vitest"; +import { fighter } from "../test/fixtures"; +import type { CombatEvent, FighterSnapshot } from "../types"; +import { BIG_SHOT, BLOOD_SHADES, HARD_SHOT, ROCKING_COUNTER, bloodDropsFor, bloodShade, buildChunkGeometry, buildDropletGeometry, buildWoundGeometry, closeCut, cutRim, dropletShape, teethFor } from "./gore"; +import { BoxingGraph, SkinnedBoxer, loadBoxerGlb } from "./graph"; +import { HEAD_SITES, InjuryShading, NECK_CUT_HEIGHT, applyHeadTrauma } from "./injury"; +import { SCANNED_LOOK, lookShape } from "./looks"; +import { Effects3D, confineToRopes } from "./effects"; +import { FightRenderer, aboveNeckCut, bakeSeveredHead, bakeSkinnedPart, closeUpAngle, keepClear } from "./renderer"; +import { CANVAS_TOP, ROPE_LINE, worldMapping } from "./world"; + +const gltf = await loadBoxerGlb(); + +type Pose = { position: THREE.Vector3; quaternion: THREE.Quaternion }; +const renderer = FightRenderer.prototype as unknown as { + headWorldPose(this: unknown, index: number): Pose | null; + stumpWorldPose(this: unknown, index: number): (Pose & { rim: Float32Array; across: THREE.Vector3 }) | null; + closeUpFrame(this: unknown, seconds: number): { position: THREE.Vector3; lookAt: THREE.Vector3 } | null; +}; + +describe("flesh chunk", () => { + it("is an uneven, flattened lump rather than a regular solid", () => { + const geometry = buildChunkGeometry(); + const position = geometry.getAttribute("position"); + expect(position.count).toBeGreaterThan(60); + geometry.computeBoundingBox(); + const size = geometry.boundingBox!.getSize(new THREE.Vector3()); + expect(size.y).toBeLessThan(size.x * 0.8); + expect(size.x).toBeGreaterThan(0.04); + expect(size.x).toBeLessThan(0.09); + const vertex = new THREE.Vector3(); + let near = Infinity; + let far = 0; + for (let index = 0; index < position.count; index += 1) { + const reach = vertex.fromBufferAttribute(position, index).setY(0).length(); + if (Math.abs(position.getY(index)) > 0.004) continue; + near = Math.min(near, reach); + far = Math.max(far, reach); + } + expect(far / near).toBeGreaterThan(1.3); + }); + + it("shades smoothly", () => { + const geometry = buildChunkGeometry(); + expect(geometry.getIndex()).not.toBeNull(); + const normal = geometry.getAttribute("normal"); + const n = new THREE.Vector3(); + for (let index = 0; index < normal.count; index += 1) expect(n.fromBufferAttribute(normal, index).length()).toBeCloseTo(1, 3); + }); +}); + +describe("wound surface", () => { + it("meets the skin at the rim, rises in the middle and faces outward", () => { + const geometry = buildWoundGeometry(0.07, 0.05, 0.01); + const position = geometry.getAttribute("position"); + const normal = geometry.getAttribute("normal"); + expect(position.getY(0)).toBeCloseTo(0.01, 6); + let rim = 0; + for (let index = 0; index < position.count; index += 1) { + const x = position.getX(index) / 0.07; + const z = position.getZ(index) / 0.05; + const reach = Math.hypot(x, z); + expect(reach).toBeLessThanOrEqual(1 + 1e-6); + expect(normal.getY(index)).toBeGreaterThan(0.5); + if (reach > 1 - 1e-6) { + rim += 1; + expect(position.getY(index)).toBeCloseTo(0, 6); + } + } + expect(rim).toBe(28); + }); +}); + +describe("cut rim", () => { + it("runs round the opening a cut leaves and ignores the seams and edges the mesh already had", () => { + // An open tube with a seam down one side: the seam's vertices are doubled, as a textured mesh's are. + const tube = new THREE.CylinderGeometry(1, 1, 4, 12, 4, true); + const position = tube.getAttribute("position"); + const rim = cutRim(tube, (vertex) => vertex.y > 0.5); + expect(rim).toHaveLength(12 * 2); + expect(new Set(rim).size).toBe(12); + for (const index of rim) expect(position.getY(index)).toBeCloseTo(1, 6); + for (let edge = 0; edge < rim.length; edge += 2) { + const a = new THREE.Vector3().fromBufferAttribute(position, rim[edge]!); + const b = new THREE.Vector3().fromBufferAttribute(position, rim[edge + 1]!); + expect(a.distanceTo(b)).toBeCloseTo(2 * Math.sin(Math.PI / 12), 6); + } + expect(cutRim(tube, () => true)).toEqual([]); + expect(cutRim(tube, () => false)).toEqual([]); + tube.dispose(); + }); + + it("finds the neck of the head mesh, centred on the spine", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + const position = boxer.headMesh.geometry.getAttribute("position"); + const rim = cutRim(boxer.headMesh.geometry, aboveNeckCut); + expect(rim.length).toBeGreaterThan(40); + // A closed loop: every vertex on it ends two of its edges. + const ends = new Map(); + for (const index of rim) ends.set(index, (ends.get(index) ?? 0) + 1); + expect([...ends.values()].every((count) => count === 2)).toBe(true); + const middle = new THREE.Vector3(); + const vertex = new THREE.Vector3(); + for (const index of rim) { + vertex.fromBufferAttribute(position, index); + expect(aboveNeckCut(vertex)).toBe(true); + expect(Math.hypot(vertex.x, vertex.z + 3.8)).toBeLessThan(6.5); + middle.add(vertex); + } + middle.multiplyScalar(1 / rim.length); + expect(Math.abs(middle.x)).toBeLessThan(0.8); + expect(Math.abs(middle.z + 3.8)).toBeLessThan(0.8); + boxer.dispose(); + }); +}); + +describe("neck cut", () => { + it("leaves the chin and the back of the skull on the head and the collar on the shoulders", () => { + expect(aboveNeckCut(new THREE.Vector3(0, 110.8, 4.2))).toBe(true); + expect(aboveNeckCut(new THREE.Vector3(0, 114, -8))).toBe(true); + expect(aboveNeckCut(new THREE.Vector3(0, 125, 0))).toBe(true); + expect(aboveNeckCut(new THREE.Vector3(0, 111, -8))).toBe(false); + expect(aboveNeckCut(new THREE.Vector3(8, 107, -4))).toBe(false); + }); + + it("cuts the skin and its shadow at the same line", () => { + const material = new THREE.MeshStandardMaterial(); + const shading = new InjuryShading(material, HEAD_SITES); + const shadow = shading.shadowMaterial(); + const skin = { uniforms: {} as Record, vertexShader: "#include \n#include ", fragmentShader: "#include \n#include \n#include " }; + material.onBeforeCompile(skin as unknown as THREE.WebGLProgramParametersWithUniforms, null as unknown as THREE.WebGLRenderer); + const depth = { uniforms: {} as Record, vertexShader: "#include \n#include ", fragmentShader: "#include \n#include " }; + shadow.onBeforeCompile(depth as unknown as THREE.WebGLProgramParametersWithUniforms, null as unknown as THREE.WebGLRenderer); + expect(depth.uniforms.uInjurySever).toBe(skin.uniforms.uInjurySever); + // Both keep what is below the line: the collar stays on the shoulders and casts its shadow. + expect(skin.fragmentShader).toContain("float below = injuryCutLine(injuryPos) - injuryPos.y;"); + expect(skin.fragmentShader).toContain("if (below < 0.0) discard;"); + expect(depth.fragmentShader).toContain("vInjuryPos.y > injuryCutLine(vInjuryPos)) discard;"); + expect(skin.uniforms.uInjurySever!.value).toBeGreaterThan(500); + shading.setSevered(true); + expect(skin.uniforms.uInjurySever!.value).toBe(NECK_CUT_HEIGHT); + shading.setSevered(false); + expect(skin.uniforms.uInjurySever!.value).toBeGreaterThan(500); + }); + + it("keeps the head mesh drawn for the collar while the head is off", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + boxer.setDecapitated(true); + expect(boxer.isDecapitated).toBe(true); + expect(boxer.headMesh.visible).toBe(true); + expect(boxer.headInjury.uniforms.uInjurySever.value).toBe(NECK_CUT_HEIGHT); + expect(boxer.headMesh.customDepthMaterial).toBeInstanceOf(THREE.MeshDepthMaterial); + boxer.setDecapitated(false); + expect(boxer.headInjury.uniforms.uInjurySever.value).toBeGreaterThan(500); + boxer.dispose(); + }); + + it("leaves a severed head on an ear, its face turned to the side and the head on the canvas, however it spun", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + boxer.rig.resetToRest(); + boxer.root.updateMatrixWorld(true); + const head = boxer.bone("head")!; + const at = head.getWorldPosition(new THREE.Vector3()); + const turn = head.getWorldQuaternion(new THREE.Quaternion()); + const scene = new THREE.Scene(); + const effects = new Effects3D(scene); + const face = new THREE.Vector3(); + const point = new THREE.Vector3(); + for (let event = 1; event <= 12; event += 1) { + effects.clearDynamic(); + const baked = bakeSeveredHead(boxer, at, turn); + effects.decapitate(1, at, turn, event % 2 === 0 ? 1 : -1, event * 7, 0xb0703f, baked); + for (let frame = 0; frame < 240; frame += 1) effects.update(1 / 60); + expect(effects.severedHeadFacing(1, face)).toBe(true); + expect(Math.abs(face.y)).toBeLessThan(0.05); + const mesh = scene.children.find((child): child is THREE.Mesh => child instanceof THREE.Mesh && child.geometry === baked.geometry)!; + mesh.updateMatrixWorld(true); + const position = baked.geometry.getAttribute("position"); + const used = baked.geometry.getIndex()!; + let lowest = Infinity; + for (let index = 0; index < used.count; index += 1) lowest = Math.min(lowest, point.fromBufferAttribute(position, used.getX(index)).applyMatrix4(mesh.matrixWorld).y); + expect(Math.abs(lowest - CANVAS_TOP)).toBeLessThan(0.005); + } + effects.dispose(); + boxer.dispose(); + }); + + it("keeps the face it was beaten into once the head is off: the live skin, bruises, swelling, cuts and blood", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + applyHeadTrauma(boxer.headInjury, { head: 900, body: 0, left_eye: 600, right_eye: 200, left_cut: 300, right_cut: 0, swelling: 400, bleeding: 200 }, "full"); + boxer.rig.resetToRest(); + boxer.root.rotation.y = 0.7; + boxer.root.updateMatrixWorld(true); + const head = boxer.bone("head")!; + const at = head.getWorldPosition(new THREE.Vector3()); + const turn = head.getWorldQuaternion(new THREE.Quaternion()); + const scene = new THREE.Scene(); + const effects = new Effects3D(scene); + const baked = bakeSeveredHead(boxer, at, turn); + effects.decapitate(0, at, turn, 1, 8, 0xb0703f, baked); + const severed = scene.children.find((child): child is THREE.Mesh => child instanceof THREE.Mesh && child.geometry === baked.geometry)!; + const material = severed.material as THREE.MeshPhysicalMaterial; + const live = boxer.headMesh.material as THREE.MeshPhysicalMaterial; + expect(material).toBeInstanceOf(THREE.MeshPhysicalMaterial); + expect([material.roughness, material.metalness, material.clearcoat, material.clearcoatRoughness]).toEqual([live.roughness, live.metalness, live.clearcoat, live.clearcoatRoughness]); + expect(material.map).toBe(live.map); + const shader = { uniforms: {} as Record, vertexShader: "#include \n#include \n#include ", fragmentShader: "#include \n#include \n#include " }; + material.onBeforeCompile(shader as unknown as THREE.WebGLProgramParametersWithUniforms, null as unknown as THREE.WebGLRenderer); + expect(shader.vertexShader).toContain("vec3 injuryBind = bindPosition;"); + expect(shader.fragmentShader).toContain("uniform vec4 uInjuryCut"); + // The look's hair and beard first, then the injuries over them, as on the live head. + expect(shader.fragmentShader.indexOf("uLookGroom.y")).toBeLessThan(shader.fragmentShader.indexOf("float cut = uInjuryCut[i].y;")); + const own = shader.uniforms; + const theirs = boxer.headInjury.uniforms; + expect(own.uInjuryBruise!.value).toEqual(theirs.uInjuryBruise.value); + expect(own.uInjurySwell!.value).toEqual(theirs.uInjurySwell.value); + expect(own.uInjuryBlood!.value).toEqual(theirs.uInjuryBlood.value); + expect(own.uInjuryCut!.value).toEqual(theirs.uInjuryCut.value); + expect(own.uInjuryLid!.value).toEqual(theirs.uInjuryLid.value); + expect(own.uInjuryNose!.value).toEqual(theirs.uInjuryNose.value); + expect(Math.max(...(own.uInjurySwell!.value as Float32Array))).toBeGreaterThan(1); + // Not cut open itself: the cut is the flesh closing its neck. + expect(material.defines).not.toHaveProperty("HANDS_INJURY_OPEN"); + // The swelling and the broken nose are offsets in bind space, turned into the head's own frame as it + // was baked: the bind positions, reshaped by the look and turned so, land on the baked ones. + const toPart = own.uInjuryBindToPart!.value as THREE.Matrix3; + const position = baked.geometry.getAttribute("position"); + const bind = baked.geometry.getAttribute("bindPosition"); + const used = baked.geometry.getIndex()!; + const origin = used.getX(0); + const shapedOrigin = lookShape(new THREE.Vector3().fromBufferAttribute(bind, origin), boxer.look, new THREE.Vector3()); + let worst = 0; + for (let corner = 0; corner < used.count; corner += 97) { + const vertex = used.getX(corner); + const offset = lookShape(new THREE.Vector3().fromBufferAttribute(bind, vertex), boxer.look, new THREE.Vector3()).sub(shapedOrigin).applyMatrix3(toPart); + const actual = new THREE.Vector3().fromBufferAttribute(position, vertex).sub(new THREE.Vector3().fromBufferAttribute(position, origin)); + worst = Math.max(worst, offset.distanceTo(actual)); + } + expect(worst).toBeLessThan(1e-4); + effects.restoreFighter(0); + expect(Math.max(...(own.uInjuryBruise!.value as Float32Array))).toBe(0); + effects.dispose(); + boxer.dispose(); + }); + + it("takes the head off whole, without the skin it shares with the neck and chest trailing after the body", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + const head = boxer.bone("head")!; + const bake = (): THREE.BufferGeometry => { + boxer.root.updateMatrixWorld(true); + return bakeSeveredHead(boxer, head.getWorldPosition(new THREE.Vector3()), head.getWorldQuaternion(new THREE.Quaternion())).geometry; + }; + boxer.rig.resetToRest(); + const whole = bake(); + // A head snapped back on a body doubled over, as a knockout blow leaves it. + boxer.rig.bones.chest.rotateX(0.5); + boxer.rig.bones.upperChest.rotateX(0.4); + boxer.rig.bones.neck.rotateX(0.6); + head.rotateX(-1.2); + const posed = bake(); + const index = posed.getIndex()!; + const before = whole.getAttribute("position"); + const after = posed.getAttribute("position"); + let drift = 0; + for (let at = 0; at < index.count; at += 1) { + const vertex = index.getX(at); + drift = Math.max(drift, new THREE.Vector3().fromBufferAttribute(after, vertex).distanceTo(new THREE.Vector3().fromBufferAttribute(before, vertex))); + } + expect(drift).toBeLessThan(0.001); + whole.dispose(); + posed.dispose(); + boxer.dispose(); + }); + + /** Vertices sharing a place on the surface: the two sides of the scan's UV seams. */ + const seamGroups = (geometry: THREE.BufferGeometry): number[][] => { + const position = geometry.getAttribute("position"); + const used = new Set(); + const index = geometry.getIndex()!; + for (let at = 0; at < index.count; at += 1) used.add(index.getX(at)); + const groups = new Map(); + for (const vertex of used) { + const key = `${Math.round(position.getX(vertex) * 1e4)},${Math.round(position.getY(vertex) * 1e4)},${Math.round(position.getZ(vertex) * 1e4)}`; + groups.set(key, [...(groups.get(key) ?? []), vertex]); + } + return [...groups.values()]; + }; + /** Angles (degrees) between each vertex's normal and its surface's, its faces' normals summed across the seams. */ + const offSurface = (geometry: THREE.BufferGeometry): number[] => { + const position = geometry.getAttribute("position"); + const normal = geometry.getAttribute("normal"); + const index = geometry.getIndex()!; + const groups = seamGroups(geometry); + const groupOf = new Map(); + for (const [at, group] of groups.entries()) for (const vertex of group) groupOf.set(vertex, at); + const sums = groups.map(() => new THREE.Vector3()); + const a = new THREE.Vector3(), b = new THREE.Vector3(), c = new THREE.Vector3(); + for (let at = 0; at < index.count; at += 3) { + a.fromBufferAttribute(position, index.getX(at)); + b.fromBufferAttribute(position, index.getX(at + 1)); + c.fromBufferAttribute(position, index.getX(at + 2)); + const face = c.sub(b).cross(a.sub(b)); + for (let corner = 0; corner < 3; corner += 1) sums[groupOf.get(index.getX(at + corner))!]!.add(face); + } + const angles: number[] = []; + for (const [at, group] of groups.entries()) { + for (const vertex of group) angles.push(THREE.MathUtils.radToDeg(a.fromBufferAttribute(normal, vertex).angleTo(sums[at]!))); + } + return angles.sort((x, y) => x - y); + }; + + it("keeps the scan's own smooth shading across its UV seams on a severed head and glove", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + const head = boxer.bone("head")!; + // Posed as a knockout leaves it: the head snapped back on a body doubled over, the arm raised. + boxer.rig.bones.chest.rotateX(0.5); + boxer.rig.bones.neck.rotateX(0.6); + head.rotateX(-1.2); + boxer.rig.bones.shoulderL.rotateZ(0.9); + boxer.root.updateMatrixWorld(true); + const severed = bakeSeveredHead(boxer, head.getWorldPosition(new THREE.Vector3()), head.getWorldQuaternion(new THREE.Quaternion())).geometry; + const glove = boxer.bone("gloveL")!; + const hand = bakeSkinnedPart(boxer.gloveMesh("left"), glove.getWorldPosition(new THREE.Vector3()), glove.getWorldQuaternion(new THREE.Quaternion())).geometry; + for (const geometry of [severed, hand]) { + const normal = geometry.getAttribute("normal"); + let seams = 0; + let split = 0; + for (const group of seamGroups(geometry)) { + for (let i = 1; i < group.length; i += 1) { + seams += 1; + const angle = THREE.MathUtils.radToDeg(new THREE.Vector3().fromBufferAttribute(normal, group[0]!).angleTo(new THREE.Vector3().fromBufferAttribute(normal, group[i]!))); + if (angle > 5) split += 1; + } + } + expect(seams).toBeGreaterThan(50); + expect(split).toBe(0); + // Turned with the part as it was posed: they agree with the surface they shade. + const angles = offSurface(geometry); + expect(angles[Math.floor(angles.length / 2)]!).toBeLessThan(12); + expect(angles[Math.floor(angles.length * 0.95)]!).toBeLessThan(35); + } + // The cut's rim indexes the head mesh's own numbering, which the severed head keeps. + expect(severed.getAttribute("position").count).toBe(boxer.headMesh.geometry.getAttribute("position").count); + severed.dispose(); + hand.dispose(); + boxer.dispose(); + }); + + it("closes both sides of the cut with flesh that meets the skin all the way round and faces out", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + const graph = new BoxingGraph(boxer, worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 })); + const self = { ...fighter("one"), x: -40, y: 0, facing_x: 1000, facing_y: 0, defense: "slip_left" as const }; + const other = { ...fighter("two"), x: 40, y: 0, facing_x: -1000, facing_y: 0 }; + for (let frame = 0; frame < 40; frame += 1) graph.update(self, other, 1 / 60, frame / 60, false, "full", 100 + frame / 2); + const stub = { graphs: [graph, graph], tmpHead: new THREE.Vector3(), tmpHeadQuaternion: new THREE.Quaternion(), tmpStumpOffset: new THREE.Vector3(), tmpStump: new THREE.Vector3(), tmpStumpQuaternion: new THREE.Quaternion(), tmpStumpAcross: new THREE.Vector3(), stumpRim: new Float32Array(0) }; + const pose = renderer.headWorldPose.call(stub, 0)!; + const baked = bakeSkinnedPart(boxer.headMesh, pose.position, pose.quaternion, aboveNeckCut); + const scene = new THREE.Scene(); + const effects = new Effects3D(scene); + effects.decapitate(0, pose.position, pose.quaternion, 1, 8, 0xb0703f, baked); + const stump = renderer.stumpWorldPose.call(stub, 0)!; + effects.anchorStump(0, stump.position, stump.quaternion, stump.rim); + scene.updateMatrixWorld(true); + const head = scene.children.find((child): child is THREE.Mesh => child instanceof THREE.Mesh && child.geometry === baked.geometry)!; + const cap = head.children[0] as THREE.Mesh; + const wound = scene.children.find((child): child is THREE.Mesh => child instanceof THREE.Mesh && child !== head && child.visible && child.position.distanceTo(stump.position) < 1e-6)!; + const rim = cutRim(boxer.headMesh.geometry, aboveNeckCut); + + const corners = (mesh: THREE.Mesh): THREE.Vector3[] => { + const position = mesh.geometry.getAttribute("position"); + return Array.from({ length: position.count }, (_, index) => new THREE.Vector3().fromBufferAttribute(position, index).applyMatrix4(mesh.matrixWorld)); + }; + const nearest = (point: THREE.Vector3, among: THREE.Vector3[]): number => Math.min(...among.map((other) => other.distanceTo(point))); + const faces = (mesh: THREE.Mesh): THREE.Vector3 => { + const normal = mesh.geometry.getAttribute("normal"); + const sum = new THREE.Vector3(); + for (let index = 0; index < normal.count; index += 1) sum.add(new THREE.Vector3().fromBufferAttribute(normal, index)); + return sum.normalize().transformDirection(mesh.matrixWorld); + }; + + const baked3 = baked.geometry.getAttribute("position"); + const bind = boxer.headMesh.geometry.getAttribute("position"); + const onHead = rim.map((index) => new THREE.Vector3().fromBufferAttribute(baked3, index).applyMatrix4(head.matrixWorld)); + const onBody = rim.map((index) => boxer.headMesh.applyBoneTransform(index, new THREE.Vector3().fromBufferAttribute(bind, index)).applyMatrix4(boxer.headMesh.matrixWorld)); + + expect(cap.visible).toBe(true); + expect(cap.geometry.getAttribute("position").count).toBe((rim.length / 2) * 3); + for (const point of onHead) expect(nearest(point, corners(cap))).toBeLessThan(1e-5); + for (const point of onBody) expect(nearest(point, corners(wound))).toBeLessThan(1e-5); + // Nothing of the flesh reaches past the rim it closes. + const middle = (points: THREE.Vector3[]): THREE.Vector3 => points.reduce((sum, point) => sum.add(point), new THREE.Vector3()).multiplyScalar(1 / points.length); + const reach = (points: THREE.Vector3[]): number => Math.max(...points.map((point) => point.distanceTo(middle(points)))); + for (const corner of corners(cap)) expect(corner.distanceTo(middle(onHead))).toBeLessThanOrEqual(reach(onHead) + 1e-6); + for (const corner of corners(wound)) expect(corner.distanceTo(middle(onBody))).toBeLessThanOrEqual(reach(onBody) + 1e-6); + + expect(faces(cap).dot(middle(onHead).sub(pose.position).normalize())).toBeGreaterThan(0.7); + expect(faces(wound).dot(pose.position.clone().sub(middle(onBody)).normalize())).toBeGreaterThan(0.7); + effects.dispose(); + graph.dispose(); + }); + + it("turns the neck's cross-section with the head: the windpipe at the throat and the spine at the nape, on both sides of the cut", () => { + // Squared up along x, where a cross-section fixed to the world would put the spine at the side of the neck. + for (const facing of [1000, -1000]) { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + const graph = new BoxingGraph(boxer, worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 })); + const self = { ...fighter("one"), x: -40, y: 0, facing_x: facing, facing_y: 0 }; + const other = { ...fighter("two"), x: 40, y: 0, facing_x: -facing, facing_y: 0 }; + for (let frame = 0; frame < 40; frame += 1) graph.update(self, other, 1 / 60, frame / 60, false, "full", 100 + frame / 2); + const stub = { graphs: [graph, graph], tmpHead: new THREE.Vector3(), tmpHeadQuaternion: new THREE.Quaternion(), tmpStumpOffset: new THREE.Vector3(), tmpStump: new THREE.Vector3(), tmpStumpQuaternion: new THREE.Quaternion(), tmpStumpAcross: new THREE.Vector3(), stumpRim: new Float32Array(0) }; + const pose = renderer.headWorldPose.call(stub, 0)!; + const baked = bakeSkinnedPart(boxer.headMesh, pose.position, pose.quaternion, aboveNeckCut); + const scene = new THREE.Scene(); + const effects = new Effects3D(scene); + effects.decapitate(0, pose.position, pose.quaternion, 1, 8, 0xb0703f, baked); + const stump = renderer.stumpWorldPose.call(stub, 0)!; + effects.anchorStump(0, stump.position, stump.quaternion, stump.rim, stump.across); + const wound = scene.children.find((child): child is THREE.Mesh => child instanceof THREE.Mesh && child.visible && child.geometry !== baked.geometry && child.position.distanceTo(stump.position) < 1e-6)!; + /** The texture's v at the rim corner of a cut's flesh furthest toward `toward`, in the flesh's own frame. */ + const vToward = (flesh: THREE.BufferGeometry, toward: THREE.Vector3): number => { + const position = flesh.getAttribute("position"); + const uv = flesh.getAttribute("uv"); + let best = -Infinity; + let v = 0.5; + for (let corner = 0; corner < position.count; corner += 1) { + if (corner % 3 === 0) continue; + const along = new THREE.Vector3().fromBufferAttribute(position, corner).dot(toward); + if (along > best) { + best = along; + v = uv.getY(corner); + } + } + return v; + }; + // The neck texture draws the spine high in v and the windpipe low. + const face = new THREE.Vector3(0, 0, 1).applyQuaternion(boxer.bone("head")!.getWorldQuaternion(new THREE.Quaternion())); + face.y = 0; + face.normalize(); + expect(Math.abs(face.x)).toBeGreaterThan(0.9); + expect(vToward(wound.geometry, face)).toBeLessThan(0.3); + expect(vToward(wound.geometry, face.clone().negate())).toBeGreaterThan(0.7); + // The severed head's own cut, in the head's frame, where the face points along +z. + expect(vToward(baked.cut!.flesh, new THREE.Vector3(0, 0, 1))).toBeLessThan(0.3); + expect(vToward(baked.cut!.flesh, new THREE.Vector3(0, 0, -1))).toBeGreaterThan(0.7); + effects.dispose(); + graph.dispose(); + } + }); + + it("falls back to a disc on the neck when the cut has not been measured", () => { + const effects = new Effects3D(new THREE.Scene()); + effects.decapitate(0, new THREE.Vector3(0, 1.5, 0), new THREE.Quaternion(), 1, 8); + const turn = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), 0.3); + effects.anchorStump(0, new THREE.Vector3(0.2, 1.4, 0.1), turn); + const stumps = (effects as unknown as { stumps: { mesh: THREE.Mesh }[] }).stumps; + expect(stumps[0]!.mesh.position.toArray()).toEqual([0.2, 1.4, 0.1]); + expect(stumps[0]!.mesh.quaternion.angleTo(turn)).toBeCloseTo(0, 6); + expect(stumps[0]!.mesh.geometry.getAttribute("position").count).toBeGreaterThan(100); + effects.dispose(); + }); +}); + +describe("an arterial fountain", () => { + type Spawn = { time: number; velocity: THREE.Vector3; at: THREE.Vector3 }; + /** Every drop a stump throws, and when: from the cut until `seconds` later. */ + const pump = (effects: Effects3D, seconds: number): Spawn[] => { + const spawns: Spawn[] = []; + let time = 0; + const spawn = vi.spyOn(effects as unknown as { spawnDroplet(...args: number[]): void }, "spawnDroplet").mockImplementation((x, y, z, vx, vy, vz) => { + spawns.push({ time, velocity: new THREE.Vector3(vx, vy, vz), at: new THREE.Vector3(x, y, z) }); + }); + for (; time < seconds; time += 1 / 60) effects.update(1 / 60); + spawn.mockRestore(); + return spawns; + }; + // A neck lying on the canvas, its wound facing along +x. + const along = new THREE.Vector3(1, 0, 0); + const lying = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), along); + const severed = (effects: Effects3D): void => { + effects.decapitate(0, new THREE.Vector3(0.4, 0.25, 0.2), new THREE.Quaternion(), 1, 8); + effects.anchorStump(0, new THREE.Vector3(0.4, 0.25, 0.2), lying); + }; + + it("pumps with the heart for seconds, in spurts that weaken as he bleeds out", () => { + const effects = new Effects3D(new THREE.Scene()); + severed(effects); + const spawns = pump(effects, 9); + const between = (from: number, to: number): Spawn[] => spawns.filter((spawn) => spawn.time >= from && spawn.time < to); + expect(between(5, 6.5).length).toBeGreaterThan(5); + expect(between(6.6, 9)).toHaveLength(0); + expect(between(0, 1).length).toBeGreaterThan(between(4, 5).length * 1.5); + // Spurts at the heart's rate: tenths of a second at the top of a beat throw far more than those between beats. + const tenths = Array.from({ length: 24 }, (_, tenth) => between(tenth / 10, (tenth + 1) / 10).length); + expect(Math.max(...tenths)).toBeGreaterThanOrEqual(Math.max(4, 4 * Math.min(...tenths))); + let peaks = 0; + for (let tenth = 1; tenth + 1 < tenths.length; tenth += 1) if (tenths[tenth]! > tenths[tenth - 1]! && tenths[tenth]! >= tenths[tenth + 1]! && tenths[tenth]! >= Math.max(...tenths) / 2) peaks += 1; + // 1.7 beats a second: four beats in 2.4 s. + expect(peaks).toBeGreaterThanOrEqual(3); + expect(peaks).toBeLessThanOrEqual(5); + // Its heaviest spurts throw only so many drops, for the phones. + expect(spawns.length).toBeLessThan(260); + effects.dispose(); + }); + + it("throws the blood at arterial speed along the wound's axis, not straight up", () => { + const effects = new Effects3D(new THREE.Scene()); + severed(effects); + const spawns = pump(effects, 6.5); + const early = spawns.filter((spawn) => spawn.time < 1.5); + for (const spawn of spawns) { + expect(THREE.MathUtils.radToDeg(spawn.velocity.angleTo(along))).toBeLessThan(25); + expect(spawn.velocity.length()).toBeLessThanOrEqual(6 + 1e-9); + } + const fast = early.filter((spawn) => spawn.velocity.length() >= 4); + expect(fast.length).toBeGreaterThan(early.length * 0.3); + // Weaker as the pressure falls. + const late = spawns.filter((spawn) => spawn.time > 5.5); + const mean = (list: Spawn[]): number => list.reduce((sum, spawn) => sum + spawn.velocity.length(), 0) / Math.max(1, list.length); + expect(mean(late)).toBeLessThan(mean(early) * 0.8); + effects.dispose(); + }); + + it("faces out of the flesh closing a measured cut", () => { + const effects = new Effects3D(new THREE.Scene()); + effects.decapitate(0, new THREE.Vector3(0, 1.4, 0), new THREE.Quaternion(), 1, 8); + // The neck tipped over toward -z, its cut measured as a ring round the wound. + const outward = new THREE.Vector3(0, 0.5, -1).normalize(); + const centre = new THREE.Vector3(0, 1.4, 0); + const turn = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), outward); + const rim: number[] = []; + for (let edge = 0; edge < 16; edge += 1) { + for (const end of [edge, edge + 1]) { + const angle = (end / 16) * Math.PI * 2; + rim.push(...new THREE.Vector3(Math.cos(angle) * 0.06, 0, Math.sin(angle) * 0.06).applyQuaternion(turn).add(centre).toArray()); + } + } + effects.anchorStump(0, centre, turn, rim); + const spawns = pump(effects, 1); + expect(spawns.length).toBeGreaterThan(10); + for (const spawn of spawns) expect(THREE.MathUtils.radToDeg(spawn.velocity.angleTo(outward))).toBeLessThan(25); + effects.dispose(); + }); + + it("pumps a severed wrist too, and stops with the blood turned down", () => { + const effects = new Effects3D(new THREE.Scene()); + effects.dismemberHand(0, "left", new THREE.Vector3(0.3, 0.2, 0), new THREE.Quaternion(), 1, 21, 0x1d4ed8); + effects.anchorHandStump(0, "left", new THREE.Vector3(0.3, 0.2, 0), lying); + const spawns = pump(effects, 6); + expect(spawns.filter((spawn) => spawn.time > 3).length).toBeGreaterThan(5); + for (const spawn of spawns) expect(THREE.MathUtils.radToDeg(spawn.velocity.angleTo(along))).toBeLessThan(25); + const reduced = new Effects3D(new THREE.Scene()); + severed(reduced); + reduced.setBloodLevel("reduced"); + expect(pump(reduced, 3)).toHaveLength(0); + effects.dispose(); + reduced.dispose(); + }); +}); + +describe("ropes and a severed part", () => { + it("bounce a part that reaches them from inside", () => { + expect(confineToRopes(2.35, 3, 2.28, 2.3)).toEqual({ position: 2.3, velocity: 3 * -0.42 }); + expect(confineToRopes(-2.35, -3, -2.28, 2.3)).toEqual({ position: -2.3, velocity: -3 * -0.42 }); + expect(confineToRopes(1.2, 3, 1.15, 2.3)).toEqual({ position: 1.2, velocity: 3 }); + }); + + it("draw a part cut off beyond them back in without moving it", () => { + const outward = confineToRopes(2.8, 3, 2.75, 2.3); + expect(outward.position).toBe(2.8); + expect(outward.velocity).toBeCloseTo(-2, 9); + const still = confineToRopes(-2.6, 0, -2.6, 2.3); + expect(still.position).toBe(-2.6); + expect(still.velocity).toBeCloseTo(1.2, 9); + expect(confineToRopes(2.8, -5, 2.88, 2.3)).toEqual({ position: 2.8, velocity: -5 }); + }); + + it("never move a head or a hand cut off on the ropes more than it flies in a frame, and bring it inside", () => { + for (const x of [1.8, 2.2, 2.45, 2.62, 2.82]) { + const effects = new Effects3D(new THREE.Scene()); + const start = new THREE.Vector3(x, 1.55, -2.7); + effects.decapitate(0, start, new THREE.Quaternion(), 1, 8); + effects.dismemberHand(0, "left", start, new THREE.Quaternion(), 1, 9, 0x1d4ed8); + const head = new THREE.Vector3(); + const before = start.clone(); + for (let frame = 0; frame < 360; frame += 1) { + effects.update(1 / 60); + effects.severedHeadPosition(0, head); + expect(Math.hypot(head.x - before.x, head.z - before.z)).toBeLessThan(0.08); + before.copy(head); + } + expect(Math.abs(head.x)).toBeLessThan(ROPE_LINE - 0.1); + expect(Math.abs(head.z)).toBeLessThan(ROPE_LINE - 0.1); + effects.dispose(); + } + }); +}); + +describe("blood in the air", () => { + const hit = { event_id: 4, tick: 10, kind: "hit", actor_id: "one", target_id: "two", amount: 300, detail: "right:hook:head", blood: 100, direction: 1, action_id: null }; + const largest = (effects: Effects3D): number => { + const matrix = new THREE.Matrix4(); + const scale = new THREE.Vector3(); + let found = 0; + for (let index = 0; index < effects.dropletMesh.count; index += 1) { + effects.dropletMesh.getMatrixAt(index, matrix); + found = Math.max(found, scale.setFromMatrixScale(matrix).x); + } + return found; + }; + + it("is drawn at half the size or less for a close camera", () => { + const sizes = [6, 1.2].map((distance) => { + const effects = new Effects3D(new THREE.Scene()); + effects.setViewDistance(distance); + effects.addEvent(hit, new THREE.Vector3(0, 1.5, 0), false); + effects.update(1 / 60); + const size = largest(effects); + effects.dispose(); + return size; + }); + expect(sizes[0]).toBeGreaterThan(0.004); + expect(sizes[1]! / sizes[0]!).toBeLessThan(0.5); + expect(sizes[1]! / sizes[0]!).toBeGreaterThan(0.3); + }); + + it("is a drop with a round head and a tail that thins out behind it", () => { + const geometry = buildDropletGeometry(); + const position = geometry.getAttribute("position"); + const vertex = new THREE.Vector3(); + let tail = 0; + let widestBehind = 0; + for (let index = 0; index < position.count; index += 1) { + vertex.fromBufferAttribute(position, index); + if (vertex.y >= 0) expect(vertex.length()).toBeLessThan(1.001); + tail = Math.min(tail, vertex.y); + if (vertex.y < -0.6) widestBehind = Math.max(widestBehind, Math.hypot(vertex.x, vertex.z)); + } + // A short tail: a drop, not a needle. + expect(tail).toBeLessThan(-0.9); + expect(tail).toBeGreaterThan(-1.3); + expect(widestBehind).toBeGreaterThan(0.1); + expect(widestBehind).toBeLessThan(0.62); + geometry.dispose(); + }); + + it("is smeared along its path and thinner for it, holding its volume", () => { + const still = dropletShape(0.01, 0, { width: 0, length: 0 }); + expect(still.width).toBeCloseTo(0.01, 6); + expect(still.length).toBeCloseTo(0.01, 6); + let last = { ...still }; + for (const speed of [0.5, 1.5, 2.5, 3.4]) { + const shape = dropletShape(0.01, speed, { width: 0, length: 0 }); + expect(shape.length).toBeGreaterThan(last.length + 0.001); + expect(shape.width).toBeLessThan(last.width - 0.0003); + expect(shape.width * shape.width * shape.length).toBeCloseTo(1e-6, 9); + last = { ...shape }; + } + // However fast, a drop stays a drop: no longer than a little over twice its size. + const fastest = dropletShape(0.01, 40, { width: 0, length: 0 }); + expect(fastest.length).toBeLessThan(0.025); + expect(fastest.width).toBeGreaterThan(0.0065); + }); + + it("is dark red, never pink", () => { + for (const pick of [0, 0.34, 0.67, 0.999, 1, -1]) { + const shade = bloodShade(pick); + expect(BLOOD_SHADES).toContain(shade); + expect(shade.r).toBeLessThan(0.45); + expect(shade.g).toBeLessThan(shade.r * 0.05); + expect(shade.b).toBeLessThan(shade.r * 0.05); + } + expect(bloodShade(0).r).toBeGreaterThan(bloodShade(0.999).r * 2); + }); + + const bloodSlots = (effects: Effects3D): number[] => { + const colour = effects.dropletBuffers.color; + const position = effects.dropletBuffers.position; + const slots: number[] = []; + for (let index = 0; index < colour.count; index += 1) if (position.getY(index) > -10 && colour.getY(index) < 0.2) slots.push(index); + return slots; + }; + const dropAt = (effects: Effects3D, slot: number): THREE.Vector3 => new THREE.Vector3().fromBufferAttribute(effects.dropletBuffers.position, slot); + + it("leaves in strands: two drops trail each leader along its path, slower and smaller", () => { + const effects = new Effects3D(new THREE.Scene()); + effects.addEvent(hit, new THREE.Vector3(0, 1.5, 0), false); + const slots = bloodSlots(effects); + expect(slots.length).toBe(140); + const origin = slots.map((slot) => dropAt(effects, slot)); + for (let frame = 0; frame < 9; frame += 1) effects.update(1 / 60); + const matrix = new THREE.Matrix4(); + const size = (slot: number): number => { + effects.dropletMesh.getMatrixAt(slot, matrix); + const scale = new THREE.Vector3().setFromMatrixScale(matrix); + return scale.x * scale.x * scale.y; + }; + let strands = 0; + for (let lead = 0; lead + 2 < slots.length; lead += 5) { + const head = dropAt(effects, slots[lead]!); + const path = head.clone().sub(origin[lead]!); + expect(origin[lead + 1]!.distanceTo(origin[lead]!)).toBeLessThan(1e-6); + expect(origin[lead + 2]!.distanceTo(origin[lead]!)).toBeLessThan(1e-6); + const first = head.clone().sub(dropAt(effects, slots[lead + 1]!)); + const second = head.clone().sub(dropAt(effects, slots[lead + 2]!)); + expect(first.length()).toBeGreaterThan(0.01); + expect(second.length()).toBeGreaterThan(first.length() * 1.8); + expect(first.clone().cross(second).length()).toBeLessThan(1e-5); + expect(path.length()).toBeGreaterThan(0.1); + expect(size(slots[lead + 1]!)).toBeLessThan(size(slots[lead]!)); + expect(size(slots[lead + 2]!)).toBeLessThan(size(slots[lead + 1]!)); + strands += 1; + } + expect(strands).toBe(28); + const free = dropAt(effects, slots[3]!).sub(dropAt(effects, slots[0]!)); + const trailing = dropAt(effects, slots[1]!).sub(dropAt(effects, slots[0]!)); + expect(free.clone().cross(trailing).length()).toBeGreaterThan(1e-4); + effects.dispose(); + }); + + it("falls as fast as anything else does", () => { + const effects = new Effects3D(new THREE.Scene()); + effects.addEvent(hit, new THREE.Vector3(0, 1.5, 0), false); + const slot = bloodSlots(effects)[0]!; + const heights: number[] = []; + for (let frame = 0; frame < 3; frame += 1) { + effects.update(1 / 60); + heights.push(dropAt(effects, slot).y); + } + const fall = heights[2]! - 2 * heights[1]! + heights[0]!; + expect(fall * 3600).toBeCloseTo(-9.81, 2); + effects.dispose(); + }); + + it("thins out as the mist hangs, until it is gone", () => { + const effects = new Effects3D(new THREE.Scene()); + effects.addEvent(hit, new THREE.Vector3(0, 1.5, 0), false); + const material = effects.mistPoints.material as THREE.PointsMaterial; + const colour = effects.mistPoints.geometry.getAttribute("color"); + expect(material.vertexColors).toBe(true); + expect(colour.itemSize).toBe(4); + const thickest = (): number => { + let alpha = 0; + const position = effects.mistPoints.geometry.getAttribute("position"); + for (let index = 0; index < colour.count; index += 1) if (position.getY(index) > -10) alpha = Math.max(alpha, colour.getW(index)); + return alpha; + }; + expect(effects.liveMist).toBeGreaterThan(0); + effects.update(1 / 60); + const early = thickest(); + for (let frame = 0; frame < 20; frame += 1) effects.update(1 / 60); + const later = thickest(); + expect(early).toBeGreaterThan(0.5); + expect(later).toBeLessThan(early - 0.15); + for (let frame = 0; frame < 120; frame += 1) effects.update(1 / 60); + expect(effects.liveMist).toBe(0); + effects.dispose(); + }); +}); + +describe("severed head", () => { + it("stays inside the ropes however hard it is launched", () => { + const effects = new Effects3D(new THREE.Scene()); + effects.decapitate(0, new THREE.Vector3(2.2, 1.5, -2.2), new THREE.Quaternion(), 1, 9); + const at = new THREE.Vector3(); + for (let frame = 0; frame < 600; frame += 1) { + effects.update(1 / 60); + expect(effects.severedHeadPosition(0, at)).toBe(true); + expect(Math.abs(at.x)).toBeLessThan(ROPE_LINE - 0.12); + expect(Math.abs(at.z)).toBeLessThan(ROPE_LINE - 0.12); + } + effects.dispose(); + }); + + it("reports which way the face points", () => { + const effects = new Effects3D(new THREE.Scene()); + const facing = new THREE.Vector3(); + expect(effects.severedHeadFacing(0, facing)).toBe(false); + effects.decapitate(0, new THREE.Vector3(0, 1.5, 0), new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2), 1, 11); + expect(effects.severedHeadFacing(0, facing)).toBe(true); + expect(facing.x).toBeCloseTo(1, 5); + expect(facing.z).toBeCloseTo(0, 5); + effects.dispose(); + }); +}); + +describe("flesh over a cut", () => { + it("fans from the raised middle to every edge and faces the way it is told", () => { + const geometry = new THREE.BufferGeometry(); + const square = [[1, 0, 1], [-1, 0, 1], [-1, 0, -1], [1, 0, -1]]; + const rim = new Float32Array(square.flatMap((corner, index) => [...corner, ...square[(index + 1) % 4]!]).map((value, index) => value + [5, 2, -3][index % 3]!)); + for (const outward of [new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, -1, 0)]) { + closeCut(geometry, rim, new THREE.Vector3(5, 2, -3), outward, 0.25); + const position = geometry.getAttribute("position"); + const normal = geometry.getAttribute("normal"); + const uv = geometry.getAttribute("uv"); + expect(position.count).toBe(12); + for (let corner = 0; corner < 12; corner += 3) { + expect([position.getX(corner), position.getY(corner), position.getZ(corner)]).toEqual([0, 0.25 * outward.y, 0]); + expect([uv.getX(corner), uv.getY(corner)]).toEqual([0.5, 0.5]); + expect(Math.hypot(position.getX(corner + 1), position.getZ(corner + 1))).toBeCloseTo(Math.SQRT2, 6); + expect(Math.hypot(uv.getX(corner + 1) - 0.5, uv.getY(corner + 1) - 0.5)).toBeCloseTo(0.5, 6); + } + for (let index = 0; index < normal.count; index += 1) expect(normal.getY(index) * outward.y).toBeGreaterThan(0.5); + // Wound the right way round to be seen from outside. + for (let corner = 0; corner < 12; corner += 3) { + const [a, b, c] = [0, 1, 2].map((offset) => new THREE.Vector3().fromBufferAttribute(position, corner + offset)); + expect(b!.sub(a!).cross(c!.sub(a!)).dot(outward)).toBeGreaterThan(0); + } + } + geometry.dispose(); + }); + + it("reuses its buffers from frame to frame", () => { + const geometry = new THREE.BufferGeometry(); + const rim = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 1, -1, 0, 0, -1, 0, 0, 1, 0, 0]); + closeCut(geometry, rim, new THREE.Vector3(), new THREE.Vector3(0, 1, 0)); + const first = geometry.getAttribute("position"); + closeCut(geometry, rim.map((value) => value * 2), new THREE.Vector3(), new THREE.Vector3(0, 1, 0)); + expect(geometry.getAttribute("position")).toBe(first); + expect(Math.hypot(first.getX(1), first.getY(1), first.getZ(1))).toBe(2); + geometry.dispose(); + }); +}); + +describe("referee and a head on the canvas", () => { + it("walks round it rather than standing over it", () => { + const effects = new Effects3D(new THREE.Scene()); + effects.decapitate(1, new THREE.Vector3(0.2, 0.2, -1.4), new THREE.Quaternion(), 1, 8); + for (let frame = 0; frame < 240; frame += 1) effects.update(1 / 60); + const head = new THREE.Vector3(); + effects.severedHeadPosition(1, head); + const updated: unknown[] = []; + const stub = { + referee: { setRefereeCount: () => {}, aimBreak: () => {}, update: (...frame: unknown[]) => updated.push(frame), boxer: { root: new THREE.Object3D() } }, blobShadows: [], + mapping: worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }), + tmpA: new THREE.Vector3(head.x - 0.4, 0, head.z + 2.2), tmpB: new THREE.Vector3(head.x + 0.4, 0, head.z + 2.2), + refereePosition: new THREE.Vector3(head.x + 0.1, 0, head.z - 0.2), refereeVelocity: new THREE.Vector3(), refereeAway: new THREE.Vector3(), refereeYaw: 0, + effects, closeUpTarget: new THREE.Vector3(), replay: null, ceremony: null, + }; + const step = (FightRenderer.prototype as unknown as { updateReferee: (dt: number, time: number, snapshot: unknown, tick: number) => void }).updateReferee; + for (let frame = 0; frame < 180; frame += 1) { + step.call(stub, 1 / 60, frame / 60, null, frame / 2); + expect(Math.hypot(stub.refereePosition.x - head.x, stub.refereePosition.z - head.z)).toBeGreaterThanOrEqual(0.9 - 1e-6); + } + expect(updated).toHaveLength(180); + effects.dispose(); + }); + + it("is left out of the knockout replay's close shot, and back for the live picture", () => { + const root = new THREE.Object3D(); + const shadow = new THREE.Object3D(); + const stub = { + referee: { setRefereeCount: () => {}, aimBreak: () => {}, update: () => {}, boxer: { root } }, blobShadows: [new THREE.Object3D(), new THREE.Object3D(), shadow], + mapping: worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }), + tmpA: new THREE.Vector3(-0.5, 0, 0), tmpB: new THREE.Vector3(0.5, 0, 0), + refereePosition: new THREE.Vector3(0, 0, -1.5), refereeVelocity: new THREE.Vector3(), refereeAway: new THREE.Vector3(), refereeYaw: 0, + effects: { severedHeadPosition: () => false }, closeUpTarget: new THREE.Vector3(), replay: {} as unknown, ceremony: null, + }; + const step = (FightRenderer.prototype as unknown as { updateReferee: (dt: number, time: number, snapshot: unknown, tick: number) => void }).updateReferee; + step.call(stub, 1 / 60, 0, null, 0); + expect(root.visible).toBe(false); + expect(shadow.visible).toBe(false); + stub.replay = null; + step.call(stub, 1 / 60, 1 / 60, null, 0.5); + expect(root.visible).toBe(true); + expect(shadow.visible).toBe(true); + }); + + it("steps back from it and is left alone when already clear", () => { + expect(keepClear(0.3, 0, 0, 0, 0.9)).toEqual({ x: 0.9, z: 0 }); + const stepped = keepClear(0.3, -0.4, 0, 0, 0.9); + expect(Math.hypot(stepped.x, stepped.z)).toBeCloseTo(0.9, 9); + expect(stepped.x / stepped.z).toBeCloseTo(0.3 / -0.4, 9); + expect(keepClear(1.2, 0.4, 0, 0, 0.9)).toEqual({ x: 1.2, z: 0.4 }); + expect(keepClear(2, 1, 2, 1, 0.9).x).toBe(2); + expect(keepClear(2, 1, 2, 1, 0.9).z).toBeCloseTo(0.1, 9); + }); +}); + +describe("the winner beside the beaten fighter", () => { + const renderer = FightRenderer.prototype as unknown as { + standApart(this: unknown, fighters: readonly [FighterSnapshot, FighterSnapshot], dt: number): readonly [FighterSnapshot, FighterSnapshot]; + }; + const lying = [new THREE.Vector3(0.3, 0.1, 0.2), new THREE.Vector3(0.1, 0.2, 0.25), new THREE.Vector3(-0.2, 0.15, 0.3), new THREE.Vector3(-0.5, 0.1, 0.2), new THREE.Vector3(-0.5, 0.1, 0.4)]; + const rig = (replay: unknown) => Object.assign(Object.create(FightRenderer.prototype) as object, { + graphs: [{ fallBody: null }, { fallBody: { bodyPoint: (index: number, out: THREE.Vector3) => out.copy(lying[index]!) } }], + mapping: worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }), replay, bodyPoint: new THREE.Vector3(), + drawnFighters: [{ ...fighter("one") }, { ...fighter("two") }], drawnOffsets: [{ x: 0, y: 0 }, { x: 0, y: 0 }], drawnTargets: [{ x: 0, y: 0 }, { x: 0, y: 0 }], + }); + + it("stands clear of the body where the fall left it, never inside it", () => { + const stub = rig(null); + const winner = { ...fighter("one"), x: 0, y: -40 }; + const loser = { ...fighter("two"), x: 0, y: -40, is_downed: true }; + const [drawn, beaten] = renderer.standApart.call(stub, [winner, loser], 1 / 60); + const at = new THREE.Vector3(stub.mapping.x(drawn.x), 0, stub.mapping.z(drawn.y)); + for (const point of lying) expect(Math.hypot(at.x - point.x, at.z - point.z)).toBeGreaterThanOrEqual(0.5 - 1e-6); + expect(beaten).toBe(loser); + const far = { ...fighter("one"), x: 300, y: 200 }; + expect(renderer.standApart.call(rig(null), [far, loser], 1 / 60)[0]).toBe(far); + }); + + it("stands off the body in the replay as he did live", () => { + // The live fall's obstacle was the pushed winner, so the replayed fall lies where he would stand in it unpushed. + const winner = { ...fighter("one"), x: 0, y: -40 }; + const loser = { ...fighter("two"), x: 0, y: -40, is_downed: true }; + const live = { ...renderer.standApart.call(rig(null), [winner, loser], 1 / 60)[0] }; + const replayed = renderer.standApart.call(rig({}), [winner, loser], 1 / 60)[0]; + expect(replayed).not.toBe(winner); + expect({ x: replayed.x, y: replayed.y }).toEqual({ x: live.x, y: live.y }); + }); +}); + +describe("severed head close-up camera", () => { + const shoot = (eventId: number, start = new THREE.Vector3(0.4, 1.55, 0.1), referee = new THREE.Vector3(-0.6, 0, -1.9)): { chosen: (number | null)[]; steps: number[]; first: number } => { + const effects = new Effects3D(new THREE.Scene()); + effects.decapitate(1, start, new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2), 1, eventId); + const stub = { + finishCloseUpUntil: 101.7, finishCloseUpIndex: 1, finishCloseUpBearing: null as number | null, headCacheValid: [true, true], + headCache: [new THREE.Vector3(), new THREE.Vector3(0.4, 0.3, 0.1)], arcadeInjuries: [null, "decapitation"], effects, + closeUpTarget: new THREE.Vector3(), closeUpFacing: new THREE.Vector3(), closeUpPosition: new THREE.Vector3(), replayLookAt: new THREE.Vector3(), + tmpA: new THREE.Vector3(-1.6, 0, -1.2), tmpB: new THREE.Vector3(0.4, 0, 0.1), refereePosition: referee, + }; + const chosen: (number | null)[] = []; + const steps: number[] = []; + const previous = new THREE.Vector3(); + let first = 0; + for (let frame = 0; frame < 100; frame += 1) { + effects.update(1 / 60); + const shot = renderer.closeUpFrame.call(stub, 100 + frame / 60)!; + chosen.push(stub.finishCloseUpBearing); + if (frame === 0) first = Math.atan2(shot.position.x - shot.lookAt.x, shot.position.z - shot.lookAt.z); + else steps.push(shot.position.distanceTo(previous)); + previous.copy(shot.position); + } + effects.dispose(); + return { chosen, steps, first }; + }; + + it("starts on the side the face points to", () => { + const shot = shoot(8); + expect(shot.chosen[0]).toBeCloseTo(Math.PI / 2, 1); + expect(shot.first).toBeCloseTo(Math.PI / 2 - 0.2, 1); + }); + + it("takes another side when the referee stands where the face points", () => { + const open = shoot(8); + const taken = shoot(8, new THREE.Vector3(0.4, 1.55, 0.1), new THREE.Vector3(0.4 + 0.75, 0, 0.1)); + expect(Math.abs(taken.chosen[0]! - open.chosen[0]!)).toBeGreaterThan(0.5); + }); + + it("keeps to that side while the head tumbles, and never jumps", () => { + for (let eventId = 2; eventId <= 60; eventId += 2) { + for (const start of [new THREE.Vector3(0.4, 1.55, 0.1), new THREE.Vector3(2.1, 1.55, -2.1)]) { + const shot = shoot(eventId, start); + expect(new Set(shot.chosen).size).toBe(1); + expect(Math.max(...shot.steps)).toBeLessThan(0.12); + } + } + }); +}); + +describe("severed head close-up", () => { + const fallback = 2.5; + + it("shoots from the side the face points to", () => { + expect(closeUpAngle(0.5, 0.2, { x: 1, y: 0, z: 0 }, 0.8, 2.2, fallback)).toBeCloseTo(Math.PI / 2, 6); + expect(closeUpAngle(0.5, 0.2, { x: 0, y: 0.2, z: -1 }, 0.8, 2.2, fallback)).toBeCloseTo(Math.PI, 6); + }); + + it("falls back when the face points up or down, or into the ropes", () => { + expect(closeUpAngle(0.5, 0.2, { x: 0.1, y: 0.99, z: 0.1 }, 0.8, 2.2, fallback)).toBe(fallback); + expect(closeUpAngle(2.1, 0, { x: 1, y: 0, z: 0 }, 0.8, 2.2, fallback)).toBe(fallback); + expect(closeUpAngle(0.5, 0.2, null, 0.8, 2.2, fallback)).toBe(fallback); + }); + + it("passes over a side where someone is standing or stands in the way", () => { + const inTheShot = [{ x: 0.5 + 0.8, z: 0.2, radius: 0.5 }]; + expect(closeUpAngle(0.5, 0.2, { x: 1, y: 0, z: 0 }, 0.8, 2.2, fallback, inTheShot)).toBe(fallback); + const onTheWay = [{ x: 0.5 + 0.5, z: 0.45, radius: 0.5 }]; + expect(closeUpAngle(0.5, 0.2, { x: 1, y: 0, z: 0 }, 0.8, 2.2, fallback, onTheWay)).toBe(fallback); + const both = [...inTheShot, { x: 0.5 + Math.sin(fallback) * 0.8, z: 0.2 + Math.cos(fallback) * 0.8, radius: 0.5 }]; + const chosen = closeUpAngle(0.5, 0.2, { x: 1, y: 0, z: 0 }, 0.8, 2.2, fallback, both); + expect(chosen).toBeCloseTo(fallback + 1.05, 9); + for (const blocker of both) expect(Math.hypot(0.5 + Math.sin(chosen) * 0.8 - blocker.x, 0.2 + Math.cos(chosen) * 0.8 - blocker.z)).toBeGreaterThan(0.5); + }); + + it("looks past someone standing over the head from the far side of them", () => { + const over = [{ x: 0.5, z: 0.2 - 0.3, radius: 0.5 }]; + const chosen = closeUpAngle(0.5, 0.2, { x: 0, y: 0, z: -1 }, 0.8, 2.2, 0, over); + expect(Math.cos(chosen)).toBeGreaterThan(0.4); + }); + + it("keeps the fallback when every side is taken", () => { + const ringed = Array.from({ length: 12 }, (_, index) => ({ x: 0.5 + Math.sin(index * 0.5236) * 0.8, z: 0.2 + Math.cos(index * 0.5236) * 0.8, radius: 0.5 })); + expect(closeUpAngle(0.5, 0.2, { x: 1, y: 0, z: 0 }, 0.8, 2.2, fallback, ringed)).toBe(fallback); + }); +}); + +describe("gore from a real punch", () => { + // Engine damage: a landed jab is about 30, a straight 50 to 80, a power counter up to about 150. + it("knocks teeth out with the punch that floors a man and with big counters, never with an ordinary shot", () => { + expect(teethFor("knockdown", 1, true)).toBe(2); + expect(teethFor("counter_hit", BIG_SHOT, true)).toBe(1); + expect(teethFor("counter_hit", 130, true)).toBe(2); + expect(teethFor("counter_hit", BIG_SHOT - 1, true)).toBe(0); + expect(teethFor("hit", 80, true)).toBe(0); + expect(teethFor("hit", 118, true)).toBe(1); + expect(teethFor("counter_hit", 140, false)).toBe(0); + expect(teethFor("knockdown", 1, false)).toBe(0); + expect(teethFor("block", 140, true)).toBe(0); + }); + + it("throws more blood from an open wound and from a harder punch, up to a limit", () => { + expect(bloodDropsFor(8, 30)).toBeLessThan(15); + expect(bloodDropsFor(25, 110)).toBeGreaterThan(bloodDropsFor(25, 50) + 25); + expect(bloodDropsFor(60, 60)).toBeGreaterThan(bloodDropsFor(20, 60) + 50); + expect(bloodDropsFor(100, 500)).toBe(140); + expect(bloodDropsFor(0, 0)).toBe(0); + }); + + it("puts a mist in the air from a hard shot, which an ordinary one does not", () => { + const origin = new THREE.Vector3(); + const mist = (kind: string, amount: number): number => { + const effects = new Effects3D(new THREE.Scene()); + effects.addEvent({ event_id: 3, tick: 1, kind, actor_id: "one", target_id: "two", amount, detail: "straight:head", blood: 20, direction: 1, action_id: null }, origin, false); + const live = effects.liveMist; + effects.dispose(); + return live; + }; + expect(mist("hit", HARD_SHOT)).toBeGreaterThan(0); + expect(mist("hit", HARD_SHOT - 1)).toBe(0); + expect(mist("counter_hit", 40)).toBeGreaterThan(0); + }); + + // The engine's blood is the cut's bleeding over eight plus the damage over four. + const punchOn = (kind: string, amount: number, bleeding: number, detail = "straight:head"): CombatEvent => ({ + event_id: 7, tick: 1, kind, actor_id: "one", target_id: "two", amount, detail, blood: Math.floor(bleeding / 8) + Math.floor(amount / 4), direction: 1, action_id: null, + }); + const origin = new THREE.Vector3(); + const after = (event: CombatEvent, read: (effects: Effects3D) => T): T => { + const effects = new Effects3D(new THREE.Scene(), 256); + effects.addEvent(event, origin, false); + const value = read(effects); + effects.dispose(); + return value; + }; + /** Stains on the canvas once everything the event threw has come down. */ + const landed = (effects: Effects3D): number => { + for (let frame = 0; frame < 150; frame += 1) effects.update(1 / 60); + return effects.canvasStains; + }; + + it("splashes the canvas from an open cut and a knockdown, not from every punch that draws a little blood", () => { + const splash = (event: CombatEvent): number => after(event, (effects) => effects.canvasStains); + expect(splash(punchOn("hit", 34, 0, "jab:head"))).toBe(0); + expect(splash(punchOn("hit", 110, 0))).toBe(0); + expect(splash(punchOn("hit", 64, 130, "hook:head"))).toBe(4); + expect(splash(punchOn("counter_hit", 64, 320, "hook:head"))).toBe(8); + expect(splash({ ...punchOn("knockdown", 1, 0), blood: 18 })).toBe(12); + }); + + it("throws less blood from a punch the guard took than from a clean jab, however hard it hit the guard", () => { + // A block carries the guard's damage as its amount and the blood that leaked through it. + const jab = punchOn("hit", 34, 0, "jab:head"); + const jabDrops = after(jab, (effects) => effects.liveBloodParticles); + const jabStains = after(jab, landed); + for (const guard of [57, 76, 120]) { + const blocked = { ...punchOn("block", guard, 0, "hook:head"), blood: 7 }; + expect(after(blocked, (effects) => effects.liveBloodParticles)).toBeLessThan(jabDrops); + expect(after(blocked, (effects) => effects.canvasStains)).toBe(0); + expect(after(blocked, landed)).toBeLessThanOrEqual(jabStains); + } + }); + + it("adds a few drops to a cut's drip at each beat of the bleeding, with no splash, sweat or shake", () => { + const beat: CombatEvent = { event_id: 9, tick: 30, kind: "bleed", actor_id: "two", target_id: null, amount: 30, detail: "", blood: 25, direction: 0, action_id: null }; + const effects = new Effects3D(new THREE.Scene(), 256); + effects.addEvent(beat, origin, false); + expect(effects.liveParticles).toBe(effects.liveBloodParticles); + expect(effects.liveBloodParticles).toBeGreaterThan(0); + expect(effects.liveBloodParticles).toBeLessThanOrEqual(5); + expect(effects.canvasStains).toBe(0); + expect(effects.shakeAmount).toBe(0); + expect(landed(effects)).toBeLessThanOrEqual(5); + effects.dispose(); + }); + + it("tears flesh from an open cut with a punch that rocks him, and from any face with a big counter", () => { + const torn = (event: CombatEvent): number => after(event, (effects) => effects.liveGibs); + expect(torn(punchOn("hit", HARD_SHOT, 130))).toBeGreaterThan(0); + expect(torn(punchOn("counter_hit", ROCKING_COUNTER, 130))).toBeGreaterThan(0); + expect(torn(punchOn("hit", HARD_SHOT - 1, 130))).toBe(0); + expect(torn(punchOn("counter_hit", ROCKING_COUNTER - 1, 130))).toBe(0); + // A cut that has barely opened, and an unmarked face, keep their flesh but for a big counter. + expect(torn(punchOn("hit", 110, 40))).toBe(0); + expect(torn(punchOn("counter_hit", BIG_SHOT, 0))).toBeGreaterThan(0); + expect(torn(punchOn("hit", 120, 300, "hook:body"))).toBe(0); + expect(torn(punchOn("counter_hit", 140, 300))).toBeGreaterThan(torn(punchOn("counter_hit", 60, 300))); + }); + + it("keeps a knockdown, teeth and gum shield included, to one reduced spray of blood with reduced blood", () => { + const thrown = (level: "full" | "reduced"): number => { + const effects = new Effects3D(new THREE.Scene(), 256); + effects.setBloodLevel(level); + const mouth = new THREE.Vector3(0, 1.5, 0); + effects.addEvent({ ...punchOn("knockdown", 1, 0), blood: 30 }, origin, false); + effects.spawnTeeth(mouth, 1, 2, 7); + effects.ejectMouthpiece(1, mouth, new THREE.Quaternion(), 1, 7, 0x1d4ed8); + // Nothing thrown has come down yet a third of a second on, while the gum shield trails its thread. + let most = effects.liveBloodParticles; + for (let frame = 0; frame < 18; frame += 1) { + effects.update(1 / 60); + most = Math.max(most, effects.liveBloodParticles); + } + effects.dispose(); + return most; + }; + // Reduced caps a single spray at 24 drops. + expect(thrown("reduced")).toBeLessThanOrEqual(24); + expect(thrown("full")).toBeGreaterThan(60); + }); + + it("throws a thicker mist off a punch the worse the cut it lands on", () => { + const mist = (event: CombatEvent): number => after(event, (effects) => effects.liveMist); + expect(mist(punchOn("hit", 75, 0))).toBe(10); + expect(mist(punchOn("hit", 75, 300))).toBeGreaterThan(mist(punchOn("hit", 75, 80))); + expect(mist(punchOn("hit", 75, 600))).toBe(18); + // A face cut badly enough mists at every punch, a jab included. + expect(mist(punchOn("hit", 34, 0, "jab:head"))).toBe(0); + expect(mist(punchOn("hit", 34, 160, "jab:head"))).toBeGreaterThan(0); + }); +}); diff --git a/web/hands/src/render/gore.ts b/web/hands/src/render/gore.ts new file mode 100644 index 00000000..ed18bde8 --- /dev/null +++ b/web/hands/src/render/gore.ts @@ -0,0 +1,487 @@ +import * as THREE from "three"; +import { mergeVertices } from "three/examples/jsm/utils/BufferGeometryUtils.js"; +import manifestJson from "../../../../src/intelstream/hands/combat-manifest.json"; + +/** A lump of torn flesh: an irregular, smooth, slightly flattened blob about 6 cm across before scaling. */ +export function buildChunkGeometry(): THREE.BufferGeometry { + const source = new THREE.IcosahedronGeometry(1, 2); + source.deleteAttribute("normal"); + source.deleteAttribute("uv"); + const geometry = mergeVertices(source, 1e-4); + source.dispose(); + const position = geometry.getAttribute("position"); + const vertex = new THREE.Vector3(); + for (let index = 0; index < position.count; index += 1) { + vertex.fromBufferAttribute(position, index).normalize(); + const lump = 1 + + 0.3 * Math.sin(3.1 * vertex.x + 1.3) * Math.sin(2.7 * vertex.y + 0.7) + + 0.2 * Math.sin(5.3 * vertex.z + 2.1) * Math.sin(4.1 * vertex.x - 0.4) + + 0.12 * Math.sin(7.7 * vertex.y + 0.9); + vertex.multiplyScalar(0.03 * lump); + position.setXYZ(index, vertex.x, vertex.y * 0.62, vertex.z); + } + geometry.computeVertexNormals(); + return geometry; +} + +/** + * Engine damage of a landed punch: most deal 30 to 80, a hard shot 70 or more and a big counter + * 95 or more (the largest, a power counter, about 150). A knockdown event carries the count, not damage. + */ +export const HARD_SHOT = 70; +export const BIG_SHOT = 95; +/** A counter this heavy rocks a man, as the engine reckons it, as a hard shot does. */ +export const ROCKING_COUNTER = manifestJson.stun.rocked_counter_damage; + +/** Teeth a blow to the head knocks out: two more as the punch floors a man, one or two by a big counter or a huge shot, none otherwise. */ +export function teethFor(kind: string, amount: number, head: boolean): number { + if (!head) return 0; + if (kind === "knockdown") return 2; + if (kind === "counter_hit" && amount >= BIG_SHOT) return amount >= 120 ? 2 : 1; + if (kind === "hit" && amount >= 115) return 1; + return 0; +} + +/** Drops of blood a blow throws at full blood: more from an open wound, and more from a harder punch. */ +export function bloodDropsFor(blood: number, amount: number): number { + return Math.min(140, Math.round(Math.max(0, blood) * 1.4 + Math.max(0, amount - 50) * 0.5)); +} + +const DROPLET_TAIL = 1.1; +const DROPLET_TAIL_TAPER = 0.6; +const DROPLET_STRETCH_RATE = 0.35; +const DROPLET_MAX_STRETCH = 1.2; + +/** A drop in flight, travelling along +Y: a round head of unit radius and a tail that thins out behind it. */ +export function buildDropletGeometry(): THREE.BufferGeometry { + const geometry = new THREE.SphereGeometry(1, 8, 6); + const position = geometry.getAttribute("position"); + for (let index = 0; index < position.count; index += 1) { + const back = -position.getY(index); + if (back <= 0) continue; + const thin = 1 - DROPLET_TAIL_TAPER * back; + position.setXYZ(index, position.getX(index) * thin, -back * DROPLET_TAIL, position.getZ(index) * thin); + } + geometry.computeVertexNormals(); + return geometry; +} + +/** + * How a drop of `radius` moving at `speed` is drawn: smeared along its path as a camera would catch it, + * and thinner the longer the smear, so a fast drop is a streak and not a bigger drop. + */ +export function dropletShape(radius: number, speed: number, out: { width: number; length: number }): { width: number; length: number } { + const stretch = 1 + Math.min(DROPLET_MAX_STRETCH, Math.max(0, speed) * DROPLET_STRETCH_RATE); + out.width = radius / Math.sqrt(stretch); + out.length = radius * stretch; + return out; +} + +/** Blood in the air, from freshly lit to nearly black. Linear colour. */ +export const BLOOD_SHADES = [ + { r: 0.4, g: 0.012, b: 0.016 }, + { r: 0.26, g: 0.006, b: 0.01 }, + { r: 0.15, g: 0.003, b: 0.006 }, +] as const; + +export function bloodShade(pick: number): { r: number; g: number; b: number } { + return BLOOD_SHADES[Math.min(BLOOD_SHADES.length - 1, Math.max(0, Math.floor(pick * BLOOD_SHADES.length)))]!; +} + +/** + * The exposed surface of a cut through a limb or the neck: an ellipse in the XZ plane facing +Y, + * level with the skin at the rim and rising to uneven flesh in the middle. + */ +export function buildWoundGeometry(radiusX: number, radiusZ: number, rise = 0.012): THREE.BufferGeometry { + const rings = 6; + const segments = 28; + const positions: number[] = [0, rise, 0]; + const uvs: number[] = [0.5, 0.5]; + const index: number[] = []; + for (let ring = 1; ring <= rings; ring += 1) { + const reach = ring / rings; + for (let segment = 0; segment < segments; segment += 1) { + const angle = (segment / segments) * Math.PI * 2; + const lump = Math.sin(angle * 3 + reach * 5) * Math.sin(angle * 7 + 1.3); + positions.push(Math.cos(angle) * reach * radiusX, rise * (1 - reach * reach) + rise * 0.3 * lump * (1 - reach), Math.sin(angle) * reach * radiusZ); + uvs.push(0.5 + 0.5 * reach * Math.cos(angle), 0.5 + 0.5 * reach * Math.sin(angle)); + } + } + const at = (ring: number, segment: number): number => 1 + (ring - 1) * segments + (segment % segments); + for (let segment = 0; segment < segments; segment += 1) index.push(0, at(1, segment + 1), at(1, segment)); + for (let ring = 1; ring < rings; ring += 1) { + for (let segment = 0; segment < segments; segment += 1) { + index.push(at(ring, segment), at(ring, segment + 1), at(ring + 1, segment)); + index.push(at(ring, segment + 1), at(ring + 1, segment + 1), at(ring + 1, segment)); + } + } + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); + geometry.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2)); + geometry.setIndex(index); + geometry.computeVertexNormals(); + return geometry; +} + +/** + * The open edge a cut leaves, as the two vertices of each of its edges in turn: the edges of the + * kept triangles that the whole mesh closes. Vertices that share a position count as one, so a seam + * is not an edge. + */ +export function cutRim(geometry: THREE.BufferGeometry, keep: (position: THREE.Vector3) => boolean): number[] { + const position = geometry.getAttribute("position"); + const index = geometry.getIndex(); + if (index === null) return []; + const first = new Map(); + const shared = new Int32Array(position.count); + const kept = new Uint8Array(position.count); + const vertex = new THREE.Vector3(); + for (let at = 0; at < position.count; at += 1) { + vertex.fromBufferAttribute(position, at); + const key = `${Math.round(vertex.x * 1000)},${Math.round(vertex.y * 1000)},${Math.round(vertex.z * 1000)}`; + const known = first.get(key); + if (known === undefined) first.set(key, at); + shared[at] = known ?? at; + kept[at] = keep(vertex) ? 1 : 0; + } + // Per edge: how many triangles of the whole mesh use it, and how many of the kept ones. + const edges = new Map(); + for (let at = 0; at < index.count; at += 3) { + const corners = [index.getX(at), index.getX(at + 1), index.getX(at + 2)]; + const whole = corners.every((corner) => kept[corner] === 1); + for (let edge = 0; edge < 3; edge += 1) { + const a = shared[corners[edge]!]!; + const b = shared[corners[(edge + 1) % 3]!]!; + const key = a < b ? a * position.count + b : b * position.count + a; + const uses = edges.get(key) ?? [0, 0]; + uses[0] += 1; + if (whole) uses[1] += 1; + edges.set(key, uses); + } + } + const rim: number[] = []; + for (const [key, [all, held]] of edges) if (held === 1 && all >= 2) rim.push(Math.floor(key / position.count), key % position.count); + return rim; +} + +/** How far the shading of the flesh leans outward at its rim. */ +const CUT_ROUNDING = 0.5; +const cutAcross = new THREE.Vector3(); +const cutAlong = new THREE.Vector3(); +const cutA = new THREE.Vector3(); +const cutB = new THREE.Vector3(); +const cutFace = new THREE.Vector3(); + +/** + * Closes the opening a cut leaves with flesh: a fan from the middle of the cut, raised a little, to + * every edge of its rim, so it meets the skin all the way round. `rim` holds both ends of each edge + * in turn. The surface is written into `geometry`, measured from `centre`, and faces `outward`. The + * texture runs along `across` when it is given, so a wound with a front and back turns with the body. + */ +/** The normal at a corner of a cut's rim when the cut is shaded as one rounded surface, left in `cutFace`. */ +function roundedRimNormal(end: THREE.Vector3, outward: THREE.Vector3, reach: number): void { + cutFace.copy(end).addScaledVector(outward, -end.dot(outward)).multiplyScalar(CUT_ROUNDING / reach).add(outward).normalize(); +} + +export function closeCut(geometry: THREE.BufferGeometry, rim: ArrayLike, centre: THREE.Vector3, outward: THREE.Vector3, rise = 0.01, across?: THREE.Vector3): void { + const edges = Math.floor(rim.length / 6); + let position = geometry.getAttribute("position") as THREE.BufferAttribute | undefined; + let normal = geometry.getAttribute("normal") as THREE.BufferAttribute | undefined; + let uv = geometry.getAttribute("uv") as THREE.BufferAttribute | undefined; + if (position === undefined || normal === undefined || uv === undefined || position.count !== edges * 3) { + position = new THREE.BufferAttribute(new Float32Array(edges * 9), 3).setUsage(THREE.DynamicDrawUsage); + normal = new THREE.BufferAttribute(new Float32Array(edges * 9), 3).setUsage(THREE.DynamicDrawUsage); + uv = new THREE.BufferAttribute(new Float32Array(edges * 6), 2).setUsage(THREE.DynamicDrawUsage); + geometry.setAttribute("position", position); + geometry.setAttribute("normal", normal); + geometry.setAttribute("uv", uv); + } + cutAcross.set(1, 0, 0); + if (Math.abs(outward.x) > 0.9) cutAcross.set(0, 0, 1); + if (across !== undefined && across.lengthSq() > 1e-8 && Math.abs(across.dot(outward)) < 0.95 * across.length()) cutAcross.copy(across); + cutAlong.crossVectors(outward, cutAcross).normalize(); + cutAcross.crossVectors(cutAlong, outward).normalize(); + let reach = 1e-6; + for (let at = 0; at < edges * 6; at += 3) reach = Math.max(reach, Math.hypot(rim[at]! - centre.x, rim[at + 1]! - centre.y, rim[at + 2]! - centre.z)); + for (let edge = 0; edge < edges; edge += 1) { + cutA.set(rim[edge * 6]!, rim[edge * 6 + 1]!, rim[edge * 6 + 2]!).sub(centre); + cutB.set(rim[edge * 6 + 3]!, rim[edge * 6 + 4]!, rim[edge * 6 + 5]!).sub(centre); + const flipped = cutFace.crossVectors(cutA, cutB).dot(outward) < 0; + const first = flipped ? cutB : cutA; + const second = flipped ? cutA : cutB; + position.setXYZ(edge * 3, outward.x * rise, outward.y * rise, outward.z * rise); + position.setXYZ(edge * 3 + 1, first.x, first.y, first.z); + position.setXYZ(edge * 3 + 2, second.x, second.y, second.z); + // Shaded as one rounded surface: the rim of a cut is ragged, and its own faces would glint like an umbrella. + normal.setXYZ(edge * 3, outward.x, outward.y, outward.z); + roundedRimNormal(first, outward, reach); + normal.setXYZ(edge * 3 + 1, cutFace.x, cutFace.y, cutFace.z); + roundedRimNormal(second, outward, reach); + normal.setXYZ(edge * 3 + 2, cutFace.x, cutFace.y, cutFace.z); + uv.setXY(edge * 3, 0.5, 0.5); + uv.setXY(edge * 3 + 1, 0.5 + (0.5 * first.dot(cutAcross)) / reach, 0.5 + (0.5 * first.dot(cutAlong)) / reach); + uv.setXY(edge * 3 + 2, 0.5 + (0.5 * second.dot(cutAcross)) / reach, 0.5 + (0.5 * second.dot(cutAlong)) / reach); + } + position.needsUpdate = true; + normal.needsUpdate = true; + uv.needsUpdate = true; + geometry.computeBoundingSphere(); +} + +/** Cross-section of a neck: muscle around the windpipe, the spine at the back and the great vessels either side. */ +export function woundTexture(): THREE.CanvasTexture { + const size = 128; + const canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext("2d"); + if (ctx !== null) { + const base = ctx.createRadialGradient(64, 64, 6, 64, 64, 64); + base.addColorStop(0, "#6e0d12"); + base.addColorStop(0.7, "#4a070b"); + base.addColorStop(1, "#2a0306"); + ctx.fillStyle = base; + ctx.fillRect(0, 0, size, size); + let seed = 0x9e3779b9; + const rand = (): number => { + seed = Math.imul(seed ^ (seed >>> 15), 1 | seed); + seed ^= seed + Math.imul(seed ^ (seed >>> 7), 61 | seed); + return ((seed ^ (seed >>> 14)) >>> 0) / 4294967296; + }; + for (let bundle = 0; bundle < 46; bundle += 1) { + const angle = rand() * Math.PI * 2; + const distance = 14 + rand() * 42; + ctx.fillStyle = rand() < 0.5 ? "rgba(150,28,30,0.55)" : "rgba(40,4,8,0.5)"; + ctx.beginPath(); + ctx.ellipse(64 + Math.cos(angle) * distance, 64 + Math.sin(angle) * distance, 3 + rand() * 6, 2 + rand() * 3, angle, 0, Math.PI * 2); + ctx.fill(); + } + const disc = (x: number, y: number, radius: number, fill: string): void => { + ctx.fillStyle = fill; + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.fill(); + }; + disc(64, 38, 10, "#8f7c64"); + disc(61, 36, 4.5, "#6a2a25"); + disc(66, 78, 7, "#7a3c34"); + disc(66, 78, 4.5, "#150203"); + disc(41, 64, 3, "#1d0204"); + disc(90, 69, 2.6, "#1d0204"); + for (let fleck = 0; fleck < 70; fleck += 1) { + const angle = rand() * Math.PI * 2; + const distance = rand() * 60; + disc(64 + Math.cos(angle) * distance, 64 + Math.sin(angle) * distance, 0.6 + rand() * 1.6, rand() < 0.4 ? "rgba(190,60,50,0.5)" : "rgba(20,2,4,0.55)"); + } + skinAndFat(ctx); + } + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + return texture; +} + +/** The layers just under the edge of any cut: a rim of skin, then a band of yellow fat over the muscle. */ +function skinAndFat(ctx: CanvasRenderingContext2D): void { + ctx.lineWidth = 7; + ctx.strokeStyle = "rgba(214,178,92,0.85)"; + ctx.beginPath(); + ctx.arc(64, 64, 55, 0, Math.PI * 2); + ctx.stroke(); + ctx.lineWidth = 4; + ctx.strokeStyle = "#b8775c"; + ctx.beginPath(); + ctx.arc(64, 64, 61, 0, Math.PI * 2); + ctx.stroke(); +} + +/** Cross-section of a forearm cut through at the wrist: the two bones with their marrow, tendons and vessels round them. */ +export function wristWoundTexture(): THREE.CanvasTexture { + const size = 128; + const canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext("2d"); + if (ctx !== null) { + const base = ctx.createRadialGradient(64, 64, 6, 64, 64, 64); + base.addColorStop(0, "#741014"); + base.addColorStop(0.7, "#52090d"); + base.addColorStop(1, "#300407"); + ctx.fillStyle = base; + ctx.fillRect(0, 0, size, size); + const disc = (x: number, y: number, radius: number, fill: string): void => { + ctx.fillStyle = fill; + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.fill(); + }; + // Radius and ulna: bone with a ring of hard cortex round the marrow. + for (const [x, y, radius] of [[46, 60, 13], [84, 66, 10]] as const) { + disc(x, y, radius, "#e4d8c0"); + disc(x, y, radius * 0.62, "#a4483c"); + disc(x - radius * 0.2, y - radius * 0.2, radius * 0.25, "rgba(220,110,90,0.6)"); + } + for (let tendon = 0; tendon < 9; tendon += 1) { + const angle = Math.PI * (0.15 + (tendon / 8) * 0.7); + disc(64 + Math.cos(angle) * 34, 64 + Math.sin(angle) * 34, 3.2, "#d9c9b3"); + } + disc(30, 84, 3.4, "#1a0204"); + disc(98, 46, 3, "#1a0204"); + disc(66, 30, 2.6, "#1a0204"); + skinAndFat(ctx); + } + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + return texture; +} + +/** + * A piece of skull blown out of a bursting head: a curved plate of bone with a ragged outline, scalp + * outside and bone and blood inside, about five centimetres across, centred on its middle. Faceted, + * with vertex colours. + */ +export function buildShardGeometry(): THREE.BufferGeometry { + const skull = 0.09; + const thickness = 0.007; + const sides = 9; + const spread = 0.32; + const outline: [number, number][] = []; + for (let side = 0; side < sides; side += 1) { + const angle = (side / sides) * Math.PI * 2; + const reach = spread * (0.62 + 0.38 * Math.abs(Math.sin(side * 2.3 + 0.4))); + outline.push([Math.cos(angle) * reach, Math.sin(angle) * reach]); + } + const on = (u: number, v: number, radius: number): [number, number, number] => { + const length = Math.hypot(u, v, 1); + return [(u / length) * radius, (v / length) * radius, (1 / length) * radius - skull]; + }; + const positions: number[] = []; + const colors: number[] = []; + const scalp = [0.07, 0.035, 0.025]; + const bone = [0.78, 0.7, 0.58]; + const bloodied = [0.42, 0.05, 0.05]; + const triangle = (a: [number, number, number], b: [number, number, number], c: [number, number, number], color: number[]): void => { + positions.push(...a, ...b, ...c); + for (let corner = 0; corner < 3; corner += 1) colors.push(...color); + }; + for (let side = 0; side < sides; side += 1) { + const [u0, v0] = outline[side]!; + const [u1, v1] = outline[(side + 1) % sides]!; + triangle(on(0, 0, skull), on(u0, v0, skull), on(u1, v1, skull), scalp); + triangle(on(0, 0, skull - thickness), on(u1, v1, skull - thickness), on(u0, v0, skull - thickness), side % 3 === 0 ? bone : bloodied); + triangle(on(u0, v0, skull), on(u0, v0, skull - thickness), on(u1, v1, skull), bone); + triangle(on(u1, v1, skull), on(u0, v0, skull - thickness), on(u1, v1, skull - thickness), bone); + } + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); + geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3)); + geometry.computeVertexNormals(); + geometry.computeBoundingSphere(); + return geometry; +} + +/** + * What is left on the neck when a head bursts, seen from above: the tongue at the back, the lower + * teeth in an arch toward the front (the bottom of the texture) and the jawbone round them. + */ +export function jawWoundTexture(): THREE.CanvasTexture { + const size = 128; + const canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext("2d"); + if (ctx !== null) { + const base = ctx.createRadialGradient(64, 60, 6, 64, 64, 66); + base.addColorStop(0, "#7a1016"); + base.addColorStop(0.75, "#530a0e"); + base.addColorStop(1, "#300408"); + ctx.fillStyle = base; + ctx.fillRect(0, 0, size, size); + ctx.fillStyle = "#8a2a2e"; + ctx.beginPath(); + ctx.ellipse(64, 50, 22, 17, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = "rgba(220,120,120,0.35)"; + ctx.beginPath(); + ctx.ellipse(60, 45, 9, 5, -0.3, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = "#a99c86"; + ctx.lineWidth = 5; + ctx.beginPath(); + ctx.ellipse(64, 62, 50, 50, 0, Math.PI * 0.08, Math.PI * 0.92); + ctx.stroke(); + for (let tooth = 0; tooth < 12; tooth += 1) { + const angle = Math.PI * (0.14 + (tooth / 11) * 0.72); + ctx.save(); + ctx.translate(64 + Math.cos(angle) * 42, 62 + Math.sin(angle) * 42); + ctx.rotate(angle + Math.PI / 2); + ctx.fillStyle = tooth % 5 === 2 ? "#3a0508" : "#ece4cf"; + ctx.fillRect(-3.5, -4.5, 7, 9); + ctx.restore(); + } + let seed = 0x2545f491; + const rand = (): number => { + seed = Math.imul(seed ^ (seed >>> 15), 1 | seed); + seed ^= seed + Math.imul(seed ^ (seed >>> 7), 61 | seed); + return ((seed ^ (seed >>> 14)) >>> 0) / 4294967296; + }; + for (let fleck = 0; fleck < 60; fleck += 1) { + ctx.fillStyle = rand() < 0.5 ? "rgba(150,20,24,0.6)" : "rgba(20,2,4,0.5)"; + ctx.beginPath(); + ctx.arc(rand() * size, rand() * size, 0.8 + rand() * 2.2, 0, Math.PI * 2); + ctx.fill(); + } + skinAndFat(ctx); + } + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + return texture; +} + +/** + * An eye torn from its socket, for a sphere's own mapping: the iris and pupil face +Z (a quarter of the + * way round the texture), white veined with red round them, and the torn back of it raw. + */ +export function eyeTexture(): THREE.CanvasTexture { + const canvas = document.createElement("canvas"); + canvas.width = 128; + canvas.height = 64; + const ctx = canvas.getContext("2d"); + if (ctx !== null) { + const sclera = ctx.createLinearGradient(0, 0, 128, 0); + sclera.addColorStop(0, "#e9ddd4"); + sclera.addColorStop(0.42, "#e9ddd4"); + sclera.addColorStop(0.62, "#8a2a2c"); + sclera.addColorStop(0.85, "#4a070b"); + sclera.addColorStop(1, "#e9ddd4"); + ctx.fillStyle = sclera; + ctx.fillRect(0, 0, 128, 64); + ctx.strokeStyle = "rgba(170,20,26,0.75)"; + ctx.lineWidth = 0.8; + for (let vein = 0; vein < 14; vein += 1) { + const angle = (vein / 14) * Math.PI * 2; + ctx.beginPath(); + ctx.moveTo(32 + Math.cos(angle) * 30, 32 + Math.sin(angle) * 26); + ctx.quadraticCurveTo(32 + Math.cos(angle + 0.3) * 20, 32 + Math.sin(angle + 0.3) * 18, 32 + Math.cos(angle) * 11, 32 + Math.sin(angle) * 11); + ctx.stroke(); + } + const iris = ctx.createRadialGradient(32, 32, 2, 32, 32, 9); + iris.addColorStop(0, "#6b4524"); + iris.addColorStop(0.8, "#4a2d14"); + iris.addColorStop(1, "#1e1208"); + ctx.fillStyle = iris; + ctx.beginPath(); + ctx.arc(32, 32, 9, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = "#050302"; + ctx.beginPath(); + ctx.arc(32, 32, 4, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = "rgba(255,255,255,0.85)"; + ctx.beginPath(); + ctx.arc(29.5, 29.5, 1.4, 0, Math.PI * 2); + ctx.fill(); + } + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + return texture; +} diff --git a/web/hands/src/render/graph.ts b/web/hands/src/render/graph.ts index 59df7563..4c9f0fd2 100644 --- a/web/hands/src/render/graph.ts +++ b/web/hands/src/render/graph.ts @@ -1,17 +1,22 @@ import * as THREE from "three"; import { GLTFLoader, type GLTF } from "three/examples/jsm/loaders/GLTFLoader.js"; import { clone as cloneSkeleton } from "three/examples/jsm/utils/SkeletonUtils.js"; -import { GLOVE_HITBOX_RADIUS, HURTBOXES, cancelsRecovery, punchTiming, recoveryCancelAge, totalTicks, type PunchTiming } from "../manifest"; +import { GLOVE_HITBOX_RADIUS, GUARD_BLOCK_MINIMUM, HURTBOXES, cancelsRecovery, punchTiming, recoveryCancelAge, totalTicks, type PunchTiming } from "../manifest"; import type { BloodLevel } from "../settings"; import type { CombatEvent, FighterSnapshot, Hand, Power, PunchAction, PunchClass, SemanticAction, Target } from "../types"; import { FIGHTER_GLB_GZIP_BASE64 } from "../assets/fighter-glb"; import { BONE_ADAPTER } from "./skeleton"; export { BONE_ADAPTER }; import { FIGHTER_TEXTURE_DATA_URLS } from "../assets/fighter-textures"; -import { BODY_SITES, HEAD_SITES, InjuryShading, applyBodyTrauma, applyHeadTrauma } from "./injury"; +import { wearCornerColour } from "./gear"; +import { BODY_SITES, BODY_SWELL_CORE, EYE_LIDS, HEAD_SITES, HEAD_SWELL_CORE, InjuryShading, applyBodyTrauma, applyHeadTrauma, trunksBloodFor } from "./injury"; +import { LookShading, SCANNED_LOOK, type FighterLook } from "./looks"; +import { applyOutfitShading, buildCuffGeometry, buildHandGeometry, type OfficialOutfit, type OutfitPart } from "./outfit"; import { PoseSolver, STANCE, easeIn, easeOut, mirrorX, smoothstep, vec, type FootTarget, type HandTarget, type PoseDescription } from "./poser"; +import { KnockoutRagdoll, blowImpulse, fallStyleFor } from "./ragdoll"; +import { ropeExcess } from "./ring"; import { SolvedRig } from "./rig"; -import { POST_RADIUS, type WorldMapping } from "./world"; +import { CANVAS_TOP, PLATFORM_HALF, POST_RADIUS, type WorldMapping } from "./world"; export const FIGHTER_MODEL_SCALE = 1; @@ -104,19 +109,49 @@ function fighterTexture(name: FighterTexture): THREE.Texture { interface AppliedFighterMaterials { readonly skin: readonly THREE.MeshStandardMaterial[]; - readonly gloves: THREE.MeshStandardMaterial; + readonly gloveBlood: { value: number }; + readonly trunksBlood: { value: number }; readonly owned: readonly THREE.Material[]; readonly headInjury: InjuryShading; readonly bodyInjury: InjuryShading; } +/** + * Lifts a posed vertex that is under the ring's floor onto it, in the mesh's own space, just before it is projected. + * Off the platform the floor is the arena's, a metre lower, and a vertex there is left alone. + */ +export const PRESS_ONTO_CANVAS_GLSL = /* glsl */ `{ + vec4 pressedOntoCanvas = modelMatrix * vec4( transformed, 1.0 ); + if ( pressedOntoCanvas.y < ${CANVAS_TOP.toFixed(4)} && max( abs( pressedOntoCanvas.x ), abs( pressedOntoCanvas.z ) ) < ${PLATFORM_HALF.toFixed(4)} ) { + transformed += inverse( mat3( modelMatrix ) ) * vec3( 0.0, ${CANVAS_TOP.toFixed(4)} - pressedOntoCanvas.y, 0.0 ); + } +}`; + +/** + * Skin and kit meet the canvas instead of passing through it: a vertex the skeleton puts below the canvas is drawn on + * it, as flesh or cloth pressed flat on the floor. A body lying on the canvas rests on spheres at its joints, which the + * calves, the loose trunks and the gloves' padding reach past: they went 5-10 cm under it. Composes with a shader + * patch already on the material. + */ +export function pressOntoCanvas(material: THREE.Material): void { + const before = material.onBeforeCompile; + const beforeKey = Object.hasOwn(material, "customProgramCacheKey") ? material.customProgramCacheKey() : "plain"; + material.onBeforeCompile = (shader, renderer) => { + before.call(material, shader, renderer); + shader.vertexShader = shader.vertexShader.replace("#include ", `${PRESS_ONTO_CANVAS_GLSL}\n#include `); + }; + material.customProgramCacheKey = () => `${beforeKey}-on-canvas`; + material.needsUpdate = true; +} + export function applyFighterSkin(target: THREE.Object3D, palette: BoxerPaletteColors): AppliedFighterMaterials { // An official never takes damage or sweats: plain materials, injury state that shades nothing, and no shadow. - const official = palette.bodyMap !== undefined; + const official = palette.outfit !== undefined; const skin: THREE.MeshStandardMaterial[] = []; const owned: THREE.Material[] = []; const bySource = new Map(); - let gloves: THREE.MeshStandardMaterial | null = null; + let gloveBlood: { value: number } | null = null; + let trunksBlood: { value: number } = { value: 0 }; let headInjury: InjuryShading | null = null; let bodyInjury: InjuryShading | null = null; target.traverse((object) => { @@ -127,39 +162,58 @@ export function applyFighterSkin(target: THREE.Object3D, palette: BoxerPaletteCo let material = bySource.get(sourceName); if (material === undefined) { const isSkin = sourceName === "MHeadMat0" || sourceName === "MBodyMat0"; - const color = sourceName === "GlovesMat0" ? palette.gear : sourceName === "PantsMat0" ? (palette.pants ?? palette.gear) : 0xffffff; - const map = sourceName === "MBodyMat0" && palette.bodyMap !== undefined ? palette.bodyMap : fighterTexture(textureName); + const outfit = palette.outfit; + const color = sourceName === "GlovesMat0" ? palette.gear + : sourceName === "PantsMat0" ? (outfit?.trousers ?? palette.gear) + : sourceName === "ShoesMat0" && outfit !== undefined ? outfit.shoes + : 0xffffff; + const map = fighterTexture(textureName); + // Only a fighter's skin takes the clearcoat that sweat shows on; the outfit makes an official's shirt cloth. material = isSkin && !official ? new THREE.MeshPhysicalMaterial({ map, color, roughness: 0.58, metalness: 0.02, clearcoat: 0.25, clearcoatRoughness: 0.6 }) - : new THREE.MeshStandardMaterial({ map, color, roughness: isSkin ? (sourceName === "MBodyMat0" ? 0.85 : 0.58) : 0.4, metalness: isSkin ? 0.02 : 0.03 }); + : new THREE.MeshStandardMaterial({ map, color, roughness: isSkin ? 0.58 : 0.4, metalness: isSkin ? 0.02 : 0.03 }); material.name = sourceName; bySource.set(sourceName, material); owned.push(material); if (isSkin) skin.push(material); - if (sourceName === "GlovesMat0") gloves = material; - if (sourceName === "MHeadMat0") headInjury = new InjuryShading(official ? null : material, HEAD_SITES); - if (sourceName === "MBodyMat0") bodyInjury = new InjuryShading(official ? null : material, BODY_SITES); + if (sourceName === "MHeadMat0") headInjury = new InjuryShading(official ? null : material, HEAD_SITES, { core: HEAD_SWELL_CORE, lids: EYE_LIDS }); + if (sourceName === "MBodyMat0") bodyInjury = new InjuryShading(official ? null : material, BODY_SITES, { core: BODY_SWELL_CORE, wash: true }); + const part = OUTFIT_PARTS[sourceName]; + if (outfit !== undefined && part !== undefined) applyOutfitShading(material, part, outfit); + else if (sourceName === "PantsMat0") trunksBlood = wearCornerColour(material, false, "trunks"); + else if (sourceName === "GlovesMat0") gloveBlood = wearCornerColour(material); + pressOntoCanvas(material); } object.material = material; object.castShadow = !official; object.receiveShadow = true; object.frustumCulled = false; }); - if (skin.length !== 2 || gloves === null || owned.length !== 5 || headInjury === null || bodyInjury === null) { + if (skin.length !== 2 || gloveBlood === null || owned.length !== 5 || headInjury === null || bodyInjury === null) { throw new Error(`fighter GLB material contract failed: ${skin.length} skin, ${owned.length} total`); } - return { skin, gloves, owned, headInjury, bodyInjury }; + return { skin, gloveBlood, trunksBlood, owned, headInjury, bodyInjury }; } export interface BoxerPaletteColors { readonly skin: number; readonly gear: number; - readonly pants?: number; - /** An official's shirt. Officials get plain materials: no injury shading, no sweat and no shadow (see applyFighterSkin). */ - readonly bodyMap?: THREE.Texture; + /** Skin tone, hair, beard and face shape; the scanned man when omitted. */ + readonly look?: FighterLook; + /** + * Dresses the body as a ring official and swaps the gloves for hands. Officials get plain materials: + * no injury shading, no sweat and no shadow (see applyFighterSkin). + */ + readonly outfit?: OfficialOutfit; } +const OUTFIT_PARTS: Readonly> = { MHeadMat0: "head", MBodyMat0: "body", ShoesMat0: "shoes", PantsMat0: "pants" }; +/** The lit tone of the scanned face texture in linear light, so bare hands match the face. */ +const SCANNED_SKIN = new THREE.Color().setRGB(0.6, 0.32, 0.19, THREE.LinearSRGBColorSpace); + export type ArcadeDislocation = "jaw" | "shoulder_left" | "shoulder_right"; +/** What the cutman has in his hand: the enswell for a cut or a swelling, a water bottle for the breath. */ +export type CutmanProp = "enswell" | "bottle"; const smooth = (current: number, target: number, rate: number, dt: number): number => current + (target - current) * (1 - Math.exp(-rate * dt)); const smoothAngle = (current: number, target: number, rate: number, dt: number): number => { @@ -175,16 +229,22 @@ export class SkinnedBoxer { /** Bone-derived measurements in world units (after MODEL_SCALE). */ readonly metrics: { armUpper: number; armFore: number; legThigh: number; legShin: number; headRestY: number; chestRestY: number; ankleRestY: number }; private readonly skinMaterials: readonly THREE.MeshStandardMaterial[]; - private readonly ownedMaterials: readonly THREE.Material[]; - private readonly gearMaterial: THREE.MeshStandardMaterial; + private readonly ownedMaterials: THREE.Material[]; + private readonly gloveBlood: { value: number }; + private readonly trunksBlood: { value: number }; private readonly headMeshes: THREE.SkinnedMesh[] = []; private readonly handMeshes: Record = { left: [], right: [] }; private decapitated = false; + private burst = false; private readonly dismemberedHands: Record = { left: false, right: false }; + private readonly dressed: boolean; + private readonly ownedGeometries: THREE.BufferGeometry[] = []; readonly gearBaseColor: THREE.Color; readonly skinBaseColor: THREE.Color; readonly headInjury: InjuryShading; readonly bodyInjury: InjuryShading; + private readonly headLook: LookShading; + private readonly bareHand: THREE.MeshStandardMaterial | null; constructor(gltf: GLTF, palette: BoxerPaletteColors) { const instance = cloneSkeleton(gltf.scene); @@ -192,12 +252,14 @@ export class SkinnedBoxer { this.root.add(instance); const materials = applyFighterSkin(instance, palette); this.skinMaterials = materials.skin; - this.ownedMaterials = materials.owned; - this.gearMaterial = materials.gloves; + this.ownedMaterials = [...materials.owned]; + this.gloveBlood = materials.gloveBlood; + this.trunksBlood = materials.trunksBlood; this.headInjury = materials.headInjury; this.bodyInjury = materials.bodyInjury; this.gearBaseColor = new THREE.Color(palette.gear); this.skinBaseColor = new THREE.Color(palette.skin); + this.dressed = palette.outfit !== undefined; instance.traverse((object) => { if (object instanceof THREE.SkinnedMesh && object.name === "BoxerHead") this.headMeshes.push(object); if (object instanceof THREE.SkinnedMesh && object.name === "BoxerGloveLeft") this.handMeshes.left.push(object); @@ -207,6 +269,15 @@ export class SkinnedBoxer { const missing = Object.values(BONE_ADAPTER).filter((name) => !this.bones.has(name)); if (missing.length > 0) throw new Error(`fighter GLB missing required bones: ${missing.join(", ")}`); if (this.headMeshes.length !== 1) throw new Error(`fighter GLB requires one BoxerHead mesh, found ${this.headMeshes.length}`); + const headShadow = this.headInjury.shadowMaterial(); + this.headMeshes[0]!.customDepthMaterial = headShadow; + // A head is one-sided until a finisher cuts it open, when the injury shading draws it from inside too + // so the cut shows flesh. Shadows come from the back faces as for a one-sided skin either way, or the + // face would shadow itself in stripes. + const headMaterial = this.headMeshes[0]!.material as THREE.Material; + headMaterial.side = THREE.FrontSide; + headMaterial.shadowSide = THREE.BackSide; + this.ownedMaterials.push(headShadow); for (const side of ["left", "right"] as const) { if (this.handMeshes[side].length !== 1) { throw new Error(`fighter GLB requires one ${side} glove mesh, found ${this.handMeshes[side].length}`); @@ -214,6 +285,10 @@ export class SkinnedBoxer { } this.root.updateMatrixWorld(true); this.rig = new SolvedRig(this.root); + // Last on, so it runs first in the shader: the look, then the clothes over it, then the injuries. + this.headLook = new LookShading(this.headMeshes[0]!.material as THREE.MeshStandardMaterial); + this.bareHand = palette.outfit === undefined ? null : this.addHands(palette.outfit); + this.setLook(palette.look ?? SCANNED_LOOK); const rigMetrics = this.rig.metrics; this.metrics = { armUpper: rigMetrics.armL.upper, @@ -230,8 +305,22 @@ export class SkinnedBoxer { return this.bones.get(BONE_ADAPTER[name] ?? name) ?? null; } - get gloveGear(): THREE.MeshStandardMaterial { - return this.gearMaterial; + /** How much of the opponent's blood is on the gloves, 0 to 1. */ + setGloveBlood(amount: number): void { + this.gloveBlood.value = clamp(amount, 0, 1); + } + + get gloveBloodLevel(): number { + return this.gloveBlood.value; + } + + /** How far the fighter's own blood has run into the front of the trunks, 0 to 1. */ + setTrunksBlood(amount: number): void { + this.trunksBlood.value = clamp(amount, 0, 1); + } + + get trunksBloodLevel(): number { + return this.trunksBlood.value; } get headMesh(): THREE.SkinnedMesh { @@ -250,9 +339,23 @@ export class SkinnedBoxer { return this.decapitated; } + /** Cuts the head off through the neck; the collar of the head mesh stays on the shoulders. */ setDecapitated(value: boolean): void { this.decapitated = value; - for (const mesh of this.headMeshes) mesh.visible = !value; + this.burst = false; + this.headInjury.setSevered(value); + this.bone("head")!.visible = !value; + } + + get isHeadBurst(): boolean { + return this.burst; + } + + /** Bursts the head above the mouth; the lower jaw stays on the neck. */ + setHeadBurst(value: boolean): void { + this.burst = value; + this.decapitated = false; + this.headInjury.setBurst(value); this.bone("head")!.visible = !value; } @@ -262,7 +365,51 @@ export class SkinnedBoxer { setHandDismembered(side: Hand, value: boolean): void { this.dismemberedHands[side] = value; - for (const mesh of this.handMeshes[side]) mesh.visible = !value; + for (const mesh of this.handMeshes[side]) mesh.visible = !value && !this.dressed; + } + + /** Changes who this fighter is: skin tone, hair, beard and face shape. */ + setLook(look: FighterLook): void { + this.headLook.set(look); + for (const material of this.skinMaterials) material.color.setHex(look.tint); + this.bareHand?.color.copy(SCANNED_SKIN).multiply(this.skinMaterials[0]!.color); + } + + get look(): FighterLook { + return this.headLook.look; + } + + /** Returns the material of bare hands, which follows the skin tone, or null for gloved ones. */ + private addHands(outfit: OfficialOutfit): THREE.MeshStandardMaterial | null { + const gloved = outfit.gloves !== null; + const hand = new THREE.MeshStandardMaterial({ + color: gloved ? outfit.gloves! : SCANNED_SKIN, + roughness: gloved ? 0.38 : 0.6, + metalness: 0.02, + }); + const cuff = gloved ? hand : new THREE.MeshStandardMaterial({ color: new THREE.Color(outfit.shirt).multiplyScalar(0.9), roughness: 0.88, metalness: 0 }); + this.ownedMaterials.push(hand); + if (cuff !== hand) this.ownedMaterials.push(cuff); + const cuffGeometry = buildCuffGeometry(); + this.ownedGeometries.push(cuffGeometry); + for (const side of ["left", "right"] as const) { + for (const mesh of this.handMeshes[side]) mesh.visible = false; + const geometry = buildHandGeometry(side); + this.ownedGeometries.push(geometry); + const suffix = side === "left" ? "L" : "R"; + for (const [bone, part, material, name] of [ + [this.rig.bones[`glove${suffix}`], geometry, hand, `hand-${side}`], + [this.rig.bones[`elbow${suffix}`], cuffGeometry, cuff, `cuff-${side}`], + ] as const) { + const mesh = new THREE.Mesh(part, material); + mesh.name = name; + // Too small to throw a shadow anyone would see; it would only cost a draw in the shadow pass. + mesh.castShadow = false; + mesh.receiveShadow = true; + bone.add(mesh); + } + } + return gloved ? null : hand; } /** Sweat sheen; only a fighter's skin is clearcoated, an official's stays matte. */ @@ -272,14 +419,15 @@ export class SkinnedBoxer { dispose(): void { for (const material of this.ownedMaterials) material.dispose(); + for (const geometry of this.ownedGeometries) geometry.dispose(); disposeSkeletons(this.root); } } -const BLOODED_GLOVE_COLOR = new THREE.Color(0x5c0a0e); - export type ReactionKind = "block" | "hit"; +export type Verdict = "winner" | "loser" | "level"; + interface FootState { readonly planted: THREE.Vector3; readonly from: THREE.Vector3; @@ -319,6 +467,53 @@ const springStep = (spring: Spring3, dt: number, stiffness: number, damping: num const PUNCH_CONTACT_OFFSET = 0.06; /** Damage of the hardest hit the engine deals (a countered power uppercut in a combination, about 153). */ const HARDEST_HIT = 150; +/** Between these gaps (metres between the fighters' feet) the stance closes up for infighting. */ +const CROWDED_GAP = 0.58; +/** Two heads keep at least this far apart: closer, each leans his head and shoulders away from the other's, and eases back once they are clear. */ +const HEAD_SPACE = 0.25; +const HEAD_SPACE_CLEAR = 0.32; +const HEAD_SPACE_RATE = 9; +const OPEN_GAP = 1; +/** In a clinch each fighter rests his head this far to his own right (metres), leaning his shoulders that way (radians). */ +const CLINCH_HEAD_SIDE = 0.08; +const CLINCH_HEAD_LEAN = 0.3; +/** Within this gap (metres between them) an opponent on top of a fighter held on the ropes comes in with him, and from the second on not at all. */ +const ROPE_HOLD_CLOSE = 1; +const ROPE_HOLD_OPEN = 1.6; +/** + * Held on the ropes, a fighter leans back on them like a man against a wall: in full once the engine has him + * this far past their furthest give (metres), with his feet this much further in, so his back rests on the top + * rope. He tips back from the hips toward the ropes, or sideways when they are at his side (radians), and the + * lean follows the engine's place for him, quickly settling. + */ +const ROPE_LEAN_FULL = 0.25; +const ROPE_LEAN_ROOM = 0.12; +const ROPE_LEAN_PITCH = 0.3; +const ROPE_LEAN_ROLL = 0.3; +const ROPE_LEAN_RATE = 8; + +/** + * How far (world x and z, into `out`) a fighter whose place is (x, z) is drawn in off the ropes, and how far + * he leans back on them, 0 to 1; `excess` takes how far past the ropes' furthest give his place is. + */ +function ropeHoldAt(x: number, z: number, excess: { x: number; z: number }, out: { x: number; z: number }): number { + ropeExcess(x, z, excess); + const past = Math.hypot(excess.x, excess.z); + if (past <= 0) { + out.x = 0; + out.z = 0; + return 0; + } + const lean = smoothstep(0, ROPE_LEAN_FULL, past); + const reach = 1 + (ROPE_LEAN_ROOM * lean) / past; + out.x = excess.x * reach; + out.z = excess.z * reach; + return lean; +} + +/** Along one axis, how far in to draw a fighter held `own` on the ropes, or with an opponent held `theirs`. */ +const heldIn = (own: number, theirs: number): number => (own === 0 || (Math.sign(theirs) === Math.sign(own) && Math.abs(theirs) > Math.abs(own)) ? theirs : own); + const KNOCKDOWN_FALL_SECONDS = 0.75; /** * The get-up runs lying -> all fours (RISE_FOURS) -> one knee (RISE_KNEE) -> standing (1). While he is down it @@ -334,6 +529,21 @@ const RISE_PUSH_RATE = 0.55; const RISE_SINK_RATE = 0.4; /** Rolling off the back onto all fours is a big movement, never hurried past this. */ const RISE_ROLL_RATE = 0.7; +/** + * How fast a knee's or an elbow's pole that has to come round a long way swings round its limb, down and getting up, + * and how long after he is up the swing may still be finishing. + */ +const POLE_TURN_RATE = 6; +const POLE_SETTLE_SECONDS = 0.4; +/** Seconds over which the get-up takes the body over from the pose the knockout physics left it in. */ +const RISE_BLEND_SECONDS = 0.2; +/** + * The knockout physics follows the get-up from the canvas until he is on all fours (and for at least this long): the + * get-up's first stage, blended from wherever the fall left him, can pass through the canvas; all fours does not. + */ +const RISE_HANDOVER_SECONDS = 0.3; +/** A root this far from the fighter's place when he goes down was drawn there before a gap: he falls at his place. */ +const FALL_ROOT_JUMP = 0.5; const SIDE_FROM_HAND = (hand: Hand): "L" | "R" => (hand === "left" ? "L" : "R"); @@ -382,6 +592,7 @@ export class BoxingGraph { private shownOpponent: FighterSnapshot | null = null; /** Own punches already played or cut short here; the server's copy of them is not played again. */ private readonly retiredOwnIds: string[] = []; + private anticipatedId: string | null = null; private hitstop = 0; private hitstopScale = 1; private downState: "up" | "falling" | "down" | "rising" = "up"; @@ -391,11 +602,38 @@ export class BoxingGraph { private riseFrom = 0; private fallSide = 0; private fallProne = false; - /** The fall played: the punch's own, or the nearest variant that keeps the body inside the ropes, moved in as far as it must. */ + /** The knockdown was a body shot: the fighter goes to a knee instead of falling. */ + private fallKneel = false; + /** Which side of the body took the shot that is putting him down: 1 his left, -1 his right. */ + private windedSide = -1; + private windedTime = 0; + private winded = 0; + /** + * The fall played: the punch's own, or the nearest variant that keeps the body inside the ropes, moved in as + * far as it must; after a fall the knockout physics played, where and how that left the body. + */ private landProne = false; private landSide = 0; private readonly landShift = new THREE.Vector3(); private landSettled = false; + /** Where the knockout physics left the body, as a down pose the get-up starts from; set when it hands him over. */ + private readonly fallen = downPose(); + private fallenFromPhysics = false; + /** + * How far (world x and z) the drawn fighter stands from his place in the engine after the physics took the body + * somewhere else, and how far it was when he started to get up: he gets up where he lies and steps back to his place + * as he rises off his knee. `riseRootVelocity` is that step's, for the feet. + */ + private readonly riseRoot = new THREE.Vector3(); + private readonly riseRootFrom = new THREE.Vector3(); + /** The knee and elbow poles last drawn down or getting up (lead knee, rear knee, lead elbow, rear elbow). */ + private readonly downPoles = [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()] as const; + private downPolesSet = false; + private poleSettle = 0; + private readonly characterInverse = new THREE.Matrix4(); + /** Where the engine has the fighter this frame (with the glove-touch walk and the walk back from a fall). */ + private readonly place = new THREE.Vector3(); + private readonly riseRootVelocity = new THREE.Vector3(); private readonly liveOpponentHead = new THREE.Vector3(); private hasLiveHead = false; /** @@ -419,6 +657,8 @@ export class BoxingGraph { private guardKick = 0; private guardHigh = 0; private guardLow = 0; + /** The guard is spent, worn below what can stop a punch: the gloves sag toward the chest. */ + private guardSpent = 0; private slip = 0; private weave = 0; private weaveTime = 0; @@ -434,6 +674,25 @@ export class BoxingGraph { private clinchWeight = 0; private foulWeight = 0; private tauntWeight = 0; + private crowding = 0; + /** How far he has stepped into the punch he is throwing, out of the tall, tucked stance of the inside. */ + private punchCommit = 0; + /** How far, in his own frame (x to the side, y ahead), the fighter is leaning his head away from the other's. */ + private readonly headSpace = new THREE.Vector2(); + private readonly ownHead = new THREE.Vector3(); + /** + * On the ropes: how far (world x and z) he is drawn in from his place in the engine, so his back stays on + * the ropes at their furthest give (an opponent on top of him comes in with him), how far past that give + * the engine has him himself, and how far he leans back on the ropes, toward `ropeOutward`. + */ + private readonly ropeHold = new THREE.Vector3(); + private readonly ropeExcess = { x: 0, z: 0 }; + private readonly ropeOwn = { x: 0, z: 0 }; + private readonly ropeTheirs = { x: 0, z: 0 }; + private ropeLeanTarget = 0; + private ropeLean = 0; + private readonly ropeOutward = new THREE.Vector3(1, 0, 0); + private readonly ropeLocal = new THREE.Vector3(); /** Render tick the taunt ends on (see latchEndTick). */ private tauntEnd: number | null = null; private lastSpeed = 0; @@ -442,6 +701,8 @@ export class BoxingGraph { private readonly stoolRoot = new THREE.Vector3(); private stoolYaw = 0; private readonly enswell: { group: THREE.Group; dispose: () => void }; + private readonly bottle: { group: THREE.Group; dispose: () => void }; + private treatProp: CutmanProp = "enswell"; private readonly treatTarget = new THREE.Vector3(); private readonly treatScratch = new THREE.Vector3(); private readonly treatFacing = new THREE.Vector3(0, 0, -1); @@ -461,6 +722,15 @@ export class BoxingGraph { private readonly breakFighters: [THREE.Vector3, THREE.Vector3] = [new THREE.Vector3(), new THREE.Vector3()]; private breakAimed = false; private attending = false; + /** Where the referee stands at the announcement (1 = on this fighter's left), or 0 outside it. */ + private verdictSide = 0; + private verdict: Verdict | null = null; + private squareWeight = 0; + private raisedWeight = 0; + private bowedWeight = 0; + private readonly wrists: [THREE.Vector3 | null, THREE.Vector3 | null] = [null, null]; + private readonly wristTargets = [new THREE.Vector3(), new THREE.Vector3()] as const; + private readonly wristWeights: [number, number] = [0, 0]; private attendWeight = 0; private countdownTicks: number | null = null; /** Render tick the opening countdown ends on (see latchEndTick). */ @@ -527,6 +797,18 @@ export class BoxingGraph { private refereeCounting = false; /** Render tick the referee's count last changed on; the counting arm is timed from it. */ private countChangedTick: number | null = null; + /** Knockout physics; officials have none. */ + private readonly ragdoll: KnockoutRagdoll | null; + /** Knocked out on his feet (a flash knockout): down although the engine never counts him. */ + private forcedDown = false; + /** The bout is over, or being replayed, with him on the canvas: he does not try to get up. */ + private heldDown = false; + /** The next fall is the animation's, not the physics' (a body shot that takes him to one knee). */ + private authoredNextFall = false; + private obstacle: { x: number; z: number } | null = null; + private blows = 0; + /** Seconds since the knockout physics handed the body to the get-up, which blends in from where it left it. */ + private riseBlendAge = 0; constructor(boxer: SkinnedBoxer, private readonly mapping: WorldMapping, options: { referee?: boolean } = {}) { this.boxer = boxer; @@ -538,6 +820,44 @@ export class BoxingGraph { boxer.root.add(this.stool.group); this.enswell = buildEnswell(); boxer.rig.bones.gloveL.add(this.enswell.group); + this.bottle = buildBottle(); + boxer.rig.bones.gloveL.add(this.bottle.group); + this.ragdoll = this.referee ? null : new KnockoutRagdoll(boxer.rig, boxer.root); + } + + /** The knockout replay runs the recorded fall again rather than a new one. */ + primeReplayFall(): void { + this.ragdoll?.prime(); + } + + /** The next knockdown plays the animation rather than the physics: a body shot drops him to one knee. */ + useAuthoredFall(): void { + this.authoredNextFall = true; + } + + /** Puts the fighter down for a flash knockout, which the engine ends without a count. */ + knockOut(): void { + this.forcedDown = true; + } + + /** + * Once the bout is over, and while its finish is replayed, a fighter on the canvas stays there: the + * get-up stops following his meter and the knockout physics keeps the body. + */ + stayDown(held: boolean): void { + this.heldDown = held; + } + + /** The standing opponent, whom a falling fighter does not fall through. */ + setObstacle(x: number, z: number): void { + this.obstacle ??= { x, z }; + this.obstacle.x = x; + this.obstacle.z = z; + } + + /** The knockout physics while it drives this fighter, for the camera and the officials. */ + get fallBody(): KnockoutRagdoll | null { + return this.ragdoll?.active === true ? this.ragdoll : null; } /** Between rounds the fighter walks to the corner, and once still, sits on the stool. */ @@ -560,8 +880,24 @@ export class BoxingGraph { * knockout replay. `downed` seeds the lying pose instead of standing. */ resetTransient(downed = false): void { + this.forcedDown = false; + const ragdoll = this.ragdoll; + if (ragdoll !== null) { + if (!downed) { + ragdoll.stop(); + ragdoll.clearRise(); + } else if (ragdoll.active) { + ragdoll.settle(); + } else { + ragdoll.restoreSettled(); + } + } this.downState = downed ? "down" : "up"; this.landSettled = false; + this.fallenFromPhysics = false; + this.riseRoot.set(0, 0, 0); + this.riseRootFrom.set(0, 0, 0); + this.riseRootVelocity.set(0, 0, 0); this.fallAge = downed ? KNOCKDOWN_FALL_SECONDS : 0; this.riseProgress = 0; this.hitstop = 0; @@ -574,10 +910,19 @@ export class BoxingGraph { this.pendingFold = 0; this.guardKick = 0; this.stunAmount = 0; + this.windedTime = 0; + this.winded = 0; this.celebrateTime = 0; this.celebration = 0; this.waveTime = 0; this.wave = 0; + this.verdictSide = 0; + this.verdict = null; + this.squareWeight = 0; + this.raisedWeight = 0; + this.bowedWeight = 0; + this.wrists.fill(null); + this.wristWeights.fill(0); this.seated = 0; this.stillTime = 0; this.rootX = null; @@ -587,9 +932,10 @@ export class BoxingGraph { this.ownQueued = null; this.completedActionId = null; this.retiredOwnIds.length = 0; + this.anticipatedId = null; } - /** Referee wave-off: both arms sweep crossing overhead to call the fight. */ + /** Referee wave-off: both arms sweep crossing in front of the chest to call the fight. */ waveOff(seconds = 2.6): void { this.waveTime = seconds; } @@ -608,12 +954,33 @@ export class BoxingGraph { * Cutman: crouch by a seated fighter and press the enswell on the eye at `eye` (world). `facing` is * the direction the fighter's face points and `side` is 1 when the eye is the fighter's left. */ - treat(eye: THREE.Vector3 | null, facing?: THREE.Vector3, side = 1): void { + treat(eye: THREE.Vector3 | null, facing?: THREE.Vector3, side = 1, prop: CutmanProp = "enswell"): void { this.treating = eye !== null; if (eye === null) return; this.treatTarget.copy(eye); if (facing !== undefined) this.treatFacing.copy(facing); this.treatSide = side; + this.treatProp = prop; + } + + /** + * The announcement of a decision. The fighter stands square to the camera beside the referee, who + * is on the side given (1 = the fighter's left); null ends it. + */ + awaitVerdict(side: 1 | -1 | null): void { + this.verdictSide = side ?? 0; + if (side === null) this.verdict = null; + } + + /** The winner's arm on the referee's side goes up, as do both fighters' after a draw; the loser's head goes down. */ + announce(verdict: Verdict): void { + this.verdict = verdict; + } + + /** Referee: hold up the wrists at these world positions, one to each side; null lets that arm down. */ + raise(left: THREE.Vector3 | null, right: THREE.Vector3 | null): void { + this.wrists[0] = left === null ? null : this.wristTargets[0].copy(left); + this.wrists[1] = right === null ? null : this.wristTargets[1].copy(right); } /** Referee break: both arms push out and apart at chest height to separate a clinch. */ @@ -749,23 +1116,54 @@ export class BoxingGraph { if (this.punchAgeTicks < this.punchTiming.startup) this.ownPulled = true; } + /** + * Starts a punch the server has begun but the delayed render clock has not reached, so an + * opponent's punch is seen as early as the network allows. `leadTicks` is how long the render + * clock will take to reach the punch's first tick. The wind-up is stretched over that wait, so + * a punch too far ahead is held back until it can be shown at half speed or faster. + */ + anticipate(fighter: FighterSnapshot, leadTicks: number): void { + const id = fighter.action_id; + if (id === null || fighter.action === null || leadTicks <= 0) return; + if (id === this.anticipatedId || id === this.actionId || id === this.completedActionId || id === this.ownActionId || this.retiredOwnIds.includes(id)) return; + if (leadTicks > fighter.action_startup_ticks * (1 / MIN_ANTICIPATION_RATE - 1)) return; + if (this.punchActive && this.punchAgeTicks / this.punchTotalTicks <= 0.55) return; + this.anticipatedId = id; + const target = fighter.action_target ?? "head"; + const power = fighter.action_power ?? "normal"; + const timing = { + ...punchTiming(fighter.action, target, power), + startup: fighter.action_startup_ticks, + active: fighter.action_active_ticks, + recovery: fighter.action_recovery_ticks, + }; + const hand = fighter.action_hand ?? (fighter.stance === "orthodox" ? "left" : "right"); + this.startEarly(id, fighter.action, hand, target, power, timing, leadTicks, leadTicks); + } + /** Plays the viewer's own press from its first frame, its startup stretched by `leadTicks`. */ private startOwnPunch(press: OwnPress, leadTicks: number): void { const { action, timing } = press; - this.punchClass = action.class; - this.punchHand = action.hand; - this.punchTarget = action.target; - this.punchPower = action.power; + this.startEarly(action.id, action.class, action.hand, action.target, action.power, timing, leadTicks, Math.max(0, leadTicks)); + this.ownSequence = press.sequence; + this.ownStarted = press.started; + this.ownPressedAt = press.pressedAt; + } + + /** Starts a punch ahead of the server's presentation of it: the viewer's own press, or a punch seen early. */ + private startEarly(id: string, punchClass: PunchClass, hand: Hand, target: Target, power: Power, timing: PunchTiming, leadTicks: number, expectedTicks: number): void { + this.retirePunch(); + this.punchClass = punchClass; + this.punchHand = hand; + this.punchTarget = target; + this.punchPower = power; this.punchTiming = timing; this.punchTotalTicks = Math.max(1, totalTicks(timing)); this.punchAgeTicks = 0; this.punchActive = true; - this.ownActionId = action.id; + this.ownActionId = id; this.ownLeadTicks = clamp(leadTicks, 0, MAX_OWN_LEAD_TICKS); - this.ownExpectedTicks = Math.max(0, leadTicks); - this.ownSequence = press.sequence; - this.ownStarted = press.started; - this.ownPressedAt = press.pressedAt; + this.ownExpectedTicks = expectedTicks; } /** @@ -778,7 +1176,7 @@ export class BoxingGraph { const opponent = this.shownOpponent; const id = this.actionId ?? this.ownActionId; const landed = id !== null && id === this.contactId && !this.contactParried; - if (fighter !== null && opponent !== null && cancelsRecovery(this.punchClass, landed, press.action, fighter.stamina, opponent.stunned_ticks > 0)) { + if (fighter !== null && opponent !== null && cancelsRecovery(this.punchClass, landed, press.action, fighter.stamina, opponent.stunned_ticks > 0, fighter.style)) { return recoveryCancelAge(this.punchTiming); } return this.punchTotalTicks + 1; @@ -896,6 +1294,36 @@ export class BoxingGraph { this.fallSide = punchClass === "hook" ? lateral : lateral * 0.5; this.fallProne = punchClass === "hook" || target === "body"; } + // The knockdown itself is presented as a hit too, carrying the count rather than the damage. + if (kind === "hit" && amount >= 10 && this.ragdoll !== null) { + const blow = blowImpulse({ target, punchClass, hand, lateral, amount }); + const snap = this.scratchE.set(blow.x, blow.y, blow.z).applyAxisAngle(worldUpVector, this.yaw); + const drive = this.scratchD.set(blow.driveX, blow.driveY, blow.driveZ).applyAxisAngle(worldUpVector, this.yaw); + this.blows += 1; + this.ragdoll.takeBlow(snap, drive, target, blow.twist, fallStyleFor(punchClass, target, amount, this.blows + Math.round(amount))); + } + } + + /** The puncher's power shot was parried: he is knocked back off balance with his hands thrown wide. */ + stagger(): void { + this.torsoKick.velocity.x -= 2.6; + this.headKick.velocity.z -= 1.4; + this.headKick.velocity.y += 0.5; + this.rootKick.velocity.z -= 1.1; + this.guardKick = Math.max(this.guardKick, 1.1); + } + + /** A body shot that is going to put him down: he freezes, folds over the shot and clutches it. `side` is 1 for his left. */ + windedFor(seconds: number, side: number): void { + this.windedTime = Math.max(this.windedTime, seconds); + this.windedSide = side >= 0 ? 1 : -1; + } + + /** Sets how the next knockdown looks: to a knee after a body shot, otherwise a fall. */ + fallToKnee(knee: boolean): void { + this.fallKneel = knee; + // Taking a knee is played by the animation, never by the knockout physics. + if (knee) this.authoredNextFall = true; } /** Transient compression of the struck surface at contact; the injury shading releases it. */ @@ -918,7 +1346,7 @@ export class BoxingGraph { const foot = this.feet[index]!; const isLead = (index === 0) === (mirror > 0); const offset = isLead ? STANCE.leadFoot : STANCE.rearFoot; - if (this.referee) desired.set((index === 0 ? 0.16 : -0.16), 0, index === 0 ? 0.02 : -0.02).applyQuaternion(rotate).add(rootPosition); + if (this.referee || this.squareWeight > 0.5) desired.set((index === 0 ? 0.16 : -0.16), 0, index === 0 ? 0.02 : -0.02).applyQuaternion(rotate).add(rootPosition); else desired.set(offset.x * mirror, 0, offset.z).applyQuaternion(rotate).add(rootPosition); desired.y = 0; if (!this.feetInitialized) { @@ -977,8 +1405,11 @@ export class BoxingGraph { const motionScale = reducedMotion ? 0.35 : 1; const touch = this.touchWalk(fighter, opponent, dt, sampledTick); - const worldX = this.mapping.x(fighter.x) + touch.x; - const worldZ = this.mapping.z(fighter.y) + touch.z; + this.walkBackFromTheFall(dt); + const hold = this.holdOnTheRopes(opponent, this.mapping.x(fighter.x) + touch.x + this.riseRoot.x, this.mapping.z(fighter.y) + touch.z + this.riseRoot.z); + const worldX = this.mapping.x(fighter.x) + touch.x + this.riseRoot.x - hold.x; + const worldZ = this.mapping.z(fighter.y) + touch.z + this.riseRoot.z - hold.z; + this.place.set(worldX, 0, worldZ); if (this.rootX === null) { this.rootX = worldX; this.rootZ = worldZ; @@ -1009,8 +1440,10 @@ export class BoxingGraph { if (opponentHeadWorld !== undefined) this.trackOpponentHead(opponent, opponentHeadWorld, dt); - const velocityWorld = this.scratchB.set(this.mapping.x(fighter.velocity_x) * 30, 0, this.mapping.z(fighter.velocity_y) * 30).add(this.touchVelocity); + const velocityWorld = this.scratchB.set(this.mapping.x(fighter.velocity_x) * 30, 0, this.mapping.z(fighter.velocity_y) * 30).add(this.touchVelocity).add(this.riseRootVelocity); const speed = velocityWorld.length(); + // His own walking, without the step back to his place that a get-up takes. + const ownSpeed = Math.hypot(velocityWorld.x - this.riseRootVelocity.x, velocityWorld.z - this.riseRootVelocity.z); this.lastSpeed = speed; this.stillTime = speed < 0.03 ? this.stillTime + dt : 0; const wantSeated = this.resting && this.stillTime > 0.2 && this.downState === "up"; @@ -1033,8 +1466,17 @@ export class BoxingGraph { this.breakTime = Math.max(0, this.breakTime - dt); this.breakWeight = smooth(this.breakWeight, this.breakTime > 0 && this.downState === "up" ? 1 : 0, 6, dt); this.attendWeight = smooth(this.attendWeight, this.attending ? 1 : 0, 2.5, dt); + this.squareWeight = smooth(this.squareWeight, this.verdictSide !== 0 ? 1 : 0, 3, dt); + this.raisedWeight = smooth(this.raisedWeight, this.verdictSide !== 0 && (this.verdict === "winner" || this.verdict === "level") ? 1 : 0, 3.2, dt); + this.bowedWeight = smooth(this.bowedWeight, this.verdictSide !== 0 && this.verdict === "loser" ? 1 : 0, 2.2, dt); + for (const index of [0, 1] as const) this.wristWeights[index] = smooth(this.wristWeights[index], this.wrists[index] !== null ? 1 : 0, 3.2, dt); this.treatWeight = smooth(this.treatWeight, this.treating ? 1 : 0, 3, dt); - this.enswell.group.visible = this.treatWeight > 0.4; + this.windedTime = Math.max(0, this.windedTime - dt); + // Folded over the body shot until he is down on the knee, so the fall starts from the hunch. + const sinking = this.fallKneel && this.downState === "falling"; + this.winded = smooth(this.winded, (this.windedTime > 0 && this.downState === "up") || sinking ? 1 : 0, this.windedTime > 0 || sinking ? 12 : 4, dt); + this.enswell.group.visible = this.treatWeight > 0.4 && this.treatProp === "enswell"; + this.bottle.group.visible = this.treatWeight > 0.4 && this.treatProp === "bottle"; const touching = this.countdownTicks !== null && this.countdownTicks <= TOUCH_GLOVES_START_TICKS && this.countdownTicks >= TOUCH_GLOVES_END_TICKS; this.touchWeight = smooth(this.touchWeight, touching ? 1 : 0, 6, dt); const stamina = fighter.stamina / Math.max(1, fighter.maximum_stamina); @@ -1043,6 +1485,7 @@ export class BoxingGraph { const defending = fighter.defense; this.guardHigh = smooth(this.guardHigh, defending === "guard_high" ? 1 : 0, 14, dt); this.guardLow = smooth(this.guardLow, defending === "guard_low" ? 1 : 0, 14, dt); + this.guardSpent = smooth(this.guardSpent, fighter.guard < GUARD_BLOCK_MINIMUM ? 1 : 0, 10, dt); const slipTarget = defending === "slip_left" ? 1 : defending === "slip_right" ? -1 : 0; this.slip = smooth(this.slip, slipTarget, 16, dt); const weaving = defending === "weave"; @@ -1052,6 +1495,27 @@ export class BoxingGraph { this.clinchWeight = smooth(this.clinchWeight, fighter.clinch_ticks > 0 || fighter.clinch_startup_ticks > 0 ? 1 : 0, 10, dt); this.foulWeight = smooth(this.foulWeight, fighter.is_foul_recovery_target ? 1 : 0, 8, dt); this.tauntWeight = smooth(this.tauntWeight, fighter.taunt_ticks > 0 ? 1 : 0, 10, dt); + const gap = Math.hypot(this.mapping.x(opponent.x) - this.mapping.x(fighter.x), this.mapping.z(opponent.y) - this.mapping.z(fighter.y)); + const crowded = this.referee || fighter.is_downed || opponent.is_downed ? 0 : 1 - smoothstep(CROWDED_GAP, OPEN_GAP, gap); + this.crowding = smooth(this.crowding, crowded, 9, dt); + let apart = Number.POSITIVE_INFINITY; + if (this.hasLiveHead && !this.referee && this.downState === "up") { + const own = this.boxer.rig.bones.head.getWorldPosition(this.ownHead); + const dx = own.x - this.liveOpponentHead.x; + const dz = own.z - this.liveOpponentHead.z; + const flat = Math.hypot(dx, dz); + apart = Math.hypot(dx, own.y - this.liveOpponentHead.y, dz); + if (apart < HEAD_SPACE && flat > 1e-4) { + // The lean builds while the heads are too close, in his own frame: x to his side, y ahead of him. + const push = ((HEAD_SPACE - apart) / HEAD_SPACE) * HEAD_SPACE_RATE * dt; + const cos = Math.cos(-this.yaw); + const sin = Math.sin(-this.yaw); + this.headSpace.x += ((dx * cos + dz * sin) / flat) * push; + this.headSpace.y += ((-dx * sin + dz * cos) / flat) * push; + if (this.headSpace.lengthSq() > 1) this.headSpace.normalize(); + } + } + if (apart > HEAD_SPACE_CLEAR) this.headSpace.multiplyScalar(Math.exp(-3 * dt)); this.tauntEnd = latchEndTick(this.tauntEnd, sampledTick, fighter.taunt_ticks > 0 ? fighter.taunt_ticks : null); springStep(this.headKick, dt, 190, 7.5, 0.24); @@ -1067,7 +1531,8 @@ export class BoxingGraph { } this.guardKick = Math.max(0, this.guardKick - dt * 2.4); - this.updateDownState(fighter, dt, speed); + this.ragdoll?.tick(dt); + this.updateDownState(fighter, dt, reducedMotion, ownSpeed); const bounceTempo = (1.9 - this.tired * 0.7) * (1 - this.stunAmount * 0.6); this.bouncePhase += dt * bounceTempo * Math.PI * 2 * motionScale; @@ -1130,6 +1595,11 @@ export class BoxingGraph { const rearTarget = this.scratchB.copy(STANCE.relaxedRear).lerp(STANCE.guardHighRear, this.guardHigh).lerp(STANCE.guardLowRear, this.guardLow); leadTarget.y -= this.tired * 0.12 + this.stunAmount * 0.22; rearTarget.y -= this.tired * 0.1 + this.stunAmount * 0.2; + // A spent guard, raised or not, sags from the cheekbones toward the chest: the opening shows. + leadTarget.y -= this.guardSpent * GUARD_SPENT_DROP; + rearTarget.y -= this.guardSpent * GUARD_SPENT_DROP; + leadTarget.z -= this.guardSpent * 0.04; + rearTarget.z -= this.guardSpent * 0.03; leadTarget.z += this.feint * 0.1 + Math.sin(time * 2.3) * 0.012 * motionScale; leadTarget.x += Math.sin(time * 1.9) * 0.01 * motionScale; rearTarget.y += Math.cos(time * 2.7) * 0.01 * motionScale; @@ -1180,6 +1650,48 @@ export class BoxingGraph { torso.headPitch -= p * 0.1; } + // Inside, the fighter stands taller with the guard tucked in and the head off the centre line, + // so two fighters chest to chest do not pass through each other. + // Throwing, he steps into the punch out of the tall, tucked stance, so it reaches the contact point the engine hit. + this.punchCommit = smooth(this.punchCommit, this.punchActive ? 1 : 0, 10, dt); + const inside = this.crowding * (1 - this.clinchWeight) * (1 - this.punchCommit); + if (inside > 0.001) { + torso.headOffset.x -= inside * 0.075; + torso.headOffset.z -= inside * 0.05; + torso.headYaw -= inside * 0.2; + torso.spinePitch -= inside * 0.07; + torso.hips.z -= inside * 0.05; + leadHand.position.z -= inside * 0.14; + leadHand.position.x -= inside * 0.035 * mirror; + rearHand.position.z -= inside * 0.07; + } + + // Heads never pass through each other: the one too close leans away from the other's. Tied up in a + // clinch, which already rests the heads on opposite shoulders, only the head moves. + if (Math.abs(this.headSpace.x) + Math.abs(this.headSpace.y) > 0.001) { + const side = this.headSpace.x; + const ahead = this.headSpace.y; + const free = 1 - this.clinchWeight; + torso.spinePitch += ahead * 0.7 * free; + torso.spineRoll -= side * 0.7 * free; + torso.hips.z += ahead * 0.1 * free; + torso.hips.x += side * 0.05 * free; + torso.headOffset.x += side * 0.05; + torso.headOffset.z += ahead * 0.05; + torso.headPitch += ahead * 0.2; + torso.headRoll -= side * 0.3; + } + + // Drawn in off the ropes (see holdOnTheRopes), he leans back on them. + this.ropeLean = smooth(this.ropeLean, this.referee || this.downState !== "up" ? 0 : this.ropeLeanTarget, ROPE_LEAN_RATE, dt); + if (this.ropeLean > 0.001) { + // The way to the ropes in his own frame: x to his left, z ahead of him. + const toRopes = this.ropeLocal.copy(this.ropeOutward).applyAxisAngle(worldUpVector, -this.yaw); + // The whole body tips back from the hips, as against a wall. + torso.hipsPitch += toRopes.z * ROPE_LEAN_PITCH * this.ropeLean; + torso.hipsRoll -= toRopes.x * ROPE_LEAN_ROLL * this.ropeLean; + } + // Reactions. torso.headOffset.add(this.headKick.value); torso.headPitch += -this.headKick.value.z * 2.4 + this.headKick.value.y * 1.6; @@ -1247,22 +1759,50 @@ export class BoxingGraph { hand.palm.set(-side, 0, 0.2); } + if (this.winded > 0.001 && (this.downState === "up" || sinking)) this.applyWindedPose(this.winded, time, mirror, leadHand, rearHand); + // Knockdown overrides everything above. if (this.touchWeight > 0.001 && this.downState === "up") this.applyTouchGlovesPose(this.touchWeight, mirror, leadHand, rearHand); if (this.celebration > 0.001 && this.downState === "up") this.applyCelebratePose(this.celebration, time, mirror, leadHand, rearHand, lead, rear); if (this.wave > 0.001 && this.downState === "up") this.applyWaveOffPose(this.wave, time, mirror, leadHand, rearHand); + if (this.squareWeight > 0.001 && this.downState === "up") this.applyVerdictPose(this.squareWeight, time, mirror, leadHand, rearHand, lead, rear, headRest); + if ((this.wristWeights[0] > 0.001 || this.wristWeights[1] > 0.001) && this.downState === "up") this.applyRaisePose(mirror, leadHand, rearHand); if (this.breakWeight > 0.001 && this.downState === "up") this.applyBreakPose(this.breakWeight, mirror, leadHand, rearHand); if (this.attendWeight > 0.001 && this.downState === "up") this.applyAttendPose(this.attendWeight, time, mirror, leadHand, rearHand); if (this.treatWeight > 0.001 && this.downState === "up") this.applyTreatPose(this.treatWeight, time, mirror, leadHand, rearHand, lead, rear); if (this.seated > 0.001 && this.downState === "up") this.applySeatedPose(this.seated, time, mirror, leadHand, rearHand, lead, rear); if (this.downState !== "up") this.applyDownPose(mirror, leadHand, rearHand, lead, rear); + this.steadyJointPoles(mirror, leadHand, rearHand, lead, rear, dt); crouch = 0; void crouch; const pose = this.pose as { shrugL: number; shrugR: number }; pose.shrugL = mirror > 0 ? shrugLead : shrugRear; pose.shrugR = mirror > 0 ? shrugRear : shrugLead; - this.solver.apply(boxer.root, this.pose); + const ragdoll = this.ragdoll; + if (ragdoll?.active === true && reducedMotion) { + ragdoll.stop(); + ragdoll.clearRise(); + } + if (ragdoll?.active === true && !ragdoll.handingOver) { + ragdoll.setLost(boxer.isDecapitated, boxer.isHandDismembered("left"), boxer.isHandDismembered("right")); + ragdoll.update(dt, this.obstacle); + } else { + this.solver.apply(boxer.root, this.pose); + if (ragdoll?.handingOver === true) { + ragdoll.followPose(dt, this.obstacle); + // On all fours the physics lets go, and what it left short of the get-up pose blends out from there. + if (this.riseProgress >= RISE_FOURS && ragdoll.handOverSeconds >= RISE_HANDOVER_SECONDS) { + ragdoll.letGo(); + this.riseBlendAge = 0; + } + } else if (ragdoll !== null && ragdoll.hasRisePose) { + const settled = smoothstep(0, RISE_BLEND_SECONDS, this.riseBlendAge); + if (settled < 1 && this.downState !== "up") ragdoll.blendRise(settled); + else ragdoll.clearRise(); + } + ragdoll?.sample(dt); + } this.applyDislocation(); const opponentBlood = Math.min( @@ -1270,7 +1810,8 @@ export class BoxingGraph { (opponent.trauma.bleeding + opponent.trauma.left_cut + opponent.trauma.right_cut) / 620 * (blood === "off" ? 0 : blood === "reduced" ? 0.3 : 1.5), ); - boxer.gloveGear.color.copy(boxer.gearBaseColor).lerp(BLOODED_GLOVE_COLOR, opponentBlood); + boxer.setGloveBlood(opponentBlood); + boxer.setTrunksBlood(trunksBloodFor(fighter.trauma, blood)); applyHeadTrauma(boxer.headInjury, fighter.trauma, blood); applyBodyTrauma(boxer.bodyInjury, fighter.trauma, blood); boxer.setSkinClearcoat(0.25 + (1 - stamina) * 0.4); @@ -1301,21 +1842,27 @@ export class BoxingGraph { /** * Tie-up: the fighter whose id sorts first hooks over the opponent's arms, - * the other digs under them around the ribs. Both lean in over the bladed - * chest line and rest the head to their own right, so the skulls pass on - * opposite shoulders, and both keep working for position while they hold. + * the other digs under them around the ribs. Both square up chest to chest, + * lean in and rest the head to their own right, so the skulls pass on + * opposite shoulders whatever their stances, and both keep working for + * position while they hold. */ private applyClinchPose(c: number, time: number, mirror: number, fighter: FighterSnapshot, opponent: FighterSnapshot, leadHand: HandTarget, rearHand: HandTarget, headRest: THREE.Vector3): void { const torso = this.torso; const over = fighter.player_id < opponent.player_id; const struggle = Math.sin(time * 7.3) * 0.5 + Math.sin(time * 4.1 + 1.2) * 0.5; + // Held on the ropes, the two are drawn in together (see holdOnTheRopes). const centre = this.clinchCentre - .set(this.mapping.x(opponent.x) - (this.rootX ?? 0), 0, this.mapping.z(opponent.y) - this.rootZ) + .set(this.mapping.x(opponent.x) - this.ropeHold.x - (this.rootX ?? 0), 0, this.mapping.z(opponent.y) - this.ropeHold.z - this.rootZ) .applyQuaternion(this.scratchQ.setFromAxisAngle(worldUpVector, -this.yaw)); if (centre.lengthSq() < 0.04) centre.set(0, 0, 0.5); const forward = this.clinchForward.copy(centre).normalize(); const acrossX = forward.z; const acrossZ = -forward.x; + // Bladed, a lean in takes the head toward the rear hand's side: an orthodox fighter's to his right, a + // southpaw's to his left, into the head of an orthodox opponent. Squared up, both rest it to the right. + torso.hipsYaw *= 1 - c; + torso.shouldersYaw *= 1 - c; torso.spinePitch += c * (over ? 0.28 : 0.42) + struggle * 0.025 * c; torso.hips.z += c * (over ? 0.04 : 0.06); torso.hips.x += struggle * 0.02 * c; @@ -1323,7 +1870,8 @@ export class BoxingGraph { torso.hipsYaw += struggle * 0.06 * c; torso.headPitch += c * (over ? 0.15 : 0.3); torso.headYaw -= c * 0.3; - torso.headOffset.x -= c * 0.05; + torso.headOffset.x -= c * CLINCH_HEAD_SIDE; + torso.spineRoll += c * CLINCH_HEAD_LEAN; torso.headOffset.y -= c * (over ? 0.02 : 0.06); const metrics = this.boxer.rig.metrics; const reach = Math.min(metrics.armL.upper + metrics.armL.lower, metrics.armR.upper + metrics.armR.lower) * 0.92; @@ -1353,6 +1901,27 @@ export class BoxingGraph { } } + /** + * How far (world x and z) to draw this fighter in from `x`, `z`, where he would stand: the engine lets a + * fighter's middle reach 0.36 m past the rope line, further than the ropes give, so one it has past their + * furthest give is drawn in, his back on the ropes, and leans back on them (see ropeHoldAt). An opponent + * on top of him comes in with him, so the two keep the gap the engine has between them. + */ + private holdOnTheRopes(opponent: FighterSnapshot, x: number, z: number): THREE.Vector3 { + if (this.referee) { + this.ropeLeanTarget = 0; + return this.ropeHold.set(0, 0, 0); + } + this.ropeLeanTarget = ropeHoldAt(x, z, this.ropeExcess, this.ropeOwn); + const pressed = Math.hypot(this.ropeExcess.x, this.ropeExcess.z); + if (pressed > 0) this.ropeOutward.set(this.ropeExcess.x / pressed, 0, this.ropeExcess.z / pressed); + const opponentX = this.mapping.x(opponent.x); + const opponentZ = this.mapping.z(opponent.y); + ropeHoldAt(opponentX, opponentZ, this.ropeTheirs, this.ropeTheirs); + const close = 1 - smoothstep(ROPE_HOLD_CLOSE, ROPE_HOLD_OPEN, Math.hypot(opponentX - x, opponentZ - z)); + return this.ropeHold.set(heldIn(this.ropeOwn.x, this.ropeTheirs.x * close), 0, heldIn(this.ropeOwn.z, this.ropeTheirs.z * close)); + } + /** Character-space head rest position for hand offsets (before torso deltas). */ private headRestChar(mirror: number): THREE.Vector3 { const metrics = this.boxer.rig.metrics; @@ -1483,6 +2052,8 @@ export class BoxingGraph { const startup = this.punchTiming.startup; const authoritative = this.ownAuthoritativeAge; if (authoritative === null) return this.punchAgeTicks < startup ? startup / (startup + this.ownLeadTicks) : 1; + // The server has stopped reporting it, for a clinch or the bell: the age it last gave is stale. + if (this.actionId !== this.ownActionId) return 1; if (this.punchAgeTicks - authoritative <= 0.25) return 1; if (this.punchAgeTicks < startup && authoritative < startup) return clamp((startup - this.punchAgeTicks) / (startup - authoritative), 0.25, 1); return 0.5; @@ -1759,8 +2330,9 @@ export class BoxingGraph { } } - private updateDownState(fighter: FighterSnapshot, dt: number, speed: number): void { - if (fighter.is_downed) { + private updateDownState(fighter: FighterSnapshot, dt: number, reducedMotion: boolean, speed: number): void { + this.riseBlendAge += dt; + if (fighter.is_downed || this.forcedDown) { if (this.downState === "up" || this.downState === "rising") { // Knocked down again while getting up, he falls from where the get-up had him, not from standing. this.fallFromGetUp = this.downState === "rising"; @@ -1771,20 +2343,41 @@ export class BoxingGraph { this.downState = "falling"; this.fallAge = 0; this.riseProgress = 0; + this.riseRootFrom.copy(this.riseRoot); this.landSettled = false; + this.fallenFromPhysics = false; + this.downPolesSet = false; + // The knockout physics plays the fall, from the pose on screen (so a knockdown mid-rise falls from + // where he was), unless the animation does: a body shot to one knee, or reduced motion. + this.ragdoll?.clearRise(); + if (this.authoredNextFall) this.ragdoll?.forget(); + else if (!reducedMotion) this.startFall(fighter); + this.authoredNextFall = false; } else if (this.downState === "falling") { this.fallAge += dt; if (this.fallAge >= KNOCKDOWN_FALL_SECONDS) this.downState = "down"; + } else if (this.fallKneel) { + // Down on one knee after a body shot: the get-up from there is only its last stage. + this.riseProgress = RISE_KNEE; } else { - // Each good press of the get-up prompt pushes him further up; a bad one lets him sink back. - const meter = fighter.get_up_required > 0 ? clamp(fighter.get_up_meter / fighter.get_up_required, 0, 1) : 0; + // Each good press of the get-up prompt pushes him further up; a bad one lets him sink back. Knocked + // out, or once the bout is over, he stays down. + const trying = !this.forcedDown && !this.heldDown && fighter.get_up_required > 0; + const meter = trying ? clamp(fighter.get_up_meter / fighter.get_up_required, 0, 1) : 0; const target = meter * RISE_KNEE; + // The physics keeps the body until he starts to get up, then hands it to the animated get-up. + if (this.ragdoll?.active === true && !this.ragdoll.handingOver) { + if (target <= 0) return; + this.handOverFall(fighter); + } this.riseProgress += clamp(target - this.riseProgress, -RISE_SINK_RATE * dt, RISE_PUSH_RATE * dt); } return; } if (this.downState === "down" || this.downState === "falling") { this.downState = "rising"; + if (this.ragdoll?.active === true && !this.ragdoll.handingOver) this.handOverFall(fighter); + if (this.fallKneel) this.riseProgress = Math.max(this.riseProgress, RISE_KNEE); this.riseFrom = this.riseProgress; this.feetInitialized = false; } @@ -1793,7 +2386,225 @@ export class BoxingGraph { const hurried = speed > 0.3 || this.punchActive; const rate = (1 - this.riseFrom) / (hurried ? GETUP_HURRIED_SECONDS : GETUP_SECONDS); this.riseProgress = Math.min(1, this.riseProgress + (this.riseProgress < RISE_FOURS ? Math.min(rate, RISE_ROLL_RATE) : rate) * dt); - if (this.riseProgress >= 1) this.downState = "up"; + if (this.riseProgress >= 1) { + this.downState = "up"; + this.fallKneel = false; + } + } + + /** + * Starts the knockout physics. After a gap in drawing (the Activity hidden, a reconnect, a spectator joining + * mid-count) the fighter was last drawn where he stood then, which can be metres from his place: he falls at his + * place instead, and when the count is already running he is put down where that fall ends. + */ + private startFall(fighter: FighterSnapshot): void { + const ragdoll = this.ragdoll; + if (ragdoll === null) return; + if (this.rootX !== null && Math.hypot(this.place.x - this.rootX, this.place.z - this.rootZ) > FALL_ROOT_JUMP) { + this.rootX = this.place.x; + this.rootZ = this.place.z; + this.boxer.root.position.set(this.rootX, 0, this.rootZ); + } + ragdoll.start("crumple"); + if (fighter.get_up_count > 0) { + ragdoll.settle(); + this.fallAge = KNOCKDOWN_FALL_SECONDS; + } + } + + /** + * The knockout physics hands the body to the animated get-up: the down pose it starts from is read off + * the skeleton as the fall left it (the hips' place and turn, the spine, the gloves and the ankles), so + * the get-up begins exactly there, and the joints' own turns blend in over RISE_BLEND_SECONDS. + */ + private handOverFall(fighter: FighterSnapshot): void { + const ragdoll = this.ragdoll; + if (ragdoll === null || !ragdoll.active || ragdoll.handingOver) return; + const mirror = fighter.stance === "orthodox" ? 1 : -1; + const bones = this.boxer.rig.bones; + // The bones still carry the physics' last frame; read them in character space under this frame's root. + this.boxer.root.updateMatrixWorld(true); + const root = this.scratchC.set(this.rootX ?? 0, 0, this.rootZ); + const inverse = this.scratchQ.setFromAxisAngle(worldUpVector, -this.yaw); + const fallen = this.fallen; + const toCharacter = (bone: THREE.Bone, out: THREE.Vector3): THREE.Vector3 => out.setFromMatrixPosition(bone.matrixWorld).sub(root).applyQuaternion(inverse); + // The poser builds the spine and head by adding to the hips' angles, so each turn is read as the angles nearest + // the ones it is built on: the hips' nearest the authored lying pose's, the chest's nearest the hips', the head's + // nearest the chest's. + const turnOf = (bone: THREE.Bone, pitch: number, yaw: number, roll: number): THREE.Euler => + nearestTurn(downEuler.setFromQuaternion(bone.getWorldQuaternion(fallenTurn).premultiply(inverse), "YXZ"), pitch, yaw, roll); + toCharacter(bones.hips, fallen.hips); + this.landProne = ragdoll.faceDown(); + const side = Math.abs(fallen.hips.x) > 0.12 ? Math.sign(fallen.hips.x) : 0; + const authored = this.landProne + ? placeDownPose(this.downTo, PRONE, PRONE_TWIST, mirror, side, this.boxer.rig.metrics.ankleHeight) + : placeDownPose(this.downTo, LYING, LYING_TWIST, mirror, side, this.boxer.rig.metrics.ankleHeight); + this.landSide = side; + this.landShift.set(fallen.hips.x - authored.hips.x, 0, fallen.hips.z - authored.hips.z); + const hips = turnOf(bones.hips, authored.hipsPitch, authored.hipsYaw, authored.hipsRoll); + fallen.hipsPitch = hips.x; + fallen.hipsYaw = hips.y; + fallen.hipsRoll = hips.z; + // The chest's turn is the upper chest's whole turn on the hips, its side bend included: dropped, the get-up's + // first frame drew the head and shoulders 30-70 cm from where the fall had left them. + const chest = turnOf(bones.upperChest, fallen.hipsPitch, fallen.hipsYaw, fallen.hipsRoll); + fallen.shouldersYaw = chest.y; + fallen.spinePitch = chest.x - fallen.hipsPitch; + fallen.spineRoll = chest.z - fallen.hipsRoll; + const head = turnOf(bones.head, chest.x, chest.y, chest.z); + fallen.headPitch = head.x; + fallen.turn = head.y; + fallen.headRoll = head.z; + toCharacter(mirror > 0 ? bones.gloveL : bones.gloveR, fallen.leadHand); + toCharacter(mirror > 0 ? bones.gloveR : bones.gloveL, fallen.rearHand); + toCharacter(mirror > 0 ? bones.ankleL : bones.ankleR, fallen.leadFoot); + toCharacter(mirror > 0 ? bones.ankleR : bones.ankleL, fallen.rearFoot); + // The get-up draws each foot sole down, so an ankle the fall left lower than a standing one (a foot on its side, + // or toes up) is put at the ankle's height: below it the foot went into the canvas. + const ankle = this.boxer.rig.metrics.ankleHeight; + fallen.leadFoot.y = Math.max(fallen.leadFoot.y, ankle); + fallen.rearFoot.y = Math.max(fallen.rearFoot.y, ankle); + // Each knee and elbow keeps the way it is bent: from the line between the limb's ends out to the joint. A limb + // lying nearly straight has no such line to speak of, so the way the joint faces (the kneecap, the back of the + // elbow) settles it: read off a straight limb's noise, a knee could bend into the canvas and then flip over. + const bend = (root: THREE.Bone, joint: THREE.Bone, end: THREE.Bone, out: THREE.Vector3, faces: THREE.Vector3): void => { + const from = toCharacter(root, this.scratchB); + const along = toCharacter(end, this.scratchD).sub(from).normalize(); + toCharacter(joint, out).sub(from); + out.addScaledVector(downScratch.copy(faces).applyQuaternion(root.getWorldQuaternion(fallenTurn)).applyQuaternion(inverse), STRAIGHT_LIMB_BEND); + out.addScaledVector(along, -out.dot(along)); + if (out.lengthSq() > 1e-6) out.normalize(); + else out.copy(faces === KNEECAP ? downForward : downBelow); + }; + const left = mirror > 0; + bend(left ? bones.hipL : bones.hipR, left ? bones.kneeL : bones.kneeR, left ? bones.ankleL : bones.ankleR, fallen.leadKnee, KNEECAP); + bend(left ? bones.hipR : bones.hipL, left ? bones.kneeR : bones.kneeL, left ? bones.ankleR : bones.ankleL, fallen.rearKnee, KNEECAP); + bend(left ? bones.shoulderL : bones.shoulderR, left ? bones.elbowL : bones.elbowR, left ? bones.gloveL : bones.gloveR, fallen.leadElbow, left ? ELBOW_BACK_L : ELBOW_BACK_R); + bend(left ? bones.shoulderR : bones.shoulderL, left ? bones.elbowR : bones.elbowL, left ? bones.gloveR : bones.gloveL, fallen.rearElbow, left ? ELBOW_BACK_R : ELBOW_BACK_L); + // The gloves as they lie: knuckles along the bone's y axis, the palm along its z axis. + const glove = (bone: THREE.Bone, knuckles: THREE.Vector3, palm: THREE.Vector3): void => { + const turn = bone.getWorldQuaternion(fallenTurn).premultiply(inverse); + knuckles.set(0, 1, 0).applyQuaternion(turn); + palm.set(0, 0, 1).applyQuaternion(turn); + }; + glove(left ? bones.gloveL : bones.gloveR, fallen.leadKnuckles, fallen.leadPalm); + glove(left ? bones.gloveR : bones.gloveL, fallen.rearKnuckles, fallen.rearPalm); + fallen.ownJoints = 1; + fallen.leadHeel = 0; + fallen.rearHeel = 0; + fallen.palmsDown = this.landProne ? 1 : 0; + this.fallenFromPhysics = true; + this.landSettled = true; + this.downPolesSet = false; + ragdoll.handOver(); + this.riseBlendAge = 0; + // The drawn fighter's root moves under the body, which gets up where it lies; the fitted pose and the pose + // the physics left move with it the other way, so nothing jumps. He steps back to his place as he rises. + const shift = this.scratchE.set(ragdoll.pelvis(this.scratchD).x - (this.rootX ?? 0), 0, this.scratchD.z - this.rootZ); + const local = this.scratchB.copy(shift).applyQuaternion(inverse); + for (const point of [fallen.hips, fallen.leadHand, fallen.rearHand, fallen.leadFoot, fallen.rearFoot]) { + point.x -= local.x; + point.z -= local.z; + } + ragdoll.shiftRise(this.scratchC.copy(shift).negate()); + this.rootX = (this.rootX ?? 0) + shift.x; + this.rootZ += shift.z; + this.boxer.root.position.set(this.rootX, 0, this.rootZ); + this.riseRoot.set(this.rootX - this.mapping.x(fighter.x) - this.touchOffset.x, 0, this.rootZ - this.mapping.z(fighter.y) - this.touchOffset.z); + this.riseRootFrom.copy(this.riseRoot); + } + + /** + * Getting up from where the physics took the body, he steps back to his place in the engine as he rises off his + * knee and stands up on it. Walking back only once he was up left him drawn up to 0.6 m from the place the + * opponent's punches aim at for half a second after the server had him fighting again. + */ + private walkBackFromTheFall(dt: number): void { + const share = this.downState === "rising" ? 1 - smoothstep(RISE_KNEE, 1, this.riseProgress) : this.downState === "up" ? 0 : 1; + const x = this.riseRootFrom.x * share; + const z = this.riseRootFrom.z * share; + if (dt > 0) this.riseRootVelocity.set((x - this.riseRoot.x) / dt, 0, (z - this.riseRoot.z) / dt); + else this.riseRootVelocity.set(0, 0, 0); + this.riseRoot.set(x, 0, z); + if (share === 0) this.riseRootFrom.set(0, 0, 0); + } + + /** + * Down and getting up, a knee or an elbow whose pole has to come a long way round (from how the physics left the limb + * to how the get-up has it) swings round the limb at POLE_TURN_RATE rather than straight across: a pole crossing the + * line of the limb flipped the knee to its other side in one frame, 62 cm. Each limb's line runs from its hip or + * shoulder as last drawn to where its foot or glove goes now. + */ + private steadyJointPoles(mirror: number, leadHand: HandTarget, rearHand: HandTarget, lead: FootTarget, rear: FootTarget, dt: number): void { + this.poleSettle = this.downState !== "up" ? POLE_SETTLE_SECONDS : Math.max(0, this.poleSettle - dt); + if (this.poleSettle <= 0 || (this.ragdoll?.active === true && !this.ragdoll.handingOver)) { + this.downPolesSet = false; + return; + } + const bones = this.boxer.rig.bones; + const left = mirror > 0; + // The skeleton and the root's matrix are both still last frame's, so together they give last frame's character space. + this.characterInverse.copy(this.boxer.root.matrixWorld).invert(); + // Lead knee, rear knee, lead elbow, rear elbow. + for (let index = 0; index < 4; index += 1) { + const leg = index < 2; + const leading = index % 2 === 0; + const onLeft = leading === left; + const root = leg ? (onLeft ? bones.hipL : bones.hipR) : (onLeft ? bones.shoulderL : bones.shoulderR); + const target = leg ? (leading ? lead : rear) : (leading ? leadHand : rearHand); + const end = target.position; + const pole = target.pole; + const last = this.downPoles[index]!; + if (!this.downPolesSet) { + last.copy(pole); + continue; + } + const axis = this.scratchB.setFromMatrixPosition(root.matrixWorld).applyMatrix4(this.characterInverse).sub(end).negate(); + if (axis.lengthSq() < 1e-6) { + last.copy(pole); + continue; + } + axis.normalize(); + // Both poles seen across the limb; the drawn one turns toward the wanted one about the limb, the short way, and only so fast. + const from = this.scratchD.copy(last).addScaledVector(axis, -last.dot(axis)); + const to = this.scratchE.copy(pole).addScaledVector(axis, -pole.dot(axis)); + if (from.lengthSq() < 1e-4 || to.lengthSq() < 1e-4) { + last.copy(pole); + continue; + } + const angle = Math.atan2(this.scratch.crossVectors(from, to).dot(axis), from.dot(to)); + const most = POLE_TURN_RATE * Math.max(dt, 1 / 120); + if (Math.abs(angle) <= most) { + last.copy(pole); + continue; + } + pole.copy(from.normalize().applyAxisAngle(axis, Math.sign(angle) * most)); + last.copy(pole); + } + this.downPolesSet = true; + } + + /** Folded over a body shot: knees giving, the glove on the struck side clamped over the ribs, the other hanging. */ + private applyWindedPose(blend: number, time: number, mirror: number, leadHand: HandTarget, rearHand: HandTarget): void { + const torso = this.torso; + const lerp = THREE.MathUtils.lerp; + const side = this.windedSide; + const shudder = Math.sin(time * 23) * 0.006; + torso.hips.y = lerp(torso.hips.y, STANCE.hipsHeight - 0.1, blend); + torso.hips.z = lerp(torso.hips.z, -0.04, blend); + torso.hipsPitch = lerp(torso.hipsPitch, 0.2, blend); + torso.spinePitch = lerp(torso.spinePitch, 0.62 + shudder, blend); + torso.spineRoll = lerp(torso.spineRoll, side * 0.16, blend); + torso.headPitch = lerp(torso.headPitch, 0.38, blend); + const clutch = side > 0 ? this.hand.L : this.hand.R; + const free = clutch === leadHand ? rearHand : leadHand; + clutch.position.lerp(seatedScratch.set(side * 0.13, 0.98, 0.17), blend); + clutch.palm.lerp(seatedScratch.set(-side * 0.6, 0, -0.8), blend).normalize(); + clutch.knuckles.lerp(seatedScratch.set(-side * 0.5, 0.5, 0.2), blend).normalize(); + clutch.pole.lerp(seatedScratch.set(side * 0.9, -0.3, -0.3), blend).normalize(); + free.position.lerp(seatedScratch.set(-side * 0.2, 0.86, 0.3), blend); + free.pole.lerp(seatedScratch.set(-side * 0.8, -0.6, 0), blend).normalize(); + void mirror; } private applyTouchGlovesPose( @@ -1927,10 +2738,13 @@ export class BoxingGraph { rear.toe.lerp(this.treatScratch.set(-0.1 * mirror, 0, 1), blend).normalize(); lead.pole.lerp(this.treatScratch.set(0.25 * mirror, 0.4, 1), blend).normalize(); rear.pole.lerp(this.treatScratch.set(-0.2 * mirror, 0.4, 1), blend).normalize(); - const reach = 0.08 - press; - leadHand.position.lerp(this.treatScratch.set(eye.x + facing.x * reach, eye.y - 0.07, eye.z + facing.z * reach), blend); - leadHand.knuckles.lerp(this.treatScratch.set(0, 1, 0.05), blend).normalize(); - leadHand.palm.lerp(this.treatScratch.set(-facing.x, 0, -facing.z), blend).normalize(); + // The enswell's plate sits above the fist and presses on the eye; the bottle points from the fist into the mouth. + const bottle = this.treatProp === "bottle"; + const reach = bottle ? 0.3 : 0.08 - press; + leadHand.position.lerp(this.treatScratch.set(eye.x + facing.x * reach, eye.y + (bottle ? 0.02 : -0.07), eye.z + facing.z * reach), blend); + if (bottle) leadHand.knuckles.lerp(this.treatScratch.set(-facing.x, -0.25, -facing.z), blend).normalize(); + else leadHand.knuckles.lerp(this.treatScratch.set(0, 1, 0.05), blend).normalize(); + leadHand.palm.lerp(bottle ? this.treatScratch.set(0, -1, 0) : this.treatScratch.set(-facing.x, 0, -facing.z), blend).normalize(); leadHand.pole.lerp(this.treatScratch.set(0.85 * mirror, -0.35, 0.1), blend).normalize(); const jaw = this.treatSide * 0.135; rearHand.position.lerp(this.treatScratch.set(eye.x - leftX * jaw + facing.x * 0.05, eye.y - 0.2, eye.z - leftZ * jaw + facing.z * 0.05), blend); @@ -1977,6 +2791,64 @@ export class BoxingGraph { rearHand.pole.lerp(seatedScratch.set(-0.2 * mirror, -1, 0.1), blend).normalize(); } + /** Standing square beside the referee for the decision, then the arm on the referee's side going up or the head going down. */ + private applyVerdictPose( + blend: number, + time: number, + mirror: number, + leadHand: HandTarget, + rearHand: HandTarget, + lead: FootTarget, + rear: FootTarget, + headRest: THREE.Vector3, + ): void { + const torso = this.torso; + const lerp = THREE.MathUtils.lerp; + const raised = this.raisedWeight; + const bowed = this.bowedWeight; + const breath = Math.sin(time * 1.9) * 0.008; + torso.hips.x = lerp(torso.hips.x, this.verdictSide * 0.02 * raised, blend); + torso.hips.y = lerp(torso.hips.y, STANCE.hipsHeight + 0.03 + raised * 0.01 - bowed * 0.015, blend); + torso.hips.z = lerp(torso.hips.z, 0, blend); + torso.hipsYaw = lerp(torso.hipsYaw, 0, blend); + torso.hipsPitch = lerp(torso.hipsPitch, 0, blend); + torso.shouldersYaw = lerp(torso.shouldersYaw, 0, blend); + torso.spinePitch = lerp(torso.spinePitch, 0.05 - raised * 0.1 + bowed * 0.12, blend); + torso.spineRoll = lerp(torso.spineRoll, -this.verdictSide * 0.06 * raised, blend); + torso.headYaw = lerp(torso.headYaw, 0, blend); + torso.headPitch = lerp(torso.headPitch, 0.04 - raised * 0.2 + bowed * 0.34, blend); + lead.toe.lerp(seatedScratch.set(0.12 * mirror, 0, 1), blend).normalize(); + rear.toe.lerp(seatedScratch.set(-0.12 * mirror, 0, 1), blend).normalize(); + lead.pole.lerp(seatedScratch.set(0.12 * mirror, 0.3, 1), blend).normalize(); + rear.pole.lerp(seatedScratch.set(-0.12 * mirror, 0.3, 1), blend).normalize(); + for (const [hand, side] of [[leadHand, mirror], [rearHand, -mirror]] as const) { + const lifted = side === this.verdictSide ? raised : 0; + hand.position.lerp(seatedScratch.set(side * (0.27 + lifted * 0.11), lerp(0.88 + breath, headRest.y + 0.44, lifted), lerp(0.12, 0.05, lifted)), blend); + hand.knuckles.lerp(seatedScratch.set(side * 0.15, lerp(-0.9, 1, lifted), lerp(0.35, 0, lifted)), blend).normalize(); + hand.palm.lerp(seatedScratch.set(lerp(-0.9 * side, 0, lifted), 0.1, lerp(0.35, 1, lifted)), blend).normalize(); + hand.pole.lerp(seatedScratch.set(side * lerp(0.5, 1, lifted), lerp(-0.4, 0.1, lifted), lerp(-0.8, -0.3, lifted)), blend).normalize(); + } + } + + /** Referee: a hand around each wrist it has been given, just below the glove. */ + private applyRaisePose(mirror: number, leadHand: HandTarget, rearHand: HandTarget): void { + const root = this.treatScratch.set(this.rootX ?? 0, 0, this.rootZ); + const inverse = this.scratchQ.setFromAxisAngle(worldUpVector, -this.yaw); + for (const [index, side] of [[0, 1], [1, -1]] as const) { + const wrist = this.wrists[index] ?? this.wristTargets[index]; + const weight = this.wristWeights[index]; + if (weight <= 0.001) continue; + const hand = side === mirror ? leadHand : rearHand; + const held = seatedScratch.copy(wrist).sub(root).applyQuaternion(inverse); + held.y -= 0.17; + hand.position.lerp(held, weight); + hand.knuckles.lerp(seatedScratch.set(0, 1, 0), weight).normalize(); + hand.palm.lerp(seatedScratch.set(side, 0, 0.2), weight).normalize(); + hand.pole.lerp(seatedScratch.set(side * 0.6, -0.5, -0.6), weight).normalize(); + } + this.torso.headPitch = THREE.MathUtils.lerp(this.torso.headPitch, -0.05, Math.max(this.wristWeights[0], this.wristWeights[1])); + } + private applyWaveOffPose( blend: number, time: number, @@ -1990,8 +2862,10 @@ export class BoxingGraph { torso.hipsPitch = lerp(torso.hipsPitch, 0.08, blend); torso.spinePitch = lerp(torso.spinePitch, 0.05, blend); torso.headPitch = lerp(torso.headPitch, -0.1, blend); - leadHand.position.lerp(seatedScratch.set(0.42 * sweep * mirror, 1.66 + Math.abs(sweep) * 0.08, 0.34), blend); - rearHand.position.lerp(seatedScratch.set(-0.42 * sweep * mirror, 1.62 + Math.abs(sweep) * 0.08, 0.24), blend); + // Arms all but straight, crossing below the chin and swinging wide past the shoulders, so the call + // reads from any side and the hands never come up over the face. + leadHand.position.lerp(seatedScratch.set(0.62 * sweep * mirror, 1.3 + Math.abs(sweep) * 0.1, 0.44), blend); + rearHand.position.lerp(seatedScratch.set(-0.62 * sweep * mirror, 1.24 + Math.abs(sweep) * 0.1, 0.36), blend); leadHand.palm.lerp(seatedScratch.set(0, 0, 1), blend).normalize(); rearHand.palm.lerp(seatedScratch.set(0, 0, 1), blend).normalize(); leadHand.knuckles.lerp(seatedScratch.set(0, 1, 0), blend).normalize(); @@ -2070,9 +2944,13 @@ export class BoxingGraph { // Down and getting up: lying -> all fours -> one knee -> stand, as far as the get-up has come. const u = this.riseProgress; let stand = 0; - if (u < RISE_FOURS) { + if (this.fallKneel) { + // On one knee after a body shot he rises from there straight to his feet. + stand = smoothstep(RISE_KNEE, 1, u); + lerpDownPose(pose, this.placeKneel(this.downFrom, mirror), standing, stand); + } else if (u < RISE_FOURS) { const s = smoothstep(0, RISE_FOURS, u); - const lying = this.placeLying(mirror); + const lying = this.fallenFromPhysics ? this.fallen : this.placeLying(mirror); const fours = this.placeDown(this.downTo, ALL_FOURS, mirror); lerpDownPose(pose, lying, fours, s); // Face down, he pushes up on his gloves before the knees come under him: the hips rise first. @@ -2084,9 +2962,10 @@ export class BoxingGraph { lerpDownPose(pose, this.placeDown(this.downFrom, ONE_KNEE, mirror), standing, stand); } this.writeDown(pose, mirror, leadHand, rearHand, lead, rear, 0, stand); + if (this.fallKneel) this.orientKneel(1 - stand, mirror, leadHand, rearHand, lead, rear); return; } - const lying = this.placeLying(mirror); + const lying = this.fallKneel ? this.placeKneel(this.downFrom, mirror) : this.placeLying(mirror); const u = clamp(this.fallAge / KNOCKDOWN_FALL_SECONDS, 0, 1); const t = easeIn(u, 2.1); const start = this.fallFromGetUp ? this.fallStart : standing; @@ -2099,7 +2978,9 @@ export class BoxingGraph { pose.rearFoot.lerpVectors(start.rearFoot, lying.rearFoot, feet); pose.leadHeel = THREE.MathUtils.lerp(start.leadHeel, lying.leadHeel, feet); pose.rearHeel = THREE.MathUtils.lerp(start.rearHeel, lying.rearHeel, feet); - if (this.landProne) { + if (this.fallKneel) { + // Sinking to the knee he stays folded over the shot, the gloves on the ribs and the front knee. + } else if (this.landProne) { // Pitching forward, the gloves reach out ahead of him to break the fall. const reach = easeOut(u, 2.5); pose.leadHand.lerpVectors(start.leadHand, lying.leadHand, reach); @@ -2111,6 +2992,32 @@ export class BoxingGraph { } const stand = (this.fallFromGetUp ? this.fallStartStand : 1) * (1 - t); this.writeDown(pose, mirror, leadHand, rearHand, lead, rear, Math.sin(u * Math.PI) * 0.12, stand); + if (this.fallKneel) this.orientKneel(t, mirror, leadHand, rearHand, lead, rear); + } + + /** + * Taking a knee after a body shot: the rear knee on the canvas, folded over the shot with that side's + * glove clamped on the ribs and the other glove braced on the front knee. + */ + private placeKneel(out: DownPose, mirror: number): DownPose { + const struck = this.windedSide; + const clutchLead = (struck > 0) === (mirror > 0); + out.hips.set(0.02 * mirror, 0.53, -0.06); + out.hipsYaw = STANCE.bladeYaw * mirror * 0.3; + out.hipsPitch = 0.24; + out.hipsRoll = 0; + out.shouldersYaw = STANCE.bladeYaw * mirror * 0.2; + out.spinePitch = 0.62; + out.headPitch = 0.48; + (clutchLead ? out.leadHand : out.rearHand).set(struck * 0.13, 0.66, 0.2); + (clutchLead ? out.rearHand : out.leadHand).set(0.13 * mirror, 0.66, 0.37); + // Ankle positions: the lead foot flat on the canvas, the rear one behind the knee on its toes. + out.leadFoot.set(0.16 * mirror, 0.125, 0.32); + out.rearFoot.set(-0.12 * mirror, 0.12, -0.5); + out.leadHeel = 0; + out.rearHeel = 0; + out.palmsDown = 1; + return out; } /** The pose the fighter ends up in on the canvas: face down or on his back, twisted by the fall side. */ @@ -2178,6 +3085,25 @@ export class BoxingGraph { return out.set(fitInsideRopes(minX, maxX), 0, fitInsideRopes(minZ, maxZ)).applyQuaternion(turn.invert()); } + /** The knees, feet and gloves of the kneel, which the lying poses' orientations do not fit. */ + private orientKneel(weight: number, mirror: number, leadHand: HandTarget, rearHand: HandTarget, lead: FootTarget, rear: FootTarget): void { + if (weight <= 0.001) return; + const struck = this.windedSide; + const clutchLead = (struck > 0) === (mirror > 0); + const clutch = clutchLead ? leadHand : rearHand; + const brace = clutchLead ? rearHand : leadHand; + lead.pole.lerp(seatedScratch.set(0.15 * mirror, 0.35, 1), weight).normalize(); + rear.pole.lerp(seatedScratch.set(-0.1 * mirror, -1, 0.3), weight).normalize(); + lead.toe.lerp(seatedScratch.set(0.15 * mirror, 0, 1), weight).normalize(); + rear.toe.lerp(seatedScratch.set(0, -0.35, -1), weight).normalize(); + clutch.palm.lerp(seatedScratch.set(-struck * 0.75, 0, -0.65), weight).normalize(); + clutch.knuckles.lerp(seatedScratch.set(-struck * 0.35, 0.25, 0.9), weight).normalize(); + clutch.pole.lerp(seatedScratch.set(struck * 0.9, -0.35, -0.25), weight).normalize(); + brace.palm.lerp(seatedScratch.set(0, -1, 0.1), weight).normalize(); + brace.knuckles.lerp(seatedScratch.set(0.2 * mirror, -0.2, 1), weight).normalize(); + brace.pole.lerp(seatedScratch.set(mirror * 0.7, 0.1, -0.7), weight).normalize(); + } + /** * Writes a down or rising pose over the targets. `stand` is the share of the live standing pose in * it: what the standing layers set (the guard's arm and knee directions, reactions) fades out on the @@ -2202,10 +3128,10 @@ export class BoxingGraph { torso.hipsRoll = state.hipsRoll; torso.shouldersYaw = state.shouldersYaw; torso.spinePitch = state.spinePitch; - torso.spineRoll *= stand; + torso.spineRoll = torso.spineRoll * stand + state.spineRoll; torso.headPitch = state.headPitch; - torso.headYaw *= stand; - torso.headRoll *= stand; + torso.headYaw = torso.headYaw * stand + state.turn; + torso.headRoll = torso.headRoll * stand + state.headRoll; torso.headOffset.multiplyScalar(stand); torso.headOffset.z -= headLag; leadHand.position.copy(state.leadHand); @@ -2222,6 +3148,18 @@ export class BoxingGraph { const forward = downScratch.set(0, 0, 1).applyEuler(downEuler.set(state.hipsPitch, state.hipsYaw, state.hipsRoll, "YXZ")); lead.pole.lerp(forward, down); rear.pole.lerp(forward, down); + if (state.ownJoints > 0) { + // Turned round the limb rather than mixed: a joint bent the other way from the authored one swings over. + const own = state.ownJoints * down; + turnToward(lead.pole, state.leadKnee, own); + turnToward(rear.pole, state.rearKnee, own); + turnToward(leadHand.pole, state.leadElbow, own); + turnToward(rearHand.pole, state.rearElbow, own); + turnToward(leadHand.knuckles, state.leadKnuckles, own); + turnToward(leadHand.palm, state.leadPalm, own); + turnToward(rearHand.knuckles, state.rearKnuckles, own); + turnToward(rearHand.palm, state.rearPalm, own); + } } private applyDislocation(): void { @@ -2236,6 +3174,7 @@ export class BoxingGraph { dispose(): void { this.stool.dispose(); this.enswell.dispose(); + this.bottle.dispose(); this.boxer.dispose(); } } @@ -2263,6 +3202,8 @@ const TOUCH_WALK_TOP_SPEED = 1.5; /** The engine's taunt length in ticks, over which the lead glove beckons TAUNT_BECKONS times. */ const TAUNT_TICKS = 60; const TAUNT_BECKONS = 4; +/** How far a spent guard's gloves sag, in metres: from the cheekbones to about the collarbones. */ +const GUARD_SPENT_DROP = 0.15; const seatedScratch = new THREE.Vector3(); const JAW_DISLOCATION = new THREE.Quaternion().setFromEuler(new THREE.Euler(0.08, 0.2, -0.22)); @@ -2274,7 +3215,10 @@ interface DownPose { hipsRoll: number; shouldersYaw: number; spinePitch: number; + /** The spine's and the head's side bend: only a body the knockout physics left has them; the authored poses have none. */ + spineRoll: number; headPitch: number; + headRoll: number; readonly leadHand: THREE.Vector3; readonly rearHand: THREE.Vector3; /** Ankle positions; the tables give the sole's height above the canvas instead. */ @@ -2285,9 +3229,34 @@ interface DownPose { rearHeel: number; /** 0 with the arms thrown out palm-up, 1 with the gloves pressed palm-down on the canvas. */ palmsDown: number; + /** The head's yaw on the canvas: only a body the knockout physics left has one; the authored poses look along the spine. */ + turn: number; + /** + * Where the knees and elbows point, held against the authored directions by `ownJoints`: a body the knockout + * physics left keeps its own, so the limbs solve the way they lie. + */ + readonly leadKnee: THREE.Vector3; + readonly rearKnee: THREE.Vector3; + readonly leadElbow: THREE.Vector3; + readonly rearElbow: THREE.Vector3; + /** And how the gloves are turned: the knuckles' and the palm's directions. */ + readonly leadKnuckles: THREE.Vector3; + readonly leadPalm: THREE.Vector3; + readonly rearKnuckles: THREE.Vector3; + readonly rearPalm: THREE.Vector3; + ownJoints: number; } const downScratch = new THREE.Vector3(); +const downForward = new THREE.Vector3(0, 0, 1); +const downBelow = new THREE.Vector3(0, -1, 0); +/** The way a knee faces in its thigh bone's own frame, and the back of each elbow in its upper arm's. */ +const KNEECAP = new THREE.Vector3(0, 0, 1); +const ELBOW_BACK_L = new THREE.Vector3(1, 0, 0); +const ELBOW_BACK_R = new THREE.Vector3(-1, 0, 0); +/** How much the way a joint faces counts against its bend, as metres of bend: it decides only for a limb lying nearly straight. */ +const STRAIGHT_LIMB_BEND = 0.04; +const fallenTurn = new THREE.Quaternion(); const downEuler = new THREE.Euler(); /** Arm directions on the canvas for an orthodox fighter's lead arm, thrown out palm-up or pressing palm-down. */ const PALM_UP = { pole: vec(0.4, 0.9, 0.2), knuckles: vec(0.6, 0.2, -0.75).normalize(), palm: vec(0, 1, 0.2).normalize() }; @@ -2314,7 +3283,9 @@ function downPose(pose: Partial = {}): DownPose { hipsRoll: 0, shouldersYaw: 0, spinePitch: 0, + spineRoll: 0, headPitch: 0, + headRoll: 0, leadHand: new THREE.Vector3(), rearHand: new THREE.Vector3(), leadFoot: new THREE.Vector3(), @@ -2322,6 +3293,16 @@ function downPose(pose: Partial = {}): DownPose { leadHeel: 0, rearHeel: 0, palmsDown: 0, + turn: 0, + leadKnee: new THREE.Vector3(0, 0, 1), + rearKnee: new THREE.Vector3(0, 0, 1), + leadElbow: new THREE.Vector3(0, -1, 0), + rearElbow: new THREE.Vector3(0, -1, 0), + leadKnuckles: new THREE.Vector3(0, 0, 1), + leadPalm: new THREE.Vector3(0, -1, 0), + rearKnuckles: new THREE.Vector3(0, 0, 1), + rearPalm: new THREE.Vector3(0, -1, 0), + ownJoints: 0, ...pose, }; } @@ -2358,7 +3339,9 @@ function placeDownPose(out: DownPose, base: DownPose, twist: DownPose | null, mi out.hipsRoll = base.hipsRoll * mirror; out.shouldersYaw = base.shouldersYaw * mirror; out.spinePitch = base.spinePitch; + out.spineRoll = 0; out.headPitch = base.headPitch; + out.headRoll = 0; mirrorX(base.leadHand, mirror, out.leadHand); mirrorX(base.rearHand, mirror, out.rearHand); mirrorX(base.leadFoot, mirror, out.leadFoot).y += ankle; @@ -2366,6 +3349,8 @@ function placeDownPose(out: DownPose, base: DownPose, twist: DownPose | null, mi out.leadHeel = base.leadHeel; out.rearHeel = base.rearHeel; out.palmsDown = base.palmsDown; + out.turn = 0; + out.ownJoints = 0; if (twist === null || side === 0) return out; out.hips.addScaledVector(twist.hips, side); out.hipsYaw += twist.hipsYaw * side; @@ -2381,6 +3366,39 @@ function placeDownPose(out: DownPose, base: DownPose, twist: DownPose | null, mi return out; } +const wrapAngle = (angle: number): number => Math.atan2(Math.sin(angle), Math.cos(angle)); +const turnAxis = new THREE.Vector3(); + +/** Turns the direction `out` toward `target` by `t` of the angle between them (both unit length, or near it). */ +function turnToward(out: THREE.Vector3, target: THREE.Vector3, t: number): THREE.Vector3 { + if (t <= 0) return out; + out.normalize(); + const angle = out.angleTo(target); + if (angle < 1e-4) return out; + turnAxis.crossVectors(out, target); + // Opposite directions have no one way round: go over the top, or round the side for a vertical one. + if (turnAxis.lengthSq() < 1e-8) turnAxis.crossVectors(out, Math.abs(out.y) < 0.9 ? worldUpVector : downForward); + return out.applyAxisAngle(turnAxis.normalize(), angle * Math.min(1, t)); +} + +/** + * Of the two sets of yaw-pitch-roll angles (YXZ) that give a turn, (pitch, yaw, roll) and (half a turn - pitch, + * yaw + half a turn, roll + half a turn), the one nearest the angles given, each wrapped to within half a turn of + * them, so blending toward them takes the short way round. Keeping the pitch within a quarter turn instead made a + * body lying back past it read with its yaw and roll half a turn out, and the spine built on those was wrung round. + */ +function nearestTurn(turn: THREE.Euler, pitch: number, yaw: number, roll: number): THREE.Euler { + const ax = pitch + wrapAngle(turn.x - pitch); + const ay = yaw + wrapAngle(turn.y - yaw); + const az = roll + wrapAngle(turn.z - roll); + const bx = pitch + wrapAngle(Math.PI - turn.x - pitch); + const by = yaw + wrapAngle(turn.y + Math.PI - yaw); + const bz = roll + wrapAngle(turn.z + Math.PI - roll); + const first = Math.abs(ax - pitch) + Math.abs(ay - yaw) + Math.abs(az - roll); + const second = Math.abs(bx - pitch) + Math.abs(by - yaw) + Math.abs(bz - roll); + return first <= second ? turn.set(ax, ay, az, "YXZ") : turn.set(bx, by, bz, "YXZ"); +} + /** Moves a whole pose by `shift` (character space). */ function moveDownPose(pose: DownPose, shift: THREE.Vector3): DownPose { pose.hips.add(shift); @@ -2430,7 +3448,9 @@ function lerpDownPose(out: DownPose, a: DownPose, b: DownPose, t: number): DownP out.hipsRoll = lerp(a.hipsRoll, b.hipsRoll, t); out.shouldersYaw = lerp(a.shouldersYaw, b.shouldersYaw, t); out.spinePitch = lerp(a.spinePitch, b.spinePitch, t); + out.spineRoll = lerp(a.spineRoll, b.spineRoll, t); out.headPitch = lerp(a.headPitch, b.headPitch, t); + out.headRoll = lerp(a.headRoll, b.headRoll, t); out.leadHand.copy(a.leadHand).lerp(b.leadHand, t); out.rearHand.copy(a.rearHand).lerp(b.rearHand, t); out.leadFoot.copy(a.leadFoot).lerp(b.leadFoot, t); @@ -2438,6 +3458,16 @@ function lerpDownPose(out: DownPose, a: DownPose, b: DownPose, t: number): DownP out.leadHeel = lerp(a.leadHeel, b.leadHeel, t); out.rearHeel = lerp(a.rearHeel, b.rearHeel, t); out.palmsDown = lerp(a.palmsDown, b.palmsDown, t); + out.turn = lerp(a.turn, b.turn, t); + turnToward(out.leadKnee.copy(a.leadKnee), b.leadKnee, t); + turnToward(out.rearKnee.copy(a.rearKnee), b.rearKnee, t); + turnToward(out.leadElbow.copy(a.leadElbow), b.leadElbow, t); + turnToward(out.rearElbow.copy(a.rearElbow), b.rearElbow, t); + turnToward(out.leadKnuckles.copy(a.leadKnuckles), b.leadKnuckles, t); + turnToward(out.leadPalm.copy(a.leadPalm), b.leadPalm, t); + turnToward(out.rearKnuckles.copy(a.rearKnuckles), b.rearKnuckles, t); + turnToward(out.rearPalm.copy(a.rearPalm), b.rearPalm, t); + out.ownJoints = lerp(a.ownJoints, b.ownJoints, t); return out; } @@ -2465,15 +3495,15 @@ function buildEnswell(): { group: THREE.Group; dispose: () => void } { const group = new THREE.Group(); group.name = "enswell"; group.visible = false; - const plateGeometry = new THREE.CylinderGeometry(0.03, 0.03, 0.008, 16); - const handleGeometry = new THREE.CylinderGeometry(0.008, 0.008, 0.06, 8); + const plateGeometry = new THREE.CylinderGeometry(3.2, 3.2, 0.9, 20); + const handleGeometry = new THREE.CylinderGeometry(0.8, 0.8, 6, 10); const metal = new THREE.MeshStandardMaterial({ color: 0xc8ccd2, roughness: 0.3, metalness: 0.9 }); const plate = new THREE.Mesh(plateGeometry, metal); plate.rotation.x = Math.PI / 2; - plate.position.set(0, 0.07, 0.075); + plate.position.set(0, 9.5, 3.4); group.add(plate); const handle = new THREE.Mesh(handleGeometry, metal); - handle.position.set(0, 0.035, 0.075); + handle.position.set(0, 5.5, 2.4); group.add(handle); return { group, @@ -2485,6 +3515,35 @@ function buildEnswell(): { group: THREE.Group; dispose: () => void } { }; } +/** A squeeze bottle held at its base, the spout along the fingers. Centimetres, in the glove bone's frame. */ +function buildBottle(): { group: THREE.Group; dispose: () => void } { + const group = new THREE.Group(); + group.name = "bottle"; + group.visible = false; + const bodyGeometry = new THREE.CylinderGeometry(3.3, 3.3, 15, 18); + const shoulderGeometry = new THREE.CylinderGeometry(1.3, 3.3, 3, 18); + const spoutGeometry = new THREE.CylinderGeometry(0.7, 1.1, 3.2, 10); + const plastic = new THREE.MeshStandardMaterial({ color: 0xd9e6ee, roughness: 0.32, metalness: 0 }); + const cap = new THREE.MeshStandardMaterial({ color: 0x1d4ed8, roughness: 0.5, metalness: 0 }); + const body = new THREE.Mesh(bodyGeometry, plastic); + body.position.set(0, 9, 2.6); + const shoulder = new THREE.Mesh(shoulderGeometry, plastic); + shoulder.position.set(0, 18, 2.6); + const spout = new THREE.Mesh(spoutGeometry, cap); + spout.position.set(0, 21, 2.6); + group.add(body, shoulder, spout); + return { + group, + dispose: () => { + bodyGeometry.dispose(); + shoulderGeometry.dispose(); + spoutGeometry.dispose(); + plastic.dispose(); + cap.dispose(); + }, + }; +} + function buildStool(): { group: THREE.Group; dispose: () => void } { const group = new THREE.Group(); group.name = "stool"; @@ -2521,6 +3580,8 @@ const worldUpVector = new THREE.Vector3(0, 1, 0); const MAX_OWN_LEAD_TICKS = 6; const OWN_PUNCH_GRACE_TICKS = 5; const OWN_PUNCH_PULL_RATE = 1.5; +/** An opponent's punch seen early is held back unless its wind-up can play at this share of its speed or faster. */ +const MIN_ANTICIPATION_RATE = 0.5; /** Presses closer together than this usually reach the server before the same step, where the newer one replaces the older. */ const SAME_STEP_TICKS = 0.5; /** Contact events in which a punch met the opponent: a hit or a block, a parry included. */ diff --git a/web/hands/src/render/hud-corner.test.ts b/web/hands/src/render/hud-corner.test.ts new file mode 100644 index 00000000..a2d0a458 --- /dev/null +++ b/web/hands/src/render/hud-corner.test.ts @@ -0,0 +1,44 @@ +import { EYE_SHUT_TRAUMA } from "../manifest"; +import { cornerWorkLine, drawHud, shutEyeTag } from "./hud"; +import { fighter, mockHudContext, publicPlayers, snapshot } from "../test/fixtures"; + +const players = Object.fromEntries(publicPlayers.map((player) => [player.id, player])); + +describe("a swollen shut eye", () => { + it("is tagged on the plate at the engine's threshold and not before", () => { + const eyes = (left: number, right: number) => ({ ...fighter("one").trauma, left_eye: left, right_eye: right }); + expect(shutEyeTag(eyes(EYE_SHUT_TRAUMA - 1, 0))).toBeNull(); + expect(shutEyeTag(eyes(EYE_SHUT_TRAUMA, 0))).toBe("LEFT EYE SHUT"); + expect(shutEyeTag(eyes(0, EYE_SHUT_TRAUMA))).toBe("RIGHT EYE SHUT"); + expect(shutEyeTag(eyes(1000, 1000))).toBe("BOTH EYES SHUT"); + }); + + it("is drawn over the fighter's plate", () => { + const texts: string[] = []; + const base = snapshot(); + const shut = { ...base, fighters: [base.fighters[0], { ...base.fighters[1], trauma: { ...base.fighters[1].trauma, right_eye: EYE_SHUT_TRAUMA } }] as const }; + drawHud(mockHudContext(texts), 1280, 720, shut, players, "one", null, 0); + expect(texts.filter((text) => text === "RIGHT EYE SHUT")).toHaveLength(1); + texts.length = 0; + drawHud(mockHudContext(texts), 1280, 720, base, players, "one", null, 0); + expect(texts.some((text) => text.endsWith("SHUT"))).toBe(false); + }); +}); + +describe("the corners between rounds", () => { + it("says what each corner is working on once told", () => { + const base = snapshot(); + expect(cornerWorkLine(base.fighters)).toBeNull(); + const told = [{ ...base.fighters[0], corner_choice: "cut" as const }, { ...base.fighters[1], corner_choice: "breath" as const }]; + expect(cornerWorkLine(told)).toBe("Blue: closing the cut · Red: catching breath"); + expect(cornerWorkLine([base.fighters[0], { ...base.fighters[1], corner_choice: "swelling" as const }])).toBe("Red: icing the swelling"); + }); + + it("puts it on the rest panel", () => { + const texts: string[] = []; + const base = snapshot(); + const resting = { ...base, phase: "rest" as const, phase_ticks_remaining: 300, fighters: [{ ...base.fighters[0], corner_choice: "swelling" as const }, base.fighters[1]] as const }; + drawHud(mockHudContext(texts), 1280, 720, resting, players, "two", null, 0); + expect(texts).toContain("Blue: icing the swelling"); + }); +}); diff --git a/web/hands/src/render/hud.ts b/web/hands/src/render/hud.ts index 6323d616..287b32a3 100644 --- a/web/hands/src/render/hud.ts +++ b/web/hands/src/render/hud.ts @@ -1,4 +1,11 @@ -import type { CombatEvent, EngineSnapshot, FinalMessage, PublicPlayer } from "../types"; +import { EYE_SHUT_TRAUMA, GUARD_BLOCK_MINIMUM } from "../manifest"; +import { isDebut, recordLine } from "../record"; +import { styleTag } from "../styles"; +import type { CombatEvent, CornerChoice, EngineSnapshot, FighterSnapshot, FinalMessage, FinishMethod, PublicPlayer, PunchTotals, RatingDelta, TraumaSnapshot } from "../types"; +import { monogram } from "./avatars"; + +/** Hands out a player's picture once it has loaded. */ +export type PictureSource = (player: PublicPlayer) => CanvasImageSource | null; export const HUD_MAX_GUARD = 700; export const HUD_MAX_POISE = 600; @@ -24,44 +31,168 @@ interface BarSpec { readonly maximum: number; readonly from: string; readonly to: string; + /** Below this the bar's resource is spent: it is marked on the bar, which shows spent while under it. */ + readonly spentBelow?: number; } +/** Spent: a red frame, mark and label, a dimmed fill and red hatching across the bar. */ +const SPENT_RED = "#ff7066"; + function broadcastBar(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, spec: BarSpec, mirror: boolean): void { const height = 9; + const spent = spec.spentBelow !== undefined && spec.value < spec.spentBelow; ctx.fillStyle = "rgba(2,4,9,0.85)"; ctx.fillRect(x - 1, y - 1, width + 2, height + 2); const frame = ctx.createLinearGradient(0, y, 0, y + height); frame.addColorStop(0, "rgba(210,220,235,0.5)"); frame.addColorStop(0.5, "rgba(90,100,120,0.25)"); frame.addColorStop(1, "rgba(30,36,50,0.4)"); - ctx.strokeStyle = frame; + ctx.strokeStyle = spent ? SPENT_RED : frame; ctx.lineWidth = 1; ctx.strokeRect(x - 1.5, y - 1.5, width + 3, height + 3); const ratio = Math.max(0, Math.min(1, spec.value / Math.max(1, spec.maximum))); const fill = ctx.createLinearGradient(0, y, 0, y + height); - fill.addColorStop(0, spec.from); - fill.addColorStop(1, spec.to); + fill.addColorStop(0, spent ? "#7b8494" : spec.from); + fill.addColorStop(1, spent ? "#434a57" : spec.to); ctx.fillStyle = fill; const fillWidth = width * ratio; ctx.fillRect(mirror ? x + width - fillWidth : x, y, fillWidth, height); ctx.fillStyle = "rgba(255,255,255,0.22)"; ctx.fillRect(mirror ? x + width - fillWidth : x, y, fillWidth, 2); - ctx.fillStyle = "#dfe7f5"; + if (spent) { + // Hatched across the whole bar, so even a 40 px phone bar reads as spent at a glance. + ctx.beginPath(); + for (let start = -height; start < width; start += 5) { + const from = Math.max(0, start); + const to = Math.min(width, start + height); + ctx.moveTo(x + from, y + height - (from - start)); + ctx.lineTo(x + to, y + height - (to - start)); + } + ctx.strokeStyle = SPENT_RED; + ctx.lineWidth = 1.2; + ctx.stroke(); + } + if (spec.spentBelow !== undefined) { + // The line it has to stay over, standing proud of the bar. + const at = width * Math.min(1, spec.spentBelow / Math.max(1, spec.maximum)); + ctx.fillStyle = spent ? SPENT_RED : "#f6d57a"; + ctx.fillRect((mirror ? x + width - at : x + at) - 0.75, y - 2, 1.5, height + 4); + } + ctx.fillStyle = spent ? SPENT_RED : "#dfe7f5"; ctx.font = "700 9px Inter, system-ui, sans-serif"; ctx.textAlign = mirror ? "right" : "left"; ctx.fillText(spec.label, mirror ? x + width : x, y - 4); } +/** A player's picture in a ring of their corner's colour, or the first letter of their name until there is one. */ +function portrait(ctx: CanvasRenderingContext2D, x: number, y: number, radius: number, picture: CanvasImageSource | null, name: string, accent: string): void { + const circle = (): void => { + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.closePath(); + }; + ctx.save(); + circle(); + ctx.fillStyle = "#0b1220"; + ctx.fill(); + if (picture !== null) { + ctx.save(); + ctx.clip(); + ctx.drawImage(picture, x - radius, y - radius, radius * 2, radius * 2); + ctx.restore(); + } else { + ctx.globalAlpha = 0.4; + ctx.fillStyle = accent; + ctx.fill(); + ctx.globalAlpha = 1; + ctx.fillStyle = "#f5f8ff"; + ctx.textAlign = "center"; + ctx.font = `800 ${Math.round(radius * 1.1)}px Inter, system-ui, sans-serif`; + ctx.fillText(monogram(name), x, y + radius * 0.39); + } + circle(); + ctx.lineWidth = Math.max(1.5, radius / 9); + ctx.strokeStyle = accent; + ctx.stroke(); + ctx.restore(); +} + +/** The plate's warning when an eye is swollen shut and that side is blind, or null while both eyes see. */ +export function shutEyeTag(trauma: TraumaSnapshot): string | null { + const left = trauma.left_eye >= EYE_SHUT_TRAUMA; + const right = trauma.right_eye >= EYE_SHUT_TRAUMA; + return left && right ? "BOTH EYES SHUT" : left ? "LEFT EYE SHUT" : right ? "RIGHT EYE SHUT" : null; +} + +const CORNER_WORK: Readonly> = { + cut: "closing the cut", + swelling: "icing the swelling", + breath: "catching breath", + balanced: "a little of everything", +}; + +/** What each corner is working on, for the rest panel; null until a corner has been told. Seat 0 is the blue corner. */ +export function cornerWorkLine(fighters: readonly FighterSnapshot[]): string | null { + const told = fighters.flatMap((fighter, seat) => (fighter.corner_choice === null ? [] : [`${seat === 0 ? "Blue" : "Red"}: ${CORNER_WORK[fighter.corner_choice]}`])); + return told.length === 0 ? null : told.join(" · "); +} + +function warningTag(ctx: CanvasRenderingContext2D, x: number, y: number, text: string, mirror: boolean): void { + ctx.save(); + ctx.font = "800 9px Inter, system-ui, sans-serif"; + const width = ctx.measureText(text).width + 12; + const left = mirror ? x - width : x; + ctx.fillStyle = "rgba(150,22,18,0.92)"; + ctx.fillRect(left, y, width, 14); + ctx.fillStyle = "#ffe8e3"; + ctx.textAlign = "left"; + ctx.fillText(text, left + 6, y + 10); + ctx.restore(); +} + +/** The HUD is laid out for a 1280 x 720 screen and drawn larger on bigger ones: a 1920 x 1080 window gets 1.5x. */ +export function hudScale(width: number, height: number): number { + return Math.max(1, Math.min(1.6, width / 1280, height / 720)); +} + +/** + * The line under a fighter's name, fullest first: the style, the record, the rating (or CPU), then knockdowns, + * warnings and points taken only when there are any. A plate too narrow for the whole line takes the next one + * that fits: without the record, then without the rating, then with the counts in short ("W2", "−1"), then + * without the style, so the counts, which can decide a close round, are the last thing a plate gives up. A + * phone's plate is only the style and the counts in short: the introductions give the record. + */ +export function plateDetails(player: PublicPlayer | undefined, fighter: Pick, compact: boolean): readonly string[] { + const style = styleTag(fighter.style); + const record = player?.record === undefined ? null : isDebut(player.record) ? "DEBUT" : recordLine(player.record); + const rating = player?.cpu === true ? "CPU" : `ELO ${player?.rating ?? "—"}`; + const knockdowns = fighter.knockdowns > 0 ? `${fighter.knockdowns} KD` : null; + const counts = [knockdowns, fighter.warnings > 0 ? `${fighter.warnings} WARNING${fighter.warnings === 1 ? "" : "S"}` : null, fighter.deductions > 0 ? `−${fighter.deductions} PT${fighter.deductions === 1 ? "" : "S"}` : null]; + const short = [knockdowns, fighter.warnings > 0 ? `W${fighter.warnings}` : null, fighter.deductions > 0 ? `−${fighter.deductions}` : null]; + const line = (...parts: (string | null)[]): string => parts.filter((part) => part !== null).join(" · "); + const lines = compact + ? [line(style, ...short), line(...short)] + : [line(style, record, rating, ...counts), line(style, rating, ...counts), line(style, ...counts), line(style, ...short), line(...short)]; + return lines.filter((text, index) => text !== "" && lines.indexOf(text) === index); +} + +export const PLATE_PORTRAIT_RADIUS = 16; +export const CLOCK_PORTRAIT_RADIUS = 20; +const ROUND_CARD_WIDTH = 168; + function fighterPlate( ctx: CanvasRenderingContext2D, x: number, y: number, width: number, name: string, - detail: string, + /** The line under the name, fullest first (plateDetails). */ + details: readonly string[], bars: readonly BarSpec[], mirror: boolean, accent: string, + /** Drawn at the plate's outer end when there is the width for it. */ + picture: CanvasImageSource | null | undefined, ): void { const height = 62; ctx.save(); @@ -86,19 +217,30 @@ function fighterPlate( ctx.fillStyle = accent; ctx.fillRect(mirror ? x + width - 5 : x, y + 4, 5, height - 8); - const textX = mirror ? x + width - 20 : x + 20; + const inset = picture === undefined ? 20 : 19 + PLATE_PORTRAIT_RADIUS * 2; + if (picture !== undefined) portrait(ctx, mirror ? x + width - 11 - PLATE_PORTRAIT_RADIUS : x + 11 + PLATE_PORTRAIT_RADIUS, y + 4 + PLATE_PORTRAIT_RADIUS, PLATE_PORTRAIT_RADIUS, picture, name, accent); + const textX = mirror ? x + width - inset : x + inset; ctx.textAlign = mirror ? "right" : "left"; ctx.fillStyle = "#f5f8ff"; const label = name.toUpperCase(); + const nameWidth = width - inset - 24; const nameSize = fitFontSize((size) => { ctx.font = `800 ${size}px Inter, system-ui, sans-serif`; return ctx.measureText(label).width; - }, width - 44, 16, 11); + }, nameWidth, 16, 11); ctx.font = `800 ${nameSize}px Inter, system-ui, sans-serif`; - ctx.fillText(fit(ctx, label, width - 44), textX, y + 21); + ctx.fillText(fit(ctx, label, nameWidth), textX, y + 21); ctx.fillStyle = "#93a3bd"; - ctx.font = "600 10px Inter, system-ui, sans-serif"; - ctx.fillText(detail, textX, y + 35); + const detailWidth = width - inset - 14; + const measureDetail = (text: string, size: number): number => { + ctx.font = `600 ${size}px Inter, system-ui, sans-serif`; + return ctx.measureText(text).width; + }; + // The fullest line that fits at the smallest size, set as large as it fits; whole parts go, never letters. + const detail = details.find((text) => measureDetail(text, 8) <= detailWidth) ?? details.at(-1) ?? ""; + const detailSize = fitFontSize((size) => measureDetail(detail, size), detailWidth, 10, 8); + ctx.font = `600 ${detailSize}px Inter, system-ui, sans-serif`; + ctx.fillText(fit(ctx, detail, detailWidth), textX, y + 35); const barWidth = (width - 52) / bars.length; const groupWidth = bars.length * barWidth + (bars.length - 1) * 12; @@ -110,7 +252,7 @@ function fighterPlate( } function roundCard(ctx: CanvasRenderingContext2D, centerX: number, y: number, clock: string, round: string, phase: string): void { - const width = 168; + const width = ROUND_CARD_WIDTH; const height = 58; ctx.save(); ctx.beginPath(); @@ -147,30 +289,126 @@ function roundCard(ctx: CanvasRenderingContext2D, centerX: number, y: number, cl const PANEL_HEIGHT = 78; const SHORT_PANEL_HEIGHT = 50; -const panelHeightFor = (height: number): number => (height < 480 ? SHORT_PANEL_HEIGHT : PANEL_HEIGHT); +/** The big callouts ("ROUND 2", "PARRIED"): 46 px type on a baseline this far down the screen. */ +export const CALLOUT_BASELINE = 0.22; +export const CALLOUT_BELOW_BASELINE = 14; + +export const panelHeightFor = (height: number): number => (height < 480 ? SHORT_PANEL_HEIGHT : PANEL_HEIGHT); /** Offset that parks a centre panel under the top bar, or under the round card on narrow screens, clear of the fighters. */ export const topPanelOffset = (width: number, height: number): number => Math.max(width < 640 ? 112 : 56, height * 0.1) + panelHeightFor(height) / 2 - height / 2; -function centerPanel(ctx: CanvasRenderingContext2D, width: number, height: number, title: string, subtitle: string, yOffset = 0): void { - const panelWidth = Math.min(320, width - 24); - const panelHeight = panelHeightFor(height); - const short = panelHeight === SHORT_PANEL_HEIGHT; - const x = width / 2 - panelWidth / 2; - const y = height / 2 - panelHeight / 2 + yOffset; +/** + * The touch pads (style.css .touch-pads). Held upright they are one column 118 px up from the bottom, 274 px + * tall: the moves, the modifiers and the punch pads. On its side the modifiers and punch pads stand 196 px tall + * and the moves have their own column to their left, 102 px tall at the foot; on a screen 350 px tall or less + * all of them are smaller, 154 and 86 px tall, and 112 px up. `grid` is the height of the punch pads at the foot, + * the only ones shown while the player is down, when they are the get-up pads. `reach` is how far in from the + * right edge the modifiers' and punch pads' column comes, and `movesReach` how much further the moves' column + * does, with a little to spare. + */ +export const TOUCH_PADS = { + upright: { bottom: 118, height: 274, moves: 0, grid: 118 }, + landscape: { bottom: 118, height: 196, moves: 102, grid: 118 }, + short: { bottom: 112, height: 154, moves: 86, grid: 92 }, + shortHeight: 350, + reach: 12 + 190 + 14, + movesReach: 122 + 12, +} as const; + +/** The touch pads as they stand on a screen this size. */ +const touchPadsFor = (width: number, height: number): (typeof TOUCH_PADS)["upright" | "landscape" | "short"] => + width <= height ? TOUCH_PADS.upright : height <= TOUCH_PADS.shortHeight ? TOUCH_PADS.short : TOUCH_PADS.landscape; + +/** How far in from the right edge the touch pads come beside something that ends at `bottom`: 0 while it stays above them. */ +export function touchPadsReach(width: number, height: number, bottom: number): number { + const pads = touchPadsFor(width, height); + if (bottom <= height - pads.bottom - pads.height) return 0; + // Upright the pads are one column; on its side the moves only come out at the foot of the block. + return TOUCH_PADS.reach + (pads.moves > 0 && bottom > height - pads.bottom - pads.moves ? TOUCH_PADS.movesReach : 0); +} + +export interface PanelRect { + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; +} + +/** A centre panel moved up beside the round card is no narrower than this. */ +const BESIDE_CARD_MIN_WIDTH = 160; + +/** + * Where a centre panel goes for the countdown, a foul's recovery and the rest: under the top bar, or under the + * round card on a narrow screen. With the touch pads up it keeps clear of them: held upright it is the short + * panel where the pads come up to within 8 px of it, and on its side it moves left of them, narrower where it + * must. A narrow screen on its side has room for it up beside the round card, above the controls hint. + */ +export function topPanel(width: number, height: number, touch = false): PanelRect { + let panelHeight = panelHeightFor(height); + let y = height / 2 + topPanelOffset(width, height) - panelHeight / 2; + let panelWidth = Math.min(320, width - 24); + let right = width / 2 + panelWidth / 2; + if (touch && width <= height) { + if (touchPadsReach(width, height, y + panelHeight + 8) > 0) panelHeight = SHORT_PANEL_HEIGHT; + } else if (touch) { + const besideCard = width < 640 ? Math.min(width / 2 - ROUND_CARD_WIDTH / 2 - 8, width - touchPadsReach(width, height, 56 + panelHeight)) : 0; + const up = besideCard - 12 >= BESIDE_CARD_MIN_WIDTH; + if (up) y = 56; + const limit = up ? besideCard : width - touchPadsReach(width, height, y + panelHeight); + if (right > limit) { + panelWidth = Math.min(panelWidth, limit - 12); + right = limit; + } + } + return { x: right - panelWidth, y, width: panelWidth, height: panelHeight }; +} + +/** The downed viewer's get-up panel: the count, the rhythm prompt and its meter. */ +const GET_UP_PANEL = { width: 380, height: 150 } as const; +/** The plates' guard and poise bars, with their labels, come this far up from the bottom. */ +const PLATE_BARS_TOP = 84 + 12 + 16; + +/** + * Where the downed viewer's get-up panel goes: under the knockdown headline and above the plates' guard and poise + * bars. While he is down the touch pads show only the get-up pads (style.css), and the panel keeps clear of them + * and of the thumbs on them: above them on a phone held upright, left of them on its side. Where there is too + * little room under the headline it moves up over it, and on a narrow screen on its side over the round card, + * whose clock stands still through the count. + */ +export function getUpPanel(width: number, height: number, touch = false): PanelRect { + const pads = touchPadsFor(width, height); + const getUpPadsTop = height - pads.bottom - pads.grid; + const upright = width <= height; + const bottom = touch && upright ? Math.min(height - PLATE_BARS_TOP, getUpPadsTop - 8) : height - PLATE_BARS_TOP; + const y = Math.max(56, Math.min(Math.max(height * 0.24, headlineBaseline(height, width < 640) + 34), bottom - GET_UP_PANEL.height)); + let panelWidth = Math.min(GET_UP_PANEL.width, width - 24); + let right = width / 2 + panelWidth / 2; + if (touch && !upright && y + GET_UP_PANEL.height > getUpPadsTop) { + const limit = width - TOUCH_PADS.reach; + if (right > limit) { + panelWidth = Math.min(panelWidth, limit - 12); + right = limit; + } + } + return { x: right - panelWidth, y, width: panelWidth, height: GET_UP_PANEL.height }; +} + +function centerPanel(ctx: CanvasRenderingContext2D, panel: PanelRect, title: string, subtitle: string): void { + const short = panel.height === SHORT_PANEL_HEIGHT; + const centre = panel.x + panel.width / 2; ctx.fillStyle = "rgba(3,6,12,0.88)"; - ctx.fillRect(x, y, panelWidth, panelHeight); + ctx.fillRect(panel.x, panel.y, panel.width, panel.height); ctx.strokeStyle = "rgba(246,213,122,0.45)"; ctx.lineWidth = 1.5; - ctx.strokeRect(x + 2, y + 2, panelWidth - 4, panelHeight - 4); + ctx.strokeRect(panel.x + 2, panel.y + 2, panel.width - 4, panel.height - 4); ctx.textAlign = "center"; ctx.fillStyle = "#ffd77a"; - ctx.font = `800 ${short ? 17 : 22}px Inter, system-ui, sans-serif`; - ctx.fillText(title, width / 2, y + (short ? 22 : 34)); + fitted(ctx, title, centre, panel.y + (short ? 22 : 34), panel.width - 24, 800, short ? 17 : 22, 12); ctx.fillStyle = "#c8d3e6"; ctx.font = `600 ${short ? 11 : 12}px Inter, system-ui, sans-serif`; - ctx.fillText(fit(ctx, subtitle, panelWidth - 24), width / 2, y + (short ? 39 : 58)); + ctx.fillText(fit(ctx, subtitle, panel.width - 24), centre, panel.y + (short ? 39 : 58)); } /** Holds the round clock while a knockdown count or a foul timeout runs its own timer. */ @@ -184,7 +422,8 @@ export class RoundClock { this.fightTicks = null; } if (snapshot.phase === "fight") this.fightTicks = snapshot.phase_ticks_remaining; - const held = snapshot.phase === "knockdown" || snapshot.phase === "foul_recovery"; + // A stoppage freezes the clock where the bout ended, as the broadcast does. + const held = snapshot.phase === "knockdown" || snapshot.phase === "foul_recovery" || snapshot.phase === "complete"; return held && this.fightTicks !== null ? this.fightTicks : snapshot.phase_ticks_remaining; } } @@ -192,6 +431,19 @@ export class RoundClock { export interface RoundPunchStats { thrown: number; landed: number; + /** Of those, the jabs; every other punch counts as a power punch, as CompuBox counts them. */ + jabsThrown: number; + jabsLanded: number; +} + +const blankPunches = (): RoundPunchStats => ({ thrown: 0, landed: 0, jabsThrown: 0, jabsLanded: 0 }); + +/** + * The engine reports a punch that met the guard twice: the defender's block, then the hit that leaked + * through. Only a hit with no block of the same punch beside it landed clean. + */ +export function wasBlocked(hit: CombatEvent, events: readonly CombatEvent[]): boolean { + return hit.action_id !== null && events.some((event) => (event.kind === "block" || event.kind === "perfect_block") && event.tick === hit.tick && event.action_id === hit.action_id && event.actor_id === hit.target_id); } /** Punches thrown and landed per fighter in the current round, reset on the round-start bell. */ @@ -199,34 +451,43 @@ export class RoundStatsTracker { private readonly stats = new Map(); private readonly totals = new Map(); - record(event: CombatEvent): void { + /** `events` is the rest of the snapshot the event came in, to tell a punch that landed from one that was blocked. */ + record(event: CombatEvent, events: readonly CombatEvent[] = []): void { if (event.kind === "bell") { if (event.detail === "round_start") this.stats.clear(); return; } if (event.actor_id === null) return; if (event.kind === "punch_start") { - this.entry(this.stats, event.actor_id).thrown += 1; - this.entry(this.totals, event.actor_id).thrown += 1; - } else if (event.kind === "hit" || event.kind === "counter_hit") { - this.entry(this.stats, event.actor_id).landed += 1; - this.entry(this.totals, event.actor_id).landed += 1; + // The detail is "hand:class:target". + const jab = event.detail.split(":")[1] === "jab" ? 1 : 0; + for (const entry of [this.entry(this.stats, event.actor_id), this.entry(this.totals, event.actor_id)]) { + entry.thrown += 1; + entry.jabsThrown += jab; + } + } else if ((event.kind === "hit" || event.kind === "counter_hit") && !wasBlocked(event, events)) { + // The detail is "class:target". + const jab = event.detail.split(":")[0] === "jab" ? 1 : 0; + for (const entry of [this.entry(this.stats, event.actor_id), this.entry(this.totals, event.actor_id)]) { + entry.landed += 1; + entry.jabsLanded += jab; + } } } get(playerId: string): RoundPunchStats { - return this.stats.get(playerId) ?? { thrown: 0, landed: 0 }; + return this.stats.get(playerId) ?? blankPunches(); } /** Punches over the whole bout, for the result panel. */ total(playerId: string): RoundPunchStats { - return this.totals.get(playerId) ?? { thrown: 0, landed: 0 }; + return this.totals.get(playerId) ?? blankPunches(); } private entry(map: Map, playerId: string): RoundPunchStats { let entry = map.get(playerId); if (entry === undefined) { - entry = { thrown: 0, landed: 0 }; + entry = blankPunches(); map.set(playerId, entry); } return entry; @@ -258,12 +519,28 @@ export function decisionLabel(final: FinalMessage): string { export const STOPPAGE_METHODS: ReadonlySet = new Set(["ko", "flash_ko", "tko"]); export const FINAL_REVEAL_DELAY_SECONDS = 3.6; +/** A disqualification or a doctor's stoppage is the referee's call: a moment for it before the result. */ +export const REFEREE_REVEAL_DELAY_SECONDS = 2.4; /** Seconds to hold the result panel back so a stoppage's slow-motion fall stays visible. */ export function finalRevealDelay(final: FinalMessage | null): number { - return final !== null && STOPPAGE_METHODS.has(final.method) ? FINAL_REVEAL_DELAY_SECONDS : 0; + if (final === null) return 0; + if (STOPPAGE_METHODS.has(final.method)) return FINAL_REVEAL_DELAY_SECONDS; + return final.method === "disqualification" || final.method === "doctor_stoppage" ? REFEREE_REVEAL_DELAY_SECONDS : 0; } +/** Below the top bar, or below the round card where a phone puts it under the top bar. */ +export const headlineBaseline = (height: number, compact: boolean): number => Math.max(height * 0.16, (compact ? 112 : 56) + 40); +/** How far below the headline the knockdown count's baseline sits. */ +export const COUNT_BELOW_HEADLINE = 64; +/** Engine latency that is worth telling a player about on the broadcast screen... */ +export const LAG_WARNING_MS = 120; +/** ...and the warning stays up until the latency is back under this, so a value hovering at the line cannot flicker it. */ +export const LAG_CLEAR_MS = 100; + +/** Whether the slow-connection warning is up, from the latency now and whether it was up a moment ago. */ +export const lagWarning = (latencyMs: number | null, shown: boolean): boolean => latencyMs !== null && latencyMs >= (shown ? LAG_CLEAR_MS : LAG_WARNING_MS); + export function drawHud( ctx: CanvasRenderingContext2D, width: number, @@ -276,46 +553,75 @@ export function drawHud( tickRate = 30, roundStats: RoundStatsTracker | null = null, replayLabel: string | null = null, + /** The latency while the slow-connection warning is up (lagWarning), else null. */ inputLatencyMs: number | null = null, roundCallout: string | null = null, clockTicks: number | null = null, + pictures: PictureSource | null = null, + /** The fighter the referee is counting over, as the renderer saw him go down: he is no longer on the canvas once he has beaten the count. */ + countTarget: string | null = null, + /** The touch pads are up over the right of the screen. */ + touch = false, ): void { ctx.save(); + const scale = hudScale(width, height); + ctx.scale(scale, scale); + width /= scale; + height /= scale; ctx.textBaseline = "alphabetic"; - // Below 640 px the two plates share the bottom edge and the round card moves under the top bar. + const pictureOf = (fighter: FighterSnapshot): CanvasImageSource | null => { + const player = players[fighter.player_id]; + return player === undefined || pictures === null ? null : pictures(player); + }; + // Below 640 px the two plates share the bottom edge and the round card moves under the top bar. Wider, each + // plate ends 8 px short of the round card between them. const compact = width < 640; - const plateWidth = compact ? (width - 56) / 2 : Math.min(300, width * 0.38); + const plateWidth = compact ? (width - 56) / 2 : Math.min(300, width * 0.38, width / 2 - ROUND_CARD_WIDTH / 2 - 8 - 24); const plateY = height - 84; - snapshot.fighters.forEach((fighter, index) => { + // The result card takes the place of the plates and the clock once the bout is over. + if (final === null) snapshot.fighters.forEach((fighter, index) => { const mirror = index === 1; const x = mirror ? width - 24 - plateWidth : 24; const player = players[fighter.player_id]; - const detail = `ELO ${player?.rating ?? "—"} · KD ${fighter.knockdowns} · W ${fighter.warnings} · −${fighter.deductions}`; const bars: BarSpec[] = [ { label: `${compact ? "STA" : "STAMINA"} ${Math.round(fighter.stamina)}`, value: fighter.stamina, maximum: fighter.maximum_stamina, from: "#ffe08a", to: "#d9a53a" }, { label: `${compact ? "HP" : "HEALTH"} ${Math.round(fighter.conditioning)}`, value: fighter.conditioning, maximum: HUD_MAX_CONDITIONING, from: "#ff8a7a", to: "#b02a20" }, - { label: "GUARD", value: fighter.guard, maximum: HUD_MAX_GUARD, from: "#9ec7ff", to: "#3d6fb8" }, + // Under the mark a guard stops nothing: a block that takes it there breaks it (guard_break). + { label: "GUARD", value: fighter.guard, maximum: HUD_MAX_GUARD, from: "#9ec7ff", to: "#3d6fb8", spentBelow: GUARD_BLOCK_MINIMUM }, { label: `POISE ${Math.round(fighter.poise)}`, value: fighter.poise, maximum: HUD_MAX_POISE, from: "#e8c890", to: "#8a6a34" }, ]; - fighterPlate(ctx, x, plateY, plateWidth, player?.name ?? "Fighter", detail, bars.slice(0, 2), mirror, index === 0 ? "#3d6fb8" : "#b02a20"); + const accent = index === 0 ? "#3d6fb8" : "#b02a20"; + // A narrow plate has no room for a picture, so it goes beside the clock at the top, unless the phone is on its + // side with the touch pads up, which stand where the red corner's would go. + fighterPlate(ctx, x, plateY, plateWidth, player?.name ?? "Fighter", plateDetails(player, fighter, compact), bars.slice(0, 2), mirror, accent, compact ? undefined : pictureOf(fighter)); + if (compact && !(touch && width > height)) portrait(ctx, width / 2 + (mirror ? 1 : -1) * (ROUND_CARD_WIDTH / 2 + 8 + CLOCK_PORTRAIT_RADIUS), 54 + 29, CLOCK_PORTRAIT_RADIUS, pictureOf(fighter), player?.name ?? "Fighter", accent); const miniY = plateY - 12; // Guard and poise ride above the plate, as wide as the plate's own bars once the plate is narrow. const miniWidth = Math.min(64, (plateWidth - 52) / 2); broadcastBar(ctx, mirror ? x + plateWidth - 20 - 2 * miniWidth : x + 20, miniY, miniWidth, bars[2]!, mirror); broadcastBar(ctx, mirror ? x + plateWidth - 8 - miniWidth : x + 32 + miniWidth, miniY, miniWidth, bars[3]!, mirror); + const eyeTag = shutEyeTag(fighter.trauma); + if (eyeTag !== null) warningTag(ctx, mirror ? x + plateWidth - 20 : x + 20, miniY - 34, eyeTag, mirror); }); const seconds = Math.floor((clockTicks ?? snapshot.phase_ticks_remaining) / tickRate); const clock = `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`; - roundCard(ctx, width / 2, compact ? 54 : height - 84, clock, `ROUND ${snapshot.round_number}`, snapshot.phase.replace("_", " ").toUpperCase()); - if (inputLatencyMs !== null && final === null && snapshot.fighters.some((fighter) => fighter.player_id === viewerId)) { - const rounded = Math.round(inputLatencyMs); + // A downed viewer's get-up panel takes the room it needs from the knockdown headline, and on a narrow screen on + // its side from the round card too, whose clock stands still through the count. + const getUp = snapshot.phase === "knockdown" && replayLabel === null && snapshot.fighters.some((fighter) => fighter.player_id === viewerId && fighter.is_downed) ? getUpPanel(width, height, touch) : null; + const cardCovered = getUp !== null && compact && getUp.y < 54 + 58; + if (final === null && !cardCovered) roundCard(ctx, width / 2, compact ? 54 : height - 84, clock, `ROUND ${snapshot.round_number}`, snapshot.phase === "complete" ? "FINAL" : snapshot.phase.replace("_", " ").toUpperCase()); + // Only a connection slow enough to feel is worth the broadcast screen; Settings, Diagnostics always shows it. + // A phone has no free corner for it beside the clock, the centre panels and the captions. With the touch pads up + // it goes top left, where they never reach and where the knockout replay's tag has the corner to itself. + const lag = inputLatencyMs === null ? 0 : Math.round(inputLatencyMs); + if (lag >= LAG_CLEAR_MS && !compact && final === null && !(touch && replayLabel !== null) && snapshot.fighters.some((fighter) => fighter.player_id === viewerId)) { ctx.save(); - ctx.textAlign = "right"; + ctx.textAlign = touch ? "left" : "right"; ctx.font = "700 11px ui-monospace, monospace"; - ctx.fillStyle = rounded < 90 ? "rgba(170,200,180,0.75)" : rounded < 160 ? "rgba(240,200,110,0.9)" : "rgba(255,110,100,0.95)"; - ctx.fillText(`INPUT ${rounded} ms`, width - 24, compact ? 128 : 36); + ctx.fillStyle = lag < 200 ? "rgba(240,200,110,0.9)" : "rgba(255,110,100,0.95)"; + ctx.fillText(`SLOW CONNECTION ${lag} ms`, touch ? 24 : width - 24, 76); ctx.restore(); } if (replayLabel !== null) { @@ -340,50 +646,59 @@ export function drawHud( ctx.font = "900 46px Inter, system-ui, sans-serif"; ctx.lineWidth = 6; ctx.strokeStyle = "rgba(0,0,0,0.75)"; - ctx.strokeText(roundCallout, width / 2, height * 0.22); + ctx.strokeText(roundCallout, width / 2, height * CALLOUT_BASELINE); ctx.fillStyle = "#f6d57a"; - ctx.fillText(roundCallout, width / 2, height * 0.22); + ctx.fillText(roundCallout, width / 2, height * CALLOUT_BASELINE); ctx.restore(); } if (snapshot.phase === "countdown") { - centerPanel(ctx, width, height, `ROUND ${snapshot.round_number}`, "Touch gloves. Protect yourself at all times.", topPanelOffset(width, height)); + centerPanel(ctx, topPanel(width, height, touch), `ROUND ${snapshot.round_number}`, "Touch gloves. Protect yourself at all times."); } - if (snapshot.phase === "knockdown") { - const viewer = snapshot.fighters.find((fighter) => fighter.player_id === viewerId); - const downed = snapshot.fighters.find((fighter) => fighter.is_downed) ?? viewer; - const count = Math.max(...snapshot.fighters.map((fighter) => fighter.get_up_count)); - const downedName = players[downed?.player_id ?? ""]?.name ?? "Fighter"; + // The replay's own tag says what it shows; the live count panel would only cover it. + const headline = headlineBaseline(height, compact); + if (snapshot.phase === "knockdown" && replayLabel === null) { + // Once he has beaten the count he stands for the rest of the eight, and the snapshot no longer says who went + // down: it is the fighter the renderer saw fall, or nobody by name for a spectator who arrived after it. + const downed = snapshot.fighters.find((fighter) => fighter.is_downed); + const counted = downed ?? snapshot.fighters.find((fighter) => fighter.player_id === countTarget); + const name = counted === undefined ? null : (players[counted.player_id]?.name ?? "Fighter").toUpperCase(); ctx.save(); ctx.textAlign = "center"; ctx.font = "900 44px Inter, system-ui, sans-serif"; ctx.lineWidth = 6; ctx.strokeStyle = "rgba(0,0,0,0.75)"; - ctx.strokeText("KNOCKDOWN", width / 2, height * 0.16); - ctx.fillStyle = "#ff4d4d"; - ctx.fillText("KNOCKDOWN", width / 2, height * 0.16); + // A downed viewer's own get-up panel says it all where it needs the headline's room. + if (getUp === null || getUp.y >= headline + 6) { + ctx.strokeText("KNOCKDOWN", width / 2, headline); + ctx.fillStyle = "#ff4d4d"; + ctx.fillText("KNOCKDOWN", width / 2, headline); + } ctx.font = "700 15px Inter, system-ui, sans-serif"; ctx.fillStyle = "#e6ecf7"; - ctx.fillText(`${fit(ctx, downedName.toUpperCase(), width * 0.5)} IS DOWN`, width / 2, height * 0.16 + 24); + const verb = downed === undefined ? " BEAT THE COUNT" : " IS DOWN"; + const line = name === null ? "STANDING EIGHT" : `${fit(ctx, name, Math.min(width * 0.5, width - 32 - ctx.measureText(verb).width))}${verb}`; + if (getUp === null || getUp.y >= headline + 30) ctx.fillText(line, width / 2, headline + 24); ctx.restore(); } if (snapshot.phase === "knockdown" && replayLabel === null) { const viewer = snapshot.fighters.find((fighter) => fighter.player_id === viewerId); + // The referee's first word is "one", a second after the fighter lands. const count = Math.max(...snapshot.fighters.map((fighter) => fighter.get_up_count)); if (viewer?.is_downed !== true) { - ctx.save(); - ctx.textAlign = "center"; - ctx.font = "900 36px Inter, system-ui, sans-serif"; - ctx.lineWidth = 5; - ctx.strokeStyle = "rgba(0,0,0,0.75)"; - ctx.strokeText(`COUNT ${count}`, width / 2, height * 0.16 + 64); - ctx.fillStyle = "#ffd77a"; - ctx.fillText(`COUNT ${count}`, width / 2, height * 0.16 + 64); - ctx.restore(); + if (count >= 1) { + ctx.save(); + ctx.textAlign = "center"; + ctx.font = "900 36px Inter, system-ui, sans-serif"; + ctx.lineWidth = 5; + ctx.strokeStyle = "rgba(0,0,0,0.75)"; + ctx.strokeText(`COUNT ${count}`, width / 2, headline + COUNT_BELOW_HEADLINE); + ctx.fillStyle = "#ffd77a"; + ctx.fillText(`COUNT ${count}`, width / 2, headline + COUNT_BELOW_HEADLINE); + ctx.restore(); + } } else { - const panelWidth = Math.min(380, width - 24); - const panelHeight = 150; - const x = width / 2 - panelWidth / 2; - const y = height * 0.24; + const { x, y, width: panelWidth, height: panelHeight } = getUp ?? getUpPanel(width, height, touch); + const centre = x + panelWidth / 2; ctx.fillStyle = "rgba(3,6,12,0.88)"; ctx.fillRect(x, y, panelWidth, panelHeight); ctx.strokeStyle = "rgba(246,213,122,0.5)"; @@ -392,7 +707,7 @@ export function drawHud( ctx.textAlign = "center"; ctx.fillStyle = "#ffd77a"; ctx.font = "800 30px Inter, system-ui, sans-serif"; - ctx.fillText(`COUNT ${count}`, width / 2, y + 40); + ctx.fillText(count >= 1 ? `COUNT ${count}` : "GET UP!", centre, y + 40); const prompt = viewer?.is_downed === true ? viewer.get_up_prompt : null; if (prompt !== null && prompt !== undefined && viewer !== undefined) { const inWindow = snapshot.tick >= viewer.get_up_window_start_tick && snapshot.tick <= viewer.get_up_window_end_tick; @@ -400,14 +715,14 @@ export function drawHud( if (inWindow) { ctx.fillStyle = "#ffe9a8"; ctx.font = "900 64px Inter, system-ui, sans-serif"; - ctx.fillText(arrow, width / 2 - 90, y + 106); + ctx.fillText(arrow, centre - 90, y + 106); ctx.fillStyle = "#7dffa8"; ctx.font = "900 34px Inter, system-ui, sans-serif"; - ctx.fillText("NOW!", width / 2 + 62, y + 100); + ctx.fillText("NOW!", centre + 62, y + 100); } else { ctx.fillStyle = "#8fa3c8"; ctx.font = "800 30px Inter, system-ui, sans-serif"; - ctx.fillText(`GET READY ${arrow}`, width / 2, y + 98); + ctx.fillText(`GET READY ${arrow}`, centre, y + 98); } const meterWidth = panelWidth - 60; const ratio = Math.max(0, Math.min(1, viewer.get_up_meter / Math.max(1, viewer.get_up_required))); @@ -424,26 +739,36 @@ export function drawHud( } else { ctx.fillStyle = "#c8d3e6"; ctx.font = "600 14px Inter, system-ui, sans-serif"; - ctx.fillText("Waiting for your rhythm instruction…", width / 2, y + 100); + ctx.fillText(fit(ctx, "Waiting for your rhythm instruction…", panelWidth - 24), centre, y + 100); } } } if (snapshot.phase === "foul_recovery") { const victim = snapshot.fighters.find((fighter) => fighter.is_foul_recovery_target); - centerPanel(ctx, width, height, "FOUL RECOVERY", `${players[victim?.player_id ?? ""]?.name ?? "Fighter"} is recovering`, topPanelOffset(width, height)); + centerPanel(ctx, topPanel(width, height, touch), "FOUL RECOVERY", `${players[victim?.player_id ?? ""]?.name ?? "Fighter"} is recovering`); } if (snapshot.phase === "rest") { + // By corner rather than by name, so two long names cannot push the second fighter's numbers off the panel. const statsLine = snapshot.fighters - .map((fighter) => ({ name: players[fighter.player_id]?.name ?? "Fighter", stats: roundStats?.get(fighter.player_id) ?? { thrown: 0, landed: 0 } })) + .map((fighter, seat) => ({ corner: seat === 0 ? "Blue" : "Red", stats: roundStats?.get(fighter.player_id) ?? blankPunches() })) .filter(({ stats }) => stats.thrown > 0 || stats.landed > 0) - .map(({ name, stats }) => `${fit(ctx, name, width * 0.22)} ${stats.landed}/${stats.thrown}`) + .map(({ corner, stats }) => `${corner} ${stats.landed} of ${stats.thrown}`) .join(" · "); - centerPanel(ctx, width, height, "CORNERS · RECOVER", statsLine.length > 0 ? `Landed this round: ${statsLine}` : "Conditioning governs recovery", topPanelOffset(width, height)); + const corners = cornerWorkLine(snapshot.fighters); + centerPanel(ctx, topPanel(width, height, touch), "CORNERS · RECOVER", corners ?? (statsLine.length > 0 ? `Landed this round: ${statsLine}` : "Conditioning governs recovery")); } if (reconnectMs > 0) { - centerPanel(ctx, width, height, `OPPONENT RECONNECTING · ${Math.ceil(reconnectMs / 1000)}s`, "The bout is paused"); + // Mid-screen, or with the touch pads up in the centre panels' place, which they never reach. + const pauseWidth = Math.min(320, width - 24); + const pauseHeight = panelHeightFor(height); + const pause = touch ? topPanel(width, height, true) : { x: width / 2 - pauseWidth / 2, y: height / 2 - pauseHeight / 2, width: pauseWidth, height: pauseHeight }; + centerPanel(ctx, pause, `OPPONENT RECONNECTING · ${Math.ceil(reconnectMs / 1000)}s`, "The bout is paused"); + } + if (final !== null) { + const punches = snapshot.fighters.map((fighter) => roundStats?.total(fighter.player_id) ?? blankPunches()) as [RoundPunchStats, RoundPunchStats]; + // Every viewer gets a button under the card: Rematch for a fighter, Next bout for a spectator. + drawResultCard(ctx, width, height, resultCard(final, snapshot.fighters, players, punches), true, [pictureOf(snapshot.fighters[0]), pictureOf(snapshot.fighters[1])]); } - if (final !== null) drawFinal(ctx, width, height, final, players, snapshot.fighters.map((fighter) => ({ name: players[fighter.player_id]?.name ?? "Fighter", stats: roundStats?.total(fighter.player_id) ?? { thrown: 0, landed: 0 } }))); ctx.restore(); } @@ -456,35 +781,200 @@ function fitted(ctx: CanvasRenderingContext2D, text: string, x: number, y: numbe ctx.fillText(fit(ctx, text, maxWidth), x, y); } -function drawFinal(ctx: CanvasRenderingContext2D, width: number, height: number, final: FinalMessage, players: Readonly>, punches: readonly { name: string; stats: RoundPunchStats }[] = []): void { - ctx.fillStyle = "rgba(2,4,9,0.94)"; - ctx.fillRect(width * 0.14, height * 0.13, width * 0.72, height * 0.74); - ctx.strokeStyle = "rgba(246,213,122,0.5)"; +const METHOD_HEADLINES: Readonly>> = { + ko: "KNOCKOUT", + flash_ko: "FLASH KNOCKOUT", + tko: "TECHNICAL KNOCKOUT", + doctor_stoppage: "DOCTOR STOPPAGE", + disqualification: "DISQUALIFICATION", + forfeit: "FORFEIT", +}; + +export interface ResultRow { + readonly label: string; + readonly values: readonly [string, string]; + /** The seat with the better value, or null when they are level. */ + readonly lead: 0 | 1 | null; + /** Whether each value is good or bad news for its fighter, where that applies. */ + readonly news?: readonly [boolean, boolean]; +} + +export interface ResultCard { + readonly headline: string; + readonly detail: string; + readonly verdict: string; + readonly winnerSeat: 0 | 1 | null; + readonly names: readonly [string, string]; + /** One per judge, for a bout that went to the cards. */ + readonly judges: readonly ResultRow[]; + readonly rows: readonly ResultRow[]; +} + +const lead = (one: number, two: number): 0 | 1 | null => (one > two ? 0 : two > one ? 1 : null); + +/** What the result card says. Seat 0 is the blue corner, the first fighter in every snapshot and on every scorecard. */ +export function resultCard( + final: FinalMessage, + fighters: readonly [FighterSnapshot, FighterSnapshot], + players: Readonly>, + punches: readonly [RoundPunchStats, RoundPunchStats], +): ResultCard { + const names = fighters.map((fighter) => players[fighter.player_id]?.name ?? "Fighter") as [string, string]; + const winnerSeat = final.winner_id === null ? null : fighters[0].player_id === final.winner_id ? 0 : fighters[1].player_id === final.winner_id ? 1 : null; + const rows: ResultRow[] = []; + const judges: ResultRow[] = []; + if (final.method === "decision" || final.method === "draw") { + for (const card of final.scorecards) { + const one = scoreTotal(card.player_one); + const two = scoreTotal(card.player_two); + judges.push({ label: card.judge.toUpperCase(), values: [String(one), String(two)], lead: lead(one, two) }); + } + } + const scored: [number, number] = [fighters[1].knockdowns, fighters[0].knockdowns]; + if (scored[0] > 0 || scored[1] > 0) rows.push({ label: "KNOCKDOWNS", values: [String(scored[0]), String(scored[1])], lead: lead(scored[0], scored[1]) }); + // The engine's count for the whole bout when the result carries it; what this client saw can miss + // a reconnect or a late arrival. + const [blueCount, redCount] = fighters.map((fighter) => final.punches?.[fighter.player_id]); + const counted = (total: PunchTotals): RoundPunchStats => ({ thrown: total.thrown, landed: total.landed, jabsThrown: total.jabs_thrown, jabsLanded: total.jabs_landed }); + const totals: readonly [RoundPunchStats, RoundPunchStats] = blueCount !== undefined && redCount !== undefined ? [counted(blueCount), counted(redCount)] : punches; + if (totals[0].thrown > 0 || totals[1].thrown > 0) { + // CompuBox: landed of thrown and the share that landed, for all punches, the jabs and the power punches. + // A client that joined mid-punch saw the hit but not the throw; never show more landed than thrown. + const count = (landed: number, thrown: number): string => (thrown > 0 ? `${Math.min(landed, thrown)}/${thrown} (${Math.round((Math.min(landed, thrown) / thrown) * 100)}%)` : "0/0"); + const power = totals.map((stats) => ({ landed: stats.landed - stats.jabsLanded, thrown: stats.thrown - stats.jabsThrown })); + rows.push({ label: "TOTAL PUNCHES", values: [count(totals[0].landed, totals[0].thrown), count(totals[1].landed, totals[1].thrown)], lead: lead(totals[0].landed, totals[1].landed) }); + rows.push({ label: "JABS", values: [count(totals[0].jabsLanded, totals[0].jabsThrown), count(totals[1].jabsLanded, totals[1].jabsThrown)], lead: lead(totals[0].jabsLanded, totals[1].jabsLanded) }); + rows.push({ label: "POWER PUNCHES", values: [count(power[0]!.landed, power[0]!.thrown), count(power[1]!.landed, power[1]!.thrown)], lead: lead(power[0]!.landed, power[1]!.landed) }); + } + const ratings = fighters.map((fighter) => final.ratings[fighter.player_id]); + const [first, second] = ratings; + if (fighters.some((fighter) => players[fighter.player_id]?.cpu === true)) { + rows.push({ label: "RATING", values: ["Unrated", "Unrated"], lead: null }); + } else if (first !== undefined && second !== undefined) { + const change = (rating: RatingDelta): string => `${rating.after} (${rating.after >= rating.before ? "+" : "−"}${Math.abs(rating.after - rating.before)})`; + rows.push({ label: "RATING", values: [change(first), change(second)], lead: null, news: [first.after >= first.before, second.after >= second.before] }); + } + return { + headline: METHOD_HEADLINES[final.method] ?? decisionLabel(final), + detail: `ROUND ${final.round}`, + verdict: winnerSeat === null ? (final.winner_id === null ? "DRAW" : `${(players[final.winner_id]?.name ?? "Winner").toUpperCase()} WINS`) : `${names[winnerSeat].toUpperCase()} WINS`, + winnerSeat, + names, + judges, + rows, + }; +} + +const CORNER_ACCENTS = ["#4f86d9", "#d9483c"] as const; +/** Room at the foot of the card for the rematch button, which the page draws over the canvas. */ +export const RESULT_CARD_FOOTER = 62; +const CARD_PADDING = 14; + +export interface ResultCardLayout { + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; + /** The verdict beside the table rather than above it. */ + readonly wide: boolean; + /** A short screen: everything is set smaller. */ + readonly dense: boolean; + readonly rowHeight: number; + readonly verdictHeight: number; + readonly judgesHeight: number; +} + +/** The card's place on the screen: along the bottom, so the winner and the crowd stay in view above it. */ +/** The result card's place on the screen, in screen pixels: the HUD draws it scaled up on big screens. */ +export function resultCardLayout(width: number, height: number, card: ResultCard, fighter: boolean): ResultCardLayout { + const scale = hudScale(width, height); + const layout = logicalResultCardLayout(width / scale, height / scale, card, fighter); + if (scale === 1) return layout; + return { ...layout, x: layout.x * scale, y: layout.y * scale, width: layout.width * scale, height: layout.height * scale, rowHeight: layout.rowHeight * scale, verdictHeight: layout.verdictHeight * scale, judgesHeight: layout.judgesHeight * scale }; +} + +/** On a short screen (a phone on its side) the card keeps the rows that matter most, so it leaves the ring in view. */ +const DENSE_ROWS: ReadonlySet = new Set(["KNOCKDOWNS", "TOTAL PUNCHES", "RATING"]); +export const cardRows = (card: ResultCard, dense: boolean): readonly ResultRow[] => (dense ? card.rows.filter((row) => DENSE_ROWS.has(row.label)) : card.rows); + +function logicalResultCardLayout(width: number, height: number, card: ResultCard, fighter: boolean): ResultCardLayout { + const dense = height < 480; + // Side by side where there is the width for it, or no height to stack. + const wide = width >= 900 || (width >= 640 && dense); + const cardWidth = Math.min(width - 24, wide ? 980 : 720); + const footer = fighter ? RESULT_CARD_FOOTER : CARD_PADDING; + const verdictHeight = dense ? 72 : 112; + const judgesHeight = card.judges.length === 0 ? 0 : dense ? 36 : 52; + const verdict = verdictHeight + judgesHeight; + const rows = cardRows(card, dense).length + 1; + const room = height - (width < 640 ? 118 : 70) - 14 - CARD_PADDING - footer - (wide ? 0 : verdict); + const rowHeight = Math.max(16, Math.min(dense ? 20 : 28, room / rows)); + const body = wide ? Math.max(verdict, rows * rowHeight) : verdict + rows * rowHeight; + const cardHeight = CARD_PADDING + body + footer; + return { x: (width - cardWidth) / 2, y: height - 14 - cardHeight, width: cardWidth, height: cardHeight, wide, dense, rowHeight, verdictHeight, judgesHeight }; +} + +function drawResultCard(ctx: CanvasRenderingContext2D, width: number, height: number, card: ResultCard, fighter: boolean, pictures: readonly [CanvasImageSource | null, CanvasImageSource | null]): void { + const layout = logicalResultCardLayout(width, height, card, fighter); + const { x, y } = layout; + ctx.fillStyle = "rgba(3,6,12,0.9)"; + ctx.fillRect(x, y, layout.width, layout.height); + ctx.strokeStyle = "rgba(246,213,122,0.55)"; ctx.lineWidth = 2; - ctx.strokeRect(width * 0.14 + 4, height * 0.13 + 4, width * 0.72 - 8, height * 0.74 - 8); - const inner = width * 0.72 - 32; + ctx.strokeRect(x + 1, y + 1, layout.width - 2, layout.height - 2); ctx.textAlign = "center"; + + const verdictWidth = (layout.wide ? layout.width * 0.42 : layout.width) - 32; + const verdictCentre = layout.wide ? x + layout.width * 0.21 + 4 : width / 2; + const top = y + CARD_PADDING; + const scale = layout.verdictHeight / 112; ctx.fillStyle = "#f6d57a"; - fitted(ctx, decisionLabel(final), width / 2, height * 0.21, inner, 800, 26, 14); - const winner = final.winner_id === null ? "DRAW" : `${players[final.winner_id]?.name ?? "Winner"} WINS`; - ctx.fillStyle = "white"; - fitted(ctx, winner, width / 2, height * 0.27, inner, 700, 18, 11); - const thrown = punches.filter(({ stats }) => stats.thrown > 0); - if (thrown.length > 0) { - ctx.fillStyle = "#c8d3e6"; - ctx.font = "600 13px Inter, system-ui, sans-serif"; - const line = thrown.map(({ name, stats }) => `${fit(ctx, name, inner / thrown.length - 70)} ${stats.landed}/${stats.thrown} landed`).join(" · "); - fitted(ctx, line, width / 2, height * 0.32, inner, 600, 13, 9); - } - final.scorecards.forEach((card, index) => { - const y = height * 0.37 + index * 36; - ctx.fillStyle = "#aebbd0"; - fitted(ctx, `${card.judge} ${scoreTotal(card.player_one)} — ${scoreTotal(card.player_two)} [${card.player_one.join("·")}] [${card.player_two.join("·")}]`, width / 2, y, inner, 400, 12, 8, "ui-monospace, monospace"); - }); - const ratings = Object.entries(final.ratings); - ratings.forEach(([id, rating], index) => { - ctx.fillStyle = rating.after >= rating.before ? "#55df9b" : "#ff7b74"; - const change = `${rating.before} → ${rating.after} (${rating.after - rating.before >= 0 ? "+" : ""}${rating.after - rating.before})`; - fitted(ctx, `${players[id]?.name ?? "Fighter"} ${change}`, width / 2, height * 0.61 + index * 27, inner, 600, 13, 9); + fitted(ctx, card.headline, verdictCentre, top + 34 * scale, verdictWidth, 800, (layout.wide || width >= 640 ? 32 : 28) * scale, 14); + ctx.fillStyle = "#aebbd0"; + fitted(ctx, card.detail, verdictCentre, top + 56 * scale, verdictWidth, 700, 13 * scale, 9); + ctx.fillStyle = card.winnerSeat === null ? "#f6f7fb" : CORNER_ACCENTS[card.winnerSeat]; + fitted(ctx, card.verdict, verdictCentre, top + 92 * scale, verdictWidth, 800, 25 * scale, 12); + const cell = verdictWidth / Math.max(1, card.judges.length); + const judgesTop = top + layout.verdictHeight; + card.judges.forEach((judge, index) => { + const centre = verdictCentre - verdictWidth / 2 + cell * (index + 0.5); + ctx.fillStyle = "rgba(255,255,255,0.06)"; + ctx.fillRect(centre - cell / 2 + 3, judgesTop, cell - 6, layout.judgesHeight - 8); + ctx.fillStyle = "#8fa3c8"; + fitted(ctx, judge.label, centre, judgesTop + layout.judgesHeight * 0.3, cell - 14, 700, 10, 7); + ctx.fillStyle = "#f6f7fb"; + fitted(ctx, `${judge.values[0]} – ${judge.values[1]}`, centre, judgesTop + layout.judgesHeight * 0.7, cell - 14, 800, layout.dense ? 14 : 17, 10); }); + + const tableLeft = layout.wide ? x + layout.width * 0.42 : x + 12; + const tableWidth = x + layout.width - 12 - tableLeft; + const tableTop = layout.wide ? top : judgesTop + layout.judgesHeight; + const label = tableWidth * 0.36; + const side = (tableWidth - label) / 2; + const columns = [tableLeft + side / 2, tableLeft + tableWidth - side / 2] as const; + const rowHeight = layout.rowHeight; + const text = Math.min(16, rowHeight * 0.6); + let baseline = tableTop + rowHeight * 0.7; + ctx.fillStyle = "rgba(255,255,255,0.06)"; + ctx.fillRect(tableLeft, tableTop, tableWidth, rowHeight); + const face = Math.max(6, rowHeight / 2 - 2); + for (const seat of [0, 1] as const) { + const outward = seat === 0 ? -1 : 1; + ctx.fillStyle = CORNER_ACCENTS[seat]; + ctx.fillRect(seat === 0 ? tableLeft : tableLeft + tableWidth - 4, tableTop, 4, rowHeight); + portrait(ctx, columns[seat] + outward * (side / 2 - 10 - face), tableTop + rowHeight / 2, face, pictures[seat], card.names[seat], CORNER_ACCENTS[seat]); + ctx.textAlign = "center"; + ctx.fillStyle = "#f6f7fb"; + // The names have the header row to themselves, so they may reach into the label column. + fitted(ctx, card.names[seat], columns[seat] - outward * (face + 3 + label / 4), baseline, side + label / 2 - 22 - face * 2, 800, text, 9); + } + for (const row of cardRows(card, layout.dense)) { + baseline += rowHeight; + ctx.fillStyle = "#8fa3c8"; + fitted(ctx, row.label, tableLeft + tableWidth / 2, baseline, label, 700, Math.min(12, text), 8); + for (const seat of [0, 1] as const) { + ctx.fillStyle = row.news !== undefined ? (row.news[seat] ? "#55df9b" : "#ff7b74") : row.lead === seat ? "#f6f7fb" : "#aebbd0"; + fitted(ctx, row.values[seat], columns[seat], baseline, side - 16, row.lead === seat ? 800 : 600, text, 9); + } + } } diff --git a/web/hands/src/render/injury.test.ts b/web/hands/src/render/injury.test.ts new file mode 100644 index 00000000..d3e5e6ab --- /dev/null +++ b/web/hands/src/render/injury.test.ts @@ -0,0 +1,261 @@ +import * as THREE from "three"; +import { describe, expect, it } from "vitest"; +import { fighter } from "../test/fixtures"; +import type { TraumaSnapshot } from "../types"; +import { wearCornerColour } from "./gear"; +import { BoxingGraph, SkinnedBoxer, loadBoxerGlb } from "./graph"; +import { BODY_SITES, CHEEK_SWELL, EYE_LIDS, EYE_SHUT_TRAUMA, EYE_SWELL, HEAD_SITES, HEAD_SWELL_CORE, InjuryShading, applyHeadTrauma, eyeShut, trunksBloodFor } from "./injury"; +import { REFEREE_OUTFIT } from "./outfit"; +import { worldMapping } from "./world"; + +const gltf = await loadBoxerGlb(); +const mapping = worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }); +const SHADER = { + vertexShader: "#include \n#include ", + fragmentShader: "#include \n#include \n#include ", +}; + +function compile(material: THREE.Material): { uniforms: Record; vertexShader: string; fragmentShader: string } { + const shader = { uniforms: {} as Record, ...SHADER }; + material.onBeforeCompile(shader as unknown as THREE.WebGLProgramParametersWithUniforms, null as unknown as THREE.WebGLRenderer); + return shader; +} + + +describe("the inside of a head", () => { + /** A shader as the GPU compiles it while the head is whole: without the code a finisher opens it with. */ + const whole = (shader: string): string => shader.replace(/#ifdef HANDS_INJURY_OPEN[\s\S]*?#endif/gu, ""); + + it("is drawn one-sided, by a shader that discards nothing, until a finisher opens the head", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + const head = boxer.headMesh.material as THREE.MeshStandardMaterial; + const shadow = boxer.headMesh.customDepthMaterial!; + const closed = (): void => { + expect(head.side).toBe(THREE.FrontSide); + expect(head.defines).not.toHaveProperty("HANDS_INJURY_OPEN"); + expect(shadow.defines ?? {}).not.toHaveProperty("HANDS_INJURY_OPEN"); + expect(boxer.headInjury.open).toBe(false); + }; + closed(); + const skin = compile(head); + expect(skin.fragmentShader).toContain("discard"); + expect(whole(skin.fragmentShader)).not.toContain("discard"); + expect(whole(skin.fragmentShader)).not.toContain("gl_FrontFacing"); + const depth = { uniforms: {}, vertexShader: SHADER.vertexShader, fragmentShader: "#include \n#include " }; + shadow.onBeforeCompile(depth as unknown as THREE.WebGLProgramParametersWithUniforms, null as unknown as THREE.WebGLRenderer); + expect(whole(depth.fragmentShader)).not.toContain("discard"); + for (const open of [() => boxer.setDecapitated(true), () => boxer.setHeadBurst(true)]) { + open(); + expect(head.side).toBe(THREE.DoubleSide); + expect(head.shadowSide).toBe(THREE.BackSide); + expect(head.defines).toHaveProperty("HANDS_INJURY_OPEN"); + expect(shadow.defines).toHaveProperty("HANDS_INJURY_OPEN"); + // A replay puts the head back on. + boxer.setDecapitated(false); + closed(); + } + // The body shares the shader and is never opened; an official's head never is either. + expect(boxer.bodyInjury.material!.defines).not.toHaveProperty("HANDS_INJURY_OPEN"); + expect(boxer.bodyInjury.material!.side).toBe(THREE.FrontSide); + const referee = new SkinnedBoxer(gltf, { skin: 0xc79b76, gear: 0x3b57b8, outfit: REFEREE_OUTFIT }); + referee.setDecapitated(true); + expect((referee.headMesh.material as THREE.Material).side).toBe(THREE.FrontSide); + referee.dispose(); + boxer.dispose(); + }); +}); + +const trauma = (values: Partial): TraumaSnapshot => ({ head: 0, body: 0, left_eye: 0, right_eye: 0, left_cut: 0, right_cut: 0, swelling: 0, bleeding: 0, ...values }); + +describe("swelling", () => { + it("pushes tissue out from inside the skull, so the hollow round an eye cannot fold over itself", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + const head = compile(boxer.headMesh.material as THREE.Material); + expect(head.vertexShader).toContain("injuryMoved += normalize(injuryMoved - injuryCore"); + expect(head.vertexShader).not.toContain("objectNormal * injurySwell"); + const core = boxer.headInjury.uniforms.uInjuryCore.value; + expect(core.w).toBe(0); + // Inside the head mesh's bounds (x -10.6..10.3, y 104.6..128.9, z -11.4..6.7) and behind the eyes. + expect(Math.abs(core.x)).toBeLessThan(1); + expect(core.y).toBeGreaterThan(114); + expect(core.y).toBeLessThan(124); + expect(core.z).toBeLessThan(EYE_LIDS[0]![2] - 5); + // The body swells out from the spine. + expect(boxer.bodyInjury.uniforms.uInjuryCore.value.w).toBe(1); + boxer.dispose(); + }); + + it("swells a beaten eye into a two-centimetre mound that still folds no triangle of the face", () => { + const shading = new InjuryShading(new THREE.MeshStandardMaterial(), HEAD_SITES); + applyHeadTrauma(shading, trauma({ head: 1400, left_eye: 1000, right_eye: 1000, left_cut: 1000, right_cut: 1000, swelling: 1000, bleeding: 1000 }), "full"); + expect(Math.max(...shading.uniforms.uInjurySwell.value)).toBe(EYE_SWELL); + expect(EYE_SWELL).toBe(2); + expect(shading.level("rightCheek").swell).toBeCloseTo(CHEEK_SWELL, 6); + // The vertex shader's push, out from the skull's core, on the real head at the worst of it. + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + const position = boxer.headMesh.geometry.getAttribute("position"); + const index = boxer.headMesh.geometry.getIndex()!; + const core = new THREE.Vector3(...HEAD_SWELL_CORE.slice(0, 3) as [number, number, number]); + const smooth = (edge: number, x: number): number => { + const t = THREE.MathUtils.clamp(x / edge, 0, 1); + return t * t * (3 - 2 * t); + }; + const swollen = Array.from({ length: position.count }, (_, vertex) => { + const at = new THREE.Vector3().fromBufferAttribute(position, vertex); + let push = 0; + for (const [site, place] of HEAD_SITES.entries()) { + const weight = 1 - smooth(place.radius * 1.15, at.distanceTo(new THREE.Vector3(...place.position))); + push += shading.uniforms.uInjurySwell.value[site]! * weight * weight; + } + return at.clone().addScaledVector(at.clone().sub(core).add(new THREE.Vector3(0, 0, 0.001)).normalize(), push); + }); + let face = 0; + let folded = 0; + const [a, b, c] = [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()]; + for (let corner = 0; corner < index.count; corner += 3) { + const [i, j, k] = [index.getX(corner), index.getX(corner + 1), index.getX(corner + 2)]; + a.fromBufferAttribute(position, i); + b.fromBufferAttribute(position, j); + c.fromBufferAttribute(position, k); + // The front of the face, from the jaw up. + if ((a.y + b.y + c.y) / 3 < 113 || (a.z + b.z + c.z) / 3 < -1) continue; + const before = b.clone().sub(a).cross(c.clone().sub(a)); + const after = swollen[j]!.clone().sub(swollen[i]!).cross(swollen[k]!.clone().sub(swollen[i]!)); + if (before.lengthSq() < 1e-12) continue; + face += 1; + if (before.dot(after) <= 0) folded += 1; + } + expect(face).toBeGreaterThan(2000); + expect(folded).toBe(0); + boxer.dispose(); + }); + + it("opens a cheek cut wide before the doctor stops the bout, and splits a forehead swollen for a round or two", () => { + const shading = new InjuryShading(new THREE.MeshStandardMaterial(), HEAD_SITES); + // The doctor stops a bout at a cut of 800. + applyHeadTrauma(shading, trauma({ left_cut: 760, swelling: 450 }), "full"); + expect(shading.level("leftCheek").cut).toBe(1); + expect(shading.level("forehead").cut).toBeGreaterThan(0.15); + expect(shading.level("forehead").blood).toBeGreaterThan(0.15); + applyHeadTrauma(shading, trauma({ left_cut: 300, swelling: 360 }), "full"); + expect(shading.level("leftCheek").cut).toBe(0); + expect(shading.level("forehead").cut).toBe(0); + }); +}); + +describe("an eye swelling shut", () => { + it("starts to close once the eye has taken a beating and is fully shut at the eye damage that shuts it", () => { + expect(EYE_SHUT_TRAUMA).toBe(700); + expect(eyeShut(0, 0)).toBe(0); + expect(eyeShut(330, 200)).toBe(0); + expect(eyeShut(500, 0)).toBeGreaterThan(0.3); + expect(eyeShut(500, 0)).toBeLessThan(0.7); + expect(eyeShut(EYE_SHUT_TRAUMA, 0)).toBe(1); + expect(eyeShut(EYE_SHUT_TRAUMA - 1, 0)).toBeLessThan(1); + expect(eyeShut(700, 200)).toBe(1); + expect(eyeShut(500, 800)).toBeGreaterThan(eyeShut(500, 0)); + expect(eyeShut(1000, 1000)).toBe(1); + }); + + it("closes each eye from its own damage", () => { + const shading = new InjuryShading(new THREE.MeshStandardMaterial(), HEAD_SITES, { core: [0, 119, -3.5, 0], lids: EYE_LIDS }); + applyHeadTrauma(shading, trauma({ left_eye: 720, right_eye: 120 }), "full"); + expect(shading.eyesShut[0]).toBe(1); + expect(shading.eyesShut[1]).toBe(0); + shading.clear(); + expect(shading.eyesShut).toEqual([0, 0]); + }); + + it("draws lids only where there are eyes", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + boxer.headInjury.setEyesShut(1, 0.5); + expect(boxer.headInjury.eyesShut).toEqual([1, 0.5]); + boxer.bodyInjury.setEyesShut(1, 1); + expect(boxer.bodyInjury.eyesShut).toEqual([0, 0]); + const head = compile(boxer.headMesh.material as THREE.Material); + expect(head.uniforms.uInjuryLid).toBe(boxer.headInjury.uniforms.uInjuryLid); + expect(head.fragmentShader).toContain("uniform vec4 uInjuryLid[2];"); + boxer.dispose(); + }); + + it("puts blood over a closed lid, not the lid over the blood", () => { + const head = compile(new InjuryShading(new THREE.MeshStandardMaterial(), HEAD_SITES).material!); + const lids = head.fragmentShader.indexOf("vec4 lid = uInjuryLid[e];"); + const streams = head.fragmentShader.indexOf("float blood = uInjuryBlood[i];"); + expect(lids).toBeGreaterThan(0); + expect(streams).toBeGreaterThan(lids); + }); +}); + +describe("blood running from a wound", () => { + it("splits into a second rivulet only once it runs freely", () => { + const head = compile(new InjuryShading(new THREE.MeshStandardMaterial(), HEAD_SITES).material!); + expect(head.fragmentShader).toContain("if (fk > 0.5 && blood < 0.45) break;"); + }); + + it("is washed broad across the body by sweat, and only the body", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + expect(boxer.bodyInjury.uniforms.uInjuryWash.value).toBe(1); + expect(boxer.headInjury.uniforms.uInjuryWash.value).toBe(0); + expect(new InjuryShading(new THREE.MeshStandardMaterial(), BODY_SITES, { core: [0, 0, -2, 1] }).uniforms.uInjuryWash.value).toBe(0); + boxer.dispose(); + }); +}); + +describe("a cut with blood off", () => { + it("is still drawn, as a closed dark line with no raw lips and nothing wet, and bleeds nowhere", () => { + const shading = new InjuryShading(new THREE.MeshStandardMaterial(), HEAD_SITES, { core: HEAD_SWELL_CORE, lids: EYE_LIDS }); + const beaten = trauma({ head: 600, left_eye: 500, right_eye: 300, left_cut: 300, right_cut: 120, swelling: 400, bleeding: 120 }); + applyHeadTrauma(shading, beaten, "off"); + expect(shading.level("leftBrow").cut).toBe(1); + expect(shading.level("mouth").cut).toBeGreaterThan(0.2); + for (const site of HEAD_SITES) expect(shading.level(site.name).blood).toBe(0); + expect(shading.uniforms.uInjuryRaw.value).toBe(0); + applyHeadTrauma(shading, beaten, "reduced"); + expect(shading.uniforms.uInjuryRaw.value).toBeGreaterThan(0); + expect(shading.uniforms.uInjuryRaw.value).toBeLessThan(1); + applyHeadTrauma(shading, beaten, "full"); + expect(shading.uniforms.uInjuryRaw.value).toBe(1); + // The raw lips, the width of the gash and its wetness all go with it. + const head = compile(shading.material!); + expect(head.uniforms.uInjuryRaw).toBe(shading.uniforms.uInjuryRaw); + expect(head.fragmentShader).toContain("float lip = (1.0 - smoothstep(0.1 * taper, (0.22 + cut * 0.12) * taper, slit)) * (1.0 - gash) * uInjuryRaw;"); + expect(head.fragmentShader).toContain("(0.03 + cut * 0.05 * uInjuryRaw) * taper"); + expect(head.fragmentShader).toContain("injuryWet = max(injuryWet, cut * max(gash, lip * 0.6) * uInjuryRaw);"); + }); +}); + +describe("blood in the trunks", () => { + it("runs in from a fighter's own bleeding, lighter with reduced blood and never with blood off", () => { + expect(trunksBloodFor(trauma({ bleeding: 30, left_cut: 60 }), "full")).toBe(0); + const bleeding = trauma({ bleeding: 300, left_cut: 300, right_cut: 150 }); + expect(trunksBloodFor(bleeding, "full")).toBeGreaterThan(0.6); + expect(trunksBloodFor(bleeding, "reduced")).toBeLessThan(trunksBloodFor(bleeding, "full") * 0.5); + expect(trunksBloodFor(bleeding, "off")).toBe(0); + expect(trunksBloodFor(trauma({ bleeding: 1000, left_cut: 1000, right_cut: 1000 }), "full")).toBe(1); + }); + + it("soaks the front of the waistband down, a part of the trunks shader of its own", () => { + const trunks = new THREE.MeshStandardMaterial(); + const blood = wearCornerColour(trunks, false, "trunks"); + expect(trunks.defines).toHaveProperty("GEAR_TRUNKS"); + expect(compile(trunks).uniforms.uGearBlood).toBe(blood); + const gloves = new THREE.MeshStandardMaterial(); + wearCornerColour(gloves); + expect(gloves.defines ?? {}).not.toHaveProperty("GEAR_TRUNKS"); + }); + + it("follows the fighter's bleeding through the graph", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + const graph = new BoxingGraph(boxer, mapping); + const one = { ...fighter("one"), x: 0, y: 0, facing_x: 0, facing_y: -1000 }; + const two = { ...fighter("two"), x: 0, y: -150, facing_x: 0, facing_y: 1000 }; + graph.update(one, two, 1 / 60, 0, false, "full", 1, undefined); + expect(boxer.trunksBloodLevel).toBe(0); + graph.update({ ...one, trauma: trauma({ bleeding: 460, left_cut: 300 }) }, two, 1 / 60, 0.02, false, "full", 2, undefined); + expect(boxer.trunksBloodLevel).toBeGreaterThan(0.9); + graph.update({ ...one, trauma: trauma({ bleeding: 460, left_cut: 300 }) }, two, 1 / 60, 0.04, false, "off", 3, undefined); + expect(boxer.trunksBloodLevel).toBe(0); + graph.dispose(); + }); +}); diff --git a/web/hands/src/render/injury.ts b/web/hands/src/render/injury.ts index d8782bf5..8ab0ff73 100644 --- a/web/hands/src/render/injury.ts +++ b/web/hands/src/render/injury.ts @@ -1,4 +1,5 @@ import * as THREE from "three"; +import { EYE_SHUT_TRAUMA } from "../manifest"; import type { BloodLevel } from "../settings"; import type { TraumaSnapshot } from "../types"; @@ -55,6 +56,25 @@ export const BODY_SITES: readonly InjurySite[] = [ { name: "unused3", position: [0, -100, 0], radius: 0, cutHalfLength: 0, bleeds: false }, ]; +/** Height of a cut through the neck in bind space, at the spine. It slopes down toward the throat so the chin stays on the head. */ +export const NECK_CUT_HEIGHT = 112.4; +export const NECK_CUT_SLOPE = 0.3; +export const NECK_CUT_DEPTH = -4; +/** Where a head that bursts is gone above: level with the mouth, so the lower jaw and its teeth stay on the neck. */ +export const BURST_CUT_HEIGHT = 114.5; +const BURST_RAGGED = 1.8; +const UNCUT = 1000; + +/** The cut line, shared by the skin and the shadow pass. `uInjurySeverShape` is its slope toward the throat and how ragged it is. */ +const SEVER = /* glsl */ ` +uniform float uInjurySever; +uniform vec2 uInjurySeverShape; +float injuryCutLine(vec3 p) { + float around = atan(p.z + 3.6, p.x); + return uInjurySever - uInjurySeverShape.x * (p.z - (${NECK_CUT_DEPTH.toFixed(1)})) + uInjurySeverShape.y * (0.4 * sin(around * 6.0) + 0.22 * sin(around * 13.0 + 1.7)); +} +`; + export interface InjuryLevels { readonly bruise: Float32Array; readonly swell: Float32Array; @@ -62,42 +82,76 @@ export interface InjuryLevels { readonly blood: Float32Array; } +/** + * Where swollen tissue pushes out from, in bind space: a point inside the skull for the head, and + * the spine's vertical axis for the body (w = 1). Pushing along each vertex normal instead folded + * the hollow round an eye over itself. + */ +export const HEAD_SWELL_CORE: readonly [number, number, number, number] = [0, 119, -3.5, 0]; +export const BODY_SWELL_CORE: readonly [number, number, number, number] = [0, 0, -2, 1]; +/** Middle of each eye opening in bind space, for the lids that swell shut over it: left, then right. */ +export const EYE_LIDS: readonly (readonly [number, number, number])[] = [ + [2.65, 120.65, 3.6], + [-2.75, 120.75, 3.4], +]; + const VERTEX_DECLARATIONS = /* glsl */ ` uniform vec4 uInjurySite[${INJURY_SITE_COUNT}]; uniform float uInjurySwell[${INJURY_SITE_COUNT}]; +uniform vec4 uInjuryCore; +uniform vec4 uInjuryLid[2]; +uniform vec2 uInjuryEyeOut; uniform float uInjuryJaw; uniform float uInjuryJawLevel; uniform vec4 uInjuryImpact; uniform vec3 uInjuryImpactPush; uniform vec4 uInjuryNose; +uniform mat3 uInjuryBindToPart; varying vec3 vInjuryPos; `; -const VERTEX_BODY = /* glsl */ ` -vInjuryPos = transformed; +/** + * The swelling, the broken nose, a punch's dent, an empty socket and a dislocated jaw, worked out on a + * bind-space point (centimetres). A live head is drawn in bind space and moves the point itself. A + * severed head is baked into a frame of its own: it reads its bind positions from the `bindPosition` + * attribute, which its baked look declares, and turns the offsets into its frame. + */ +const vertexBody = (baked: boolean): string => /* glsl */ ` { + vec3 injuryBind = ${baked ? "bindPosition" : "transformed"}; + vInjuryPos = injuryBind; + vec3 injuryMoved = injuryBind; float injurySwell = 0.0; for (int i = 0; i < ${INJURY_SITE_COUNT}; i++) { float radius = uInjurySite[i].w; if (radius <= 0.0) continue; - float d = distance(transformed, uInjurySite[i].xyz); + float d = distance(injuryMoved, uInjurySite[i].xyz); float w = 1.0 - smoothstep(0.0, radius * 1.15, d); injurySwell += uInjurySwell[i] * w * w; } - transformed += objectNormal * injurySwell; - float noseWeight = 1.0 - smoothstep(0.0, 2.6, distance(transformed, uInjuryNose.xyz)); - transformed.x += uInjuryNose.w * 1.1 * noseWeight * noseWeight; - transformed.z -= abs(uInjuryNose.w) * 0.5 * noseWeight * noseWeight; - float impactDistance = distance(transformed, uInjuryImpact.xyz); + vec3 injuryCore = uInjuryCore.w > 0.5 ? vec3(uInjuryCore.x, injuryMoved.y, uInjuryCore.z) : uInjuryCore.xyz; + injuryMoved += normalize(injuryMoved - injuryCore + vec3(0.0, 0.0, 0.001)) * injurySwell; + float noseWeight = 1.0 - smoothstep(0.0, 2.6, distance(injuryMoved, uInjuryNose.xyz)); + injuryMoved.x += uInjuryNose.w * 1.1 * noseWeight * noseWeight; + injuryMoved.z -= abs(uInjuryNose.w) * 0.5 * noseWeight * noseWeight; + float impactDistance = distance(injuryMoved, uInjuryImpact.xyz); float impactWeight = 1.0 - smoothstep(0.0, max(uInjuryImpact.w, 0.001), impactDistance); - transformed += uInjuryImpactPush * (impactWeight * impactWeight); + injuryMoved += uInjuryImpactPush * (impactWeight * impactWeight); + // An eye forced out leaves its socket hollow. + for (int e = 0; e < 2; e++) { + float gone = e == 0 ? uInjuryEyeOut.x : uInjuryEyeOut.y; + if (gone <= 0.0) continue; + float socket = 1.0 - smoothstep(0.0, 1.7, distance(injuryMoved, uInjuryLid[e].xyz)); + injuryMoved.z -= 1.1 * socket * socket * gone; + } // Only the mandible: the mask fades out again under the chin, above the throat and the seam with the body. - float jawMask = (1.0 - smoothstep(uInjuryJawLevel - 2.5, uInjuryJawLevel + 1.5, transformed.y)) - * smoothstep(uInjuryJawLevel - 6.0, uInjuryJawLevel - 4.0, transformed.y) - * smoothstep(-6.0, 0.0, transformed.z); - transformed.x += uInjuryJaw * 1.7 * jawMask; - transformed.y -= uInjuryJaw * 0.9 * jawMask; - transformed.z -= uInjuryJaw * 0.4 * jawMask; + float jawMask = (1.0 - smoothstep(uInjuryJawLevel - 2.5, uInjuryJawLevel + 1.5, injuryMoved.y)) + * smoothstep(uInjuryJawLevel - 6.0, uInjuryJawLevel - 4.0, injuryMoved.y) + * smoothstep(-6.0, 0.0, injuryMoved.z); + injuryMoved.x += uInjuryJaw * 1.7 * jawMask; + injuryMoved.y -= uInjuryJaw * 0.9 * jawMask; + injuryMoved.z -= uInjuryJaw * 0.4 * jawMask; + ${baked ? "transformed += uInjuryBindToPart * (injuryMoved - injuryBind);" : "transformed = injuryMoved;"} } `; @@ -107,64 +161,153 @@ uniform vec4 uInjuryCut[${INJURY_SITE_COUNT}]; uniform float uInjuryBruise[${INJURY_SITE_COUNT}]; uniform float uInjuryBlood[${INJURY_SITE_COUNT}]; uniform float uInjuryWetness; +uniform float uInjuryWash; +uniform float uInjuryRaw; +uniform vec4 uInjuryLid[2]; +uniform vec2 uInjuryEyeOut; varying vec3 vInjuryPos; float injuryWet = 0.0; +${SEVER} float injuryHash(float n) { return fract(sin(n) * 43758.5453123); } `; +/** + * Set on a head while a finisher has it open (cut off or burst): only then is its shader the one that + * cuts it away along the line and draws its inside, from both sides. A whole head, and the body, draw + * one-sided with nothing that can discard a pixel, which a tile-based phone GPU needs to cull early. + */ +const OPEN = "HANDS_INJURY_OPEN"; + const FRAGMENT_BODY = /* glsl */ ` { vec3 injuryPos = vInjuryPos; - vec3 hematomaFresh = vec3(0.31, 0.05, 0.08); - vec3 hematomaDeep = vec3(0.12, 0.035, 0.13); +#ifdef ${OPEN} + { + float below = injuryCutLine(injuryPos) - injuryPos.y; + if (below < 0.0) discard; + float torn = 1.0 - smoothstep(0.0, 1.8, below); + diffuseColor.rgb = mix(diffuseColor.rgb, vec3(0.2, 0.008, 0.012), torn * 0.94); + injuryWet = max(injuryWet, torn); + } +#endif + vec3 hematomaFresh = vec3(0.42, 0.09, 0.11); + vec3 hematomaDeep = vec3(0.2, 0.04, 0.07); vec3 bloodColor = vec3(0.30, 0.008, 0.012); - vec3 cutColor = vec3(0.13, 0.004, 0.005); + vec3 bloodEdge = vec3(0.13, 0.0, 0.004); + vec3 cutColor = vec3(0.1, 0.002, 0.004); + vec3 rawFlesh = vec3(0.42, 0.05, 0.06); for (int i = 0; i < ${INJURY_SITE_COUNT}; i++) { float radius = uInjurySite[i].w; if (radius <= 0.0) continue; vec3 site = uInjurySite[i].xyz; - float d = distance(injuryPos, site); float bruise = uInjuryBruise[i]; if (bruise > 0.001) { - float mottle = 0.92 + 0.08 * sin(injuryPos.x * 3.1 + injuryPos.y * 2.3) * sin(injuryPos.z * 2.7 - injuryPos.y * 1.9); - float w = (1.0 - smoothstep(radius * 0.2, radius, d)) * mottle; - vec3 tone = mix(hematomaFresh, hematomaDeep, clamp(bruise * 1.2 - 0.2, 0.0, 1.0)); - vec3 stained = mix(diffuseColor.rgb * tone * 2.0, tone * 1.1, 0.35); - diffuseColor.rgb = mix(diffuseColor.rgb, stained, clamp(bruise, 0.0, 1.0) * w * 0.8); + // Wider than tall, with an uneven edge: a bruise follows the bone under it, not a compass. + vec3 offset = injuryPos - site; + float ragged = 1.0 + 0.18 * sin(atan(offset.y, offset.x) * 5.0 + float(i) * 2.3) + 0.08 * sin(offset.z * 3.1 + offset.x * 2.2); + float spread = length(vec3(offset.x, offset.y * 1.35, offset.z)) / ragged; + float mottle = 0.85 + 0.15 * sin(injuryPos.x * 3.1 + injuryPos.y * 2.3) * sin(injuryPos.z * 2.7 - injuryPos.y * 1.9); + float w = (1.0 - smoothstep(radius * 0.15, radius * 1.05, spread)) * mottle; + float core = 1.0 - smoothstep(0.0, radius * 0.6, spread); + vec3 tone = mix(hematomaFresh, hematomaDeep, clamp(bruise * 1.1 - 0.15 + core * 0.35, 0.0, 1.0)); + vec3 stained = diffuseColor.rgb * tone / vec3(0.6, 0.32, 0.19); + diffuseColor.rgb = mix(diffuseColor.rgb, mix(stained, tone, 0.25), clamp(bruise, 0.0, 1.0) * w * 0.85); } float cut = uInjuryCut[i].y; float halfLength = uInjuryCut[i].x; if (cut > 0.001 && halfLength > 0.0) { + // A split: a dark gash between swollen raw lips, tapering to its ends. Without blood it is closed, + // a thin dark line with no raw flesh and nothing wet about it. vec3 local = injuryPos - site; float along = clamp(local.x, -halfLength, halfLength); + float taper = 1.0 - 0.6 * (abs(along) / halfLength) * (abs(along) / halfLength); vec2 delta = vec2(local.x - along, local.y - 0.18 * sin(local.x * 3.0)); float slit = length(delta) + abs(local.z) * 0.25; - float w = 1.0 - smoothstep(0.06 + cut * 0.06, 0.24 + cut * 0.12, slit); - float lip = (1.0 - smoothstep(0.24, 0.55, slit)) * 0.6; - diffuseColor.rgb = mix(diffuseColor.rgb, hematomaFresh * 0.8, cut * lip); - diffuseColor.rgb = mix(diffuseColor.rgb, cutColor, cut * w); - injuryWet = max(injuryWet, cut * w); + float gash = 1.0 - smoothstep((0.03 + cut * 0.05 * uInjuryRaw) * taper, (0.09 + cut * 0.09 * uInjuryRaw) * taper, slit); + float lip = (1.0 - smoothstep(0.1 * taper, (0.22 + cut * 0.12) * taper, slit)) * (1.0 - gash) * uInjuryRaw; + diffuseColor.rgb = mix(diffuseColor.rgb, rawFlesh, cut * lip * 0.75); + diffuseColor.rgb = mix(diffuseColor.rgb, mix(vec3(0.12, 0.06, 0.05), cutColor, uInjuryRaw), cut * gash); + injuryWet = max(injuryWet, cut * max(gash, lip * 0.6) * uInjuryRaw); } + } + // An eye swollen shut: the lids meet over it in a tight, shiny, purple-red mound closed to a dark slit. + vec3 lidSkin = vec3(0.6, 0.32, 0.19) * diffuse; + for (int e = 0; e < 2; e++) { + vec4 lid = uInjuryLid[e]; + if (lid.w <= 0.001) continue; + vec3 q = injuryPos - lid.xyz; + float across = q.x / 1.85; + float lifted = q.y + 0.1 * q.x * q.x; + float upDown = lifted / (lifted > 0.0 ? 0.8 + 0.35 * lid.w : 0.5 + 0.22 * lid.w); + float reach = length(vec2(across, upDown)) + max(0.0, -q.z - 1.0) * 1.5; + float closed = (1.0 - smoothstep(0.7, 1.08, reach)) * smoothstep(0.0, 0.85, lid.w); + if (closed <= 0.0) continue; + float dome = 1.0 - reach * reach * 0.6; + vec3 puffed = lidSkin * mix(vec3(0.95, 0.62, 0.72), vec3(0.78, 0.4, 0.62), lid.w) * (0.8 + 0.35 * dome); + float crease = (1.0 - smoothstep(0.035, 0.07 + 0.12 * (1.0 - lid.w), abs(lifted + 0.12))) * (1.0 - smoothstep(0.55, 0.95, abs(across))); + diffuseColor.rgb = mix(diffuseColor.rgb, mix(puffed, vec3(0.03, 0.006, 0.01), crease * 0.92), closed); + injuryWet = max(injuryWet, closed * 0.35 * (1.0 - crease)); + } + // An empty socket: a raw red rim round a dark hollow where the eye was. + for (int e = 0; e < 2; e++) { + float gone = e == 0 ? uInjuryEyeOut.x : uInjuryEyeOut.y; + if (gone <= 0.0) continue; + vec3 q = injuryPos - uInjuryLid[e].xyz; + float reach = length(vec2(q.x / 1.45, (q.y + 0.1 * q.x * q.x) / 1.0)) + max(0.0, -q.z - 1.6) * 1.5; + float hollow = (1.0 - smoothstep(0.7, 1.05, reach)) * gone; + vec3 inside = mix(vec3(0.03, 0.0, 0.004), rawFlesh * 1.15, smoothstep(0.3, 0.85, reach)); + diffuseColor.rgb = mix(diffuseColor.rgb, inside, hollow); + injuryWet = max(injuryWet, hollow); + } + for (int i = 0; i < ${INJURY_SITE_COUNT}; i++) { + float radius = uInjurySite[i].w; float blood = uInjuryBlood[i]; - if (blood > 0.001) { - vec3 origin = site + vec3(0.0, -radius * 0.35, 0.0); - float length = 3.5 + blood * 9.0; - float drop = origin.y - injuryPos.y; - if (drop > -0.4 && drop < length && injuryPos.z > origin.z - 3.2) { - float t = clamp(drop / length, 0.0, 1.0); - float wander = 0.4 * sin(injuryPos.y * 1.6 + float(i) * 1.7) + 0.25 * sin(injuryPos.y * 4.3 + float(i)); - float dx = abs(injuryPos.x - origin.x - wander * t); - float width = 0.28 + 0.28 * t + 0.25 * blood; - float edge = width + 0.18; - float stream = (1.0 - smoothstep(width * 0.6, edge, dx)) * (1.0 - t * t) * smoothstep(-0.4, 0.15, drop); - float head = (1.0 - smoothstep(0.0, radius * 0.9, distance(injuryPos, origin))) * 0.7; - float w = clamp(max(stream, head) * min(1.0, blood * 1.4), 0.0, 1.0); - diffuseColor.rgb = mix(diffuseColor.rgb, bloodColor, w * 0.96); - injuryWet = max(injuryWet, w); - } + if (radius <= 0.0 || blood <= 0.001) continue; + vec3 origin = uInjurySite[i].xyz + vec3(0.0, -radius * 0.35, 0.0); + float drop = origin.y - injuryPos.y; + if (drop < -0.6 || injuryPos.z < origin.z - 3.2) continue; + float flow = smoothstep(0.0, 0.3, blood); + float well = (1.0 - smoothstep(0.0, 0.3 + blood * 0.4, distance(injuryPos.xy, origin.xy))) * flow; + float w = well; + // Two rivulets of their own lengths run from each wound, wandering, thinning, with a bead at each tip. + for (int k = 0; k < 2; k++) { + float fk = float(k); + if (fk > 0.5 && blood < 0.45) break; + float seed = fract(sin(float(i) * 12.9898 + fk * 78.233) * 43758.5453); + float reach = (2.0 + blood * 9.0) * (fk < 0.5 ? 1.0 : 0.55 + 0.3 * seed); + float start = (fk < 0.5 ? -0.3 : 0.4) * (0.6 + 0.6 * seed) * radius * 0.35; + float t = clamp(drop / reach, 0.0, 1.0); + float wander = (0.35 * sin(injuryPos.y * 1.3 + float(i) * 1.7 + fk * 2.1) + 0.15 * sin(injuryPos.y * 3.7 + fk)) * t; + float dx = abs(injuryPos.x - origin.x - start - wander); + float width = (0.26 - 0.12 * t) * (0.7 + 0.45 * min(1.0, blood)); + float run = (1.0 - smoothstep(width * 0.55, width, dx)) * step(drop, reach) * smoothstep(-0.4, 0.1, drop); + float tipY = origin.y - reach; + vec2 tip = vec2(origin.x + start + 0.35 * sin(tipY * 1.3 + float(i) * 1.7 + fk * 2.1) + 0.15 * sin(tipY * 3.7 + fk), tipY); + float bead = 1.0 - smoothstep(width * 1.15, width * 1.15 + 0.1, distance(injuryPos.xy, tip)); + float stream = max(run, bead) * flow; + float edge = smoothstep(width * 0.2, width * 0.85, dx); + diffuseColor.rgb = mix(diffuseColor.rgb, mix(bloodColor, bloodEdge, edge * 0.6), stream * 0.95); + w = max(w, stream); + } + diffuseColor.rgb = mix(diffuseColor.rgb, bloodColor, well * 0.92); + if (uInjuryWash > 0.5) { + // On the body, sweat thins the blood into a broad wash that the gloves smear across the chest. + float t = clamp(drop / (radius * 3.2 + blood * 10.0), 0.0, 1.0); + float across = abs(injuryPos.x - origin.x + 0.9 * sin(injuryPos.y * 0.45 + float(i))); + float wash = (1.0 - smoothstep(radius * (0.5 + 0.4 * t), radius * (1.0 + 0.7 * t), across)) * (1.0 - t * t) * smoothstep(-0.6, 0.6, drop) * flow; + diffuseColor.rgb = mix(diffuseColor.rgb, diffuseColor.rgb * vec3(0.95, 0.26, 0.26) + vec3(0.07, 0.0, 0.0), wash * min(1.0, blood) * 0.7); + w = max(w, wash * 0.5); } + injuryWet = max(injuryWet, w); } +#ifdef ${OPEN} + // Where a cut or a burst opens the head, its inside shows as raw flesh rather than nothing. + if (!gl_FrontFacing) { + diffuseColor.rgb = vec3(0.2, 0.012, 0.022); + injuryWet = 1.0; + } +#endif injuryWet *= uInjuryWetness; } `; @@ -188,12 +331,32 @@ export class InjuryShading { uInjuryImpactPush: { value: new THREE.Vector3() }, uInjuryNose: { value: new THREE.Vector4(0, -1000, 0, 0) }, uInjuryWetness: { value: 1 }, + uInjuryRaw: { value: 1 }, + uInjurySever: { value: UNCUT }, + uInjurySeverShape: { value: new THREE.Vector2(NECK_CUT_SLOPE, 1) }, + uInjuryCore: { value: new THREE.Vector4(...HEAD_SWELL_CORE) }, + uInjuryWash: { value: 0 }, + uInjuryLid: { value: [new THREE.Vector4(0, -1000, 0, 0), new THREE.Vector4(0, -1000, 0, 0)] }, + uInjuryEyeOut: { value: new THREE.Vector2() }, + uInjuryBindToPart: { value: new THREE.Matrix3() }, }; private readonly index = new Map(); + private shadow: THREE.MeshDepthMaterial | null = null; - /** With no material the state is kept but shades nothing: an official, who never takes damage. */ - constructor(readonly material: THREE.MeshStandardMaterial | null, readonly sites: readonly InjurySite[]) { + /** + * With no material the state is kept but shades nothing: an official, who never takes damage. A + * `baked` material draws a severed head, whose geometry is in a frame of its own and carries its bind + * positions, read through the baked look the material must also be given (see `setBindToPart`). + */ + constructor( + readonly material: THREE.MeshStandardMaterial | null, + readonly sites: readonly InjurySite[], + shape: { readonly core: readonly [number, number, number, number]; readonly lids?: readonly (readonly [number, number, number])[]; readonly wash?: boolean; readonly baked?: boolean } = { core: HEAD_SWELL_CORE }, + ) { if (sites.length !== INJURY_SITE_COUNT) throw new Error(`injury shading requires ${INJURY_SITE_COUNT} sites`); + this.uniforms.uInjuryCore.value.set(...shape.core); + this.uniforms.uInjuryWash.value = shape.wash === true ? 1 : 0; + for (const [index, lid] of (shape.lids ?? []).slice(0, 2).entries()) this.uniforms.uInjuryLid.value[index]!.set(lid[0], lid[1], lid[2], 0); for (const [index, site] of sites.entries()) { this.index.set(site.name, index); this.uniforms.uInjurySite.value.push(new THREE.Vector4(site.position[0], site.position[1], site.position[2], site.radius)); @@ -201,17 +364,18 @@ export class InjuryShading { } if (material === null) return; const uniforms = this.uniforms; + const baked = shape.baked === true; material.onBeforeCompile = (shader) => { Object.assign(shader.uniforms, uniforms); shader.vertexShader = shader.vertexShader .replace("#include ", `#include \n${VERTEX_DECLARATIONS}`) - .replace("#include ", `#include \n${VERTEX_BODY}`); + .replace("#include ", `#include \n${vertexBody(baked)}`); shader.fragmentShader = shader.fragmentShader .replace("#include ", `#include \n${FRAGMENT_DECLARATIONS}`) .replace("#include ", `#include \n${FRAGMENT_BODY}`) .replace("#include ", `#include \n${ROUGHNESS_BODY}`); }; - material.customProgramCacheKey = () => `hands-injury-${INJURY_SITE_COUNT}`; + material.customProgramCacheKey = () => `hands-injury-${INJURY_SITE_COUNT}${baked ? "-baked" : ""}`; material.needsUpdate = true; } @@ -255,16 +419,132 @@ export class InjuryShading { return this.uniforms.uInjuryImpactPush.value.length(); } + /** Removes everything above a cut through the neck, leaving a torn, bloodied edge. */ + setSevered(severed: boolean): void { + this.uniforms.uInjurySever.value = severed ? NECK_CUT_HEIGHT : UNCUT; + this.uniforms.uInjurySeverShape.value.set(NECK_CUT_SLOPE, 1); + this.setOpen(severed); + } + + /** Removes everything above the mouth in a ragged tear, as when the head bursts. */ + setBurst(burst: boolean): void { + this.uniforms.uInjurySever.value = burst ? BURST_CUT_HEIGHT : UNCUT; + this.uniforms.uInjurySeverShape.value.set(burst ? 0 : NECK_CUT_SLOPE, burst ? BURST_RAGGED : 1); + this.setOpen(burst); + } + + /** Whether the head is drawn by the shader that cuts it open, from both sides. */ + get open(): boolean { + return this.material?.defines?.[OPEN] !== undefined; + } + + /** + * Switches the head and its shadow to the shaders that cut it open, or back. Each is a program of its + * own, which the renderer compiles before the bout so that a finisher compiles nothing. + */ + setOpen(open: boolean): void { + const material = this.material; + if (material === null || open === this.open) return; + for (const variant of [material, this.shadow]) { + if (variant === null) continue; + const defines = (variant.defines ??= {}); + if (open) defines[OPEN] = ""; + else delete defines[OPEN]; + variant.needsUpdate = true; + } + material.side = open ? THREE.DoubleSide : THREE.FrontSide; + } + + /** Shadow pass material for the same mesh, so a severed head casts no shadow from the shoulders. */ + shadowMaterial(): THREE.MeshDepthMaterial { + const material = new THREE.MeshDepthMaterial(); + const uniforms = { uInjurySever: this.uniforms.uInjurySever, uInjurySeverShape: this.uniforms.uInjurySeverShape }; + material.onBeforeCompile = (shader) => { + Object.assign(shader.uniforms, uniforms); + shader.vertexShader = shader.vertexShader + .replace("#include ", "#include \nvarying vec3 vInjuryPos;") + .replace("#include ", "#include \nvInjuryPos = transformed;"); + shader.fragmentShader = shader.fragmentShader + .replace("#include ", `#include \nvarying vec3 vInjuryPos;\n${SEVER}`) + .replace("#include ", `#include \n#ifdef ${OPEN}\nif (vInjuryPos.y > injuryCutLine(vInjuryPos)) discard;\n#endif`); + }; + material.customProgramCacheKey = () => "hands-injury-shadow"; + if (this.open) material.defines = { [OPEN]: "" }; + this.shadow = material; + return material; + } + + /** + * Takes on another head's injuries as they are now, all but its cut through the neck and the dent of a + * punch landing: a severed head keeps the face it was beaten into. + */ + copyFrom(other: InjuryShading): void { + const from = other.uniforms; + const to = this.uniforms; + to.uInjuryBruise.value.set(from.uInjuryBruise.value); + to.uInjurySwell.value.set(from.uInjurySwell.value); + to.uInjuryBlood.value.set(from.uInjuryBlood.value); + for (const [index, cut] of to.uInjuryCut.value.entries()) cut.y = from.uInjuryCut.value[index]?.y ?? 0; + for (const [index, lid] of to.uInjuryLid.value.entries()) lid.w = from.uInjuryLid.value[index]?.w ?? 0; + to.uInjuryEyeOut.value.copy(from.uInjuryEyeOut.value); + to.uInjuryNose.value.copy(from.uInjuryNose.value); + to.uInjuryJaw.value = from.uInjuryJaw.value; + to.uInjuryWetness.value = from.uInjuryWetness.value; + to.uInjuryRaw.value = from.uInjuryRaw.value; + to.uInjuryImpactPush.value.set(0, 0, 0); + } + + /** For a baked head: the turn and scale from bind space (centimetres) into the frame its geometry is baked in. */ + setBindToPart(matrix: THREE.Matrix3): void { + this.uniforms.uInjuryBindToPart.value.copy(matrix); + } + + /** How open and raw the cuts are drawn, from 1 (gashes between raw lips) to 0 (closed dark lines, for blood off). */ + setRawCuts(raw: number): void { + this.uniforms.uInjuryRaw.value = THREE.MathUtils.clamp(raw, 0, 1) || 0; + } + setJaw(amount: number): void { this.uniforms.uInjuryJaw.value = THREE.MathUtils.clamp(amount, 0, 1); } + /** How far each eye has swollen shut, 0 open to 1 closed: left, then right. */ + setEyesShut(left: number, right: number): void { + const [leftLid, rightLid] = this.uniforms.uInjuryLid.value; + if (leftLid!.y > -500) leftLid!.w = THREE.MathUtils.clamp(left, 0, 1) || 0; + if (rightLid!.y > -500) rightLid!.w = THREE.MathUtils.clamp(right, 0, 1) || 0; + } + + /** Hollows a socket whose eye has been forced out: `side` is the fighter's own. */ + setEyeOut(side: "left" | "right" | null): void { + const lids = this.uniforms.uInjuryLid.value; + const left = side === "left" && lids[0]!.y > -500; + const right = side === "right" && lids[1]!.y > -500; + this.uniforms.uInjuryEyeOut.value.set(left ? 1 : 0, right ? 1 : 0); + // The empty socket runs with blood down the cheek. + if (this.index.has("leftEye")) this.set("leftEye", { blood: left ? 1.3 : 0 }); + if (this.index.has("rightEye")) this.set("rightEye", { blood: right ? 1.3 : 0 }); + } + + get eyeOut(): "left" | "right" | null { + const out = this.uniforms.uInjuryEyeOut.value; + return out.x > 0 ? "left" : out.y > 0 ? "right" : null; + } + + get eyesShut(): readonly [number, number] { + return [this.uniforms.uInjuryLid.value[0]!.w, this.uniforms.uInjuryLid.value[1]!.w]; + } + clear(): void { this.uniforms.uInjuryBruise.value.fill(0); this.uniforms.uInjurySwell.value.fill(0); this.uniforms.uInjuryBlood.value.fill(0); for (const cut of this.uniforms.uInjuryCut.value) cut.y = 0; this.uniforms.uInjuryJaw.value = 0; + for (const lid of this.uniforms.uInjuryLid.value) lid.w = 0; + this.uniforms.uInjuryEyeOut.value.set(0, 0); + this.uniforms.uInjuryNose.value.w = 0; + this.uniforms.uInjuryImpactPush.value.set(0, 0, 0); } level(name: string): { bruise: number; swell: number; cut: number; blood: number } { @@ -280,21 +560,47 @@ export class InjuryShading { const bloodScale = (blood: BloodLevel): number => (blood === "off" ? 0 : blood === "reduced" ? 0.35 : 1); +/** Eye damage at which an eye is swollen fully shut, whatever the rest of the face: the engine's blind side starts there too. */ +export { EYE_SHUT_TRAUMA }; +/** Eye damage at which the lids start to swell together. */ +const EYE_CLOSING_TRAUMA = 330; + +/** An eye starts to close once it has taken a beating, sooner as the face swells, and is shut at `EYE_SHUT_TRAUMA`. */ +export function eyeShut(eyeTrauma: number, swelling: number): number { + return THREE.MathUtils.clamp((eyeTrauma - EYE_CLOSING_TRAUMA) / (EYE_SHUT_TRAUMA - EYE_CLOSING_TRAUMA) + Math.max(0, swelling - 200) / 1600, 0, 1); +} + +/** + * The most an eye and a cheek swell, in bind-space centimetres pushed out from inside the skull: an eye + * beaten shut stands out two centimetres in a grotesque mound. The push is radial from the skull's core, + * so even this much folds no triangle of the face. + */ +export const EYE_SWELL = 2; +export const CHEEK_SWELL = 1.8; +/** A cheek cut is wide open by this much cut damage, short of the 800 at which the doctor stops the bout. */ +const CHEEK_CUT_OPEN = 750; +/** Swelling at which the forehead splits: a face battered for a round or two, where 520 was all but out of reach. */ +const FOREHEAD_SPLITS = 380; + /** Maps authoritative trauma onto head injury sites. */ export function applyHeadTrauma(shading: InjuryShading, trauma: TraumaSnapshot, blood: BloodLevel): void { const bleed = bloodScale(blood); const graphic = blood === "full" ? 1 : 0.6; + shading.setRawCuts(blood === "full" ? 1 : blood === "reduced" ? 0.6 : 0); const eye = (value: number): number => Math.min(1.2, value / 380 + trauma.swelling / 900); - const swell = (value: number): number => Math.min(2.2, (value / 520 + trauma.swelling / 700) * 1.6 * graphic); + const swell = (value: number): number => Math.min(EYE_SWELL, (value / 600 + trauma.swelling / 900) * graphic); shading.set("leftEye", { bruise: eye(trauma.left_eye), swell: swell(trauma.left_eye) }); shading.set("rightEye", { bruise: eye(trauma.right_eye), swell: swell(trauma.right_eye) }); + shading.setEyesShut(eyeShut(trauma.left_eye, trauma.swelling), eyeShut(trauma.right_eye, trauma.swelling)); const browCut = (cut: number): number => Math.min(1, cut / 220); const browBlood = (cut: number): number => Math.min(1.4, (cut / 260 + trauma.bleeding / 520) * bleed); shading.set("leftBrow", { bruise: Math.min(1, trauma.left_eye / 700), swell: Math.min(1.2, trauma.left_eye / 900) * graphic, cut: browCut(trauma.left_cut), blood: browBlood(trauma.left_cut) }); shading.set("rightBrow", { bruise: Math.min(1, trauma.right_eye / 700), swell: Math.min(1.2, trauma.right_eye / 900) * graphic, cut: browCut(trauma.right_cut), blood: browBlood(trauma.right_cut) }); const cheek = Math.min(1, trauma.head / 950 + trauma.swelling / 1100); - shading.set("leftCheek", { bruise: cheek * 0.9, swell: Math.min(1.4, trauma.swelling / 800) * graphic, cut: Math.min(1, Math.max(0, trauma.left_cut - 350) / 500), blood: Math.min(1, Math.max(0, trauma.left_cut - 350) / 400) * bleed }); - shading.set("rightCheek", { bruise: cheek * 0.85, swell: Math.min(1.4, trauma.swelling / 800) * graphic, cut: Math.min(1, Math.max(0, trauma.right_cut - 350) / 500), blood: Math.min(1, Math.max(0, trauma.right_cut - 350) / 400) * bleed }); + const cheekSwell = Math.min(CHEEK_SWELL, trauma.swelling / 520) * graphic; + const cheekCut = (cut: number): number => Math.min(1, Math.max(0, cut - 350) / (CHEEK_CUT_OPEN - 350)); + shading.set("leftCheek", { bruise: cheek * 0.9, swell: cheekSwell, cut: cheekCut(trauma.left_cut), blood: Math.min(1, Math.max(0, trauma.left_cut - 350) / 400) * bleed }); + shading.set("rightCheek", { bruise: cheek * 0.85, swell: cheekSwell, cut: cheekCut(trauma.right_cut), blood: Math.min(1, Math.max(0, trauma.right_cut - 350) / 400) * bleed }); shading.set("nose", { bruise: Math.min(1, trauma.head / 800), swell: Math.min(0.8, trauma.head / 1400) * graphic, blood: Math.min(1.4, (Math.max(0, trauma.head - 160) / 520 + trauma.bleeding / 420) * bleed) }); shading.set("mouth", { bruise: Math.min(0.8, trauma.head / 1100), cut: Math.min(1, Math.max(0, trauma.head - 420) / 700), blood: Math.min(1.2, (Math.max(0, trauma.head - 280) / 650 + trauma.bleeding / 500) * bleed) }); shading.set("chin", { bruise: Math.min(0.7, trauma.head / 1300) }); @@ -302,7 +608,12 @@ export function applyHeadTrauma(shading: InjuryShading, trauma: TraumaSnapshot, shading.setNoseShift(noseSide * Math.min(1.1, Math.max(0, trauma.head - 520) / 450) * graphic); shading.set("leftJaw", { bruise: Math.min(0.9, trauma.left_eye / 900 + trauma.head / 1600) }); shading.set("rightJaw", { bruise: Math.min(0.9, trauma.right_eye / 900 + trauma.head / 1600) }); - shading.set("forehead", { bruise: Math.min(0.6, trauma.swelling / 1200), cut: Math.min(1, Math.max(0, trauma.swelling - 520) / 480), blood: Math.min(1.2, Math.max(0, trauma.swelling - 520) / 420) * bleed }); + shading.set("forehead", { bruise: Math.min(0.6, trauma.swelling / 1200), cut: Math.min(1, Math.max(0, trauma.swelling - FOREHEAD_SPLITS) / 400), blood: Math.min(1.2, Math.max(0, trauma.swelling - FOREHEAD_SPLITS) / 360) * bleed }); +} + +/** How far a fighter's own blood has run down into the front of the trunks, 0 to 1. */ +export function trunksBloodFor(trauma: TraumaSnapshot, blood: BloodLevel): number { + return Math.min(1, (Math.max(0, trauma.bleeding - 40) / 420 + Math.max(0, trauma.left_cut + trauma.right_cut - 150) / 1300) * bloodScale(blood)); } /** Maps authoritative trauma onto torso injury sites. Head bleeding smears down the neck and chest. */ diff --git a/web/hands/src/render/looks.test.ts b/web/hands/src/render/looks.test.ts new file mode 100644 index 00000000..42002dcd --- /dev/null +++ b/web/hands/src/render/looks.test.ts @@ -0,0 +1,352 @@ +import * as THREE from "three"; +import { describe, expect, it } from "vitest"; +import { Effects3D } from "./effects"; +import { SkinnedBoxer, loadBoxerGlb } from "./graph"; +import { LOOK_GROOM_GLSL, LOOK_MASKS_GLSL, LOOK_SHAPE_GLSL, LookShading, OFFICIAL_LOOKS, SCANNED_LOOK, lookFor, lookShape, type FighterLook } from "./looks"; +import { REFEREE_OUTFIT } from "./outfit"; +import { aboveNeckCut, bakeSkinnedPart, measureBurstStump } from "./renderer"; + +const gltf = await loadBoxerGlb(); + +const SHADER = { + vertexShader: "#include \n#include \n#include ", + fragmentShader: "#include \n#include ", +}; + +function compile(material: THREE.Material): { uniforms: Record; vertexShader: string; fragmentShader: string } { + const shader = { uniforms: {} as Record, ...SHADER }; + material.onBeforeCompile(shader as unknown as THREE.WebGLProgramParametersWithUniforms, null as unknown as THREE.WebGLRenderer); + return shader; +} + +describe("a player's look", () => { + it("is the same every time for the same player", () => { + expect(lookFor("123456789012345678")).toEqual(lookFor("123456789012345678")); + }); + + it("stays within what the face can take", () => { + for (let player = 0; player < 500; player += 1) { + const look = lookFor(String(200000000000000000 + player * 104729)); + for (const value of [look.jaw, look.nose, look.brow, look.skull]) { + expect(value).toBeGreaterThanOrEqual(-1); + expect(value).toBeLessThanOrEqual(1); + } + expect([0, 1]).toContain(look.shaved); + expect(look.beard === 0 || (look.beard >= 0.35 && look.beard <= 1)).toBe(true); + } + }); + + it("differs from player to player", () => { + const looks = new Set(); + const tints = new Set(); + const hair = new Set(); + let shaved = 0; + let bearded = 0; + const players = 400; + for (let player = 0; player < players; player += 1) { + const look = lookFor(String(300000000000000000 + player * 7919)); + looks.add(JSON.stringify(look)); + tints.add(look.tint); + hair.add(look.hair); + shaved += look.shaved; + bearded += look.beard > 0 ? 1 : 0; + } + expect(looks.size).toBe(players); + expect(tints.size).toBeGreaterThanOrEqual(6); + expect(hair.size).toBeGreaterThanOrEqual(7); + expect(shaved / players).toBeGreaterThan(0.15); + expect(shaved / players).toBeLessThan(0.45); + expect(bearded / players).toBeGreaterThan(0.35); + expect(bearded / players).toBeLessThan(0.75); + }); +}); + +describe("look shading", () => { + const look: FighterLook = { tint: 0xa38a7c, hair: 0x6e3420, shaved: 1, beard: 0.6, jaw: 0.5, nose: -0.25, brow: 0.75, skull: -1 }; + + it("keeps a shader patch that was already on the material and runs before it", () => { + const material = new THREE.MeshStandardMaterial(); + material.onBeforeCompile = (shader) => { + shader.fragmentShader = shader.fragmentShader.replace("#include ", "#include \n// earlier patch"); + shader.vertexShader = shader.vertexShader.replace("#include ", "#include \n// earlier vertex patch"); + }; + material.customProgramCacheKey = () => "earlier"; + new LookShading(material).set(look); + const shader = compile(material); + expect(shader.fragmentShader.indexOf("lookHairZone(p)")).toBeGreaterThan(0); + expect(shader.fragmentShader.indexOf("lookHairZone(p)")).toBeLessThan(shader.fragmentShader.indexOf("// earlier patch")); + expect(shader.vertexShader.indexOf("// earlier vertex patch")).toBeLessThan(shader.vertexShader.indexOf("uLookFace.x")); + expect(material.customProgramCacheKey()).toBe("earlier-look-live"); + }); + + it("carries the look in its uniforms and the skin tone in the material colour", () => { + const material = new THREE.MeshStandardMaterial(); + const shading = new LookShading(material); + shading.set(look); + const { uniforms } = compile(material); + expect((uniforms.uLookHair!.value as THREE.Color).getHex()).toBe(0x6e3420); + expect((uniforms.uLookFace!.value as THREE.Vector4).toArray()).toEqual([0.5, -0.25, 0.75, -1]); + expect((uniforms.uLookGroom!.value as THREE.Vector2).toArray()).toEqual([1, 0.6]); + expect(material.color.getHex()).toBe(0xa38a7c); + expect(shading.look).toBe(look); + }); + + it("reads bind positions from an attribute and leaves the shape alone on a baked part", () => { + const material = new THREE.MeshStandardMaterial(); + new LookShading(material, true); + const shader = compile(material); + expect(shader.vertexShader).toContain("attribute vec3 bindPosition;"); + expect(shader.vertexShader).toContain("vLookPos = bindPosition;"); + expect(shader.vertexShader).not.toContain("uLookFace.x"); + expect(material.customProgramCacheKey()).toBe("plain-look-baked"); + }); +}); + +/** + * Runs the groom's own shader text for one texel, a colour channel at a time: each colour is a vec3 whose + * arithmetic gives that channel, while positions are only read by component or measured whole. + */ +function groomed(texel: number, tint: number, look: FighterLook, at: THREE.Vector3): THREE.Color { + const js = (glsl: string): string => glsl + .replace(/^\s*(uniform|varying|attribute)\b.*$/gm, "") + .replace(/^#.*$/gm, "") + .replace(/\bfloat (\w+)\(vec3 (\w+)\)/g, "function $1($2)") + .replace(/\b(float|vec3) (?=\w+\s*=)/g, "let "); + const run = new Function("channel", "colours", ` + const smoothstep = (a, b, x) => { const t = Math.min(1, Math.max(0, (x - a) / (b - a))); return t * t * (3 - 2 * t); }; + const clamp = (x, a, b) => Math.min(b, Math.max(a, x)); + const max = Math.max; + const mix = (a, b, t) => +a + (+b - +a) * t; + const abs = Math.abs; + const sin = Math.sin; + const vec3 = (x, y, z) => ({ x, y, z, valueOf: () => [x, y, z][channel] }); + const dot = (a, b) => a.x * b.x + a.y * b.y + a.z * b.z; + const length = (v) => Math.hypot(v.x, v.y, v.z); + const colour = (c) => vec3(c.r, c.g, c.b); + const uLookHair = colour(colours.hair); + const diffuse = colour(colours.tint); + const uLookGroom = colours.groom; + const vLookPos = colours.at; + const sampledDiffuseColor = { rgb: colour(colours.texel) }; + const diffuseColor = { rgb: vec3(colours.tint.r * colours.texel.r, colours.tint.g * colours.texel.g, colours.tint.b * colours.texel.b) }; + ${js(LOOK_MASKS_GLSL)} + ${js(LOOK_GROOM_GLSL)} + return +diffuseColor.rgb; + `) as (channel: number, colours: Record) => number; + const colours = { + hair: new THREE.Color(look.hair), tint: new THREE.Color(tint), texel: new THREE.Color(texel), + groom: { x: look.shaved, y: look.beard }, at: { x: at.x, y: at.y, z: at.z }, + }; + return new THREE.Color(run(0, colours), run(1, colours), run(2, colours)); +} + +describe("grooming on every skin tone", () => { + const darkest = 0x6f5548; + // A dark texel of the scanned hair on the crown, and one of the skin along the jaw under a beard. + const crown = new THREE.Vector3(0, 127.6, -3); + const jaw = new THREE.Vector3(4.6, 112.8, 1.4); + const hairTexel = 0x2a1d16; + const skinTexel = 0xd7a184; + const close = (a: THREE.Color, b: THREE.Color, within: number): void => { + for (const channel of ["r", "g", "b"] as const) expect(Math.abs(a[channel] - b[channel])).toBeLessThanOrEqual(within * Math.max(a[channel], b[channel]) + 1e-6); + }; + + it("dyes the hair the same colour on the darkest skin as on the scanned man", () => { + for (const hair of [0x8c8a86, 0xa88c58]) { + const look: FighterLook = { ...SCANNED_LOOK, hair, shaved: 0, beard: 0 }; + close(groomed(hairTexel, darkest, look, crown), groomed(hairTexel, 0xffffff, look, crown), 0.05); + } + }); + + it("grows a beard of the hair's colour, with the skin's own tone showing through", () => { + const look: FighterLook = { ...SCANNED_LOOK, hair: 0xa88c58, shaved: 0, beard: 1 }; + // Over a black texel only the beard itself shows: most of it is the dye, whatever the skin. + const pale = groomed(0x000000, 0xffffff, look, jaw); + const dark = groomed(0x000000, darkest, look, jaw); + expect(pale.r).toBeGreaterThan(0.05); + for (const channel of ["r", "g", "b"] as const) expect(dark[channel]).toBeGreaterThan(0.7 * pale[channel]); + // Without one, the jaw is just the tinted skin. + const bare = groomed(skinTexel, darkest, { ...look, beard: 0 }, jaw); + close(bare, new THREE.Color(skinTexel).multiply(new THREE.Color(darkest)), 0.001); + }); + + it("tints a shaved scalp with the skin", () => { + const look: FighterLook = { ...SCANNED_LOOK, hair: 0x8c8a86, shaved: 1, beard: 0 }; + const pale = groomed(hairTexel, 0xffffff, look, crown); + const dark = groomed(hairTexel, darkest, look, crown); + const tint = new THREE.Color(darkest); + close(dark, new THREE.Color(pale.r * tint.r, pale.g * tint.g, pale.b * tint.b), 0.001); + }); +}); + +describe("a fighter wearing a look", () => { + it("takes the skin tone on the head and the body and can change it", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + expect(boxer.look).toBe(SCANNED_LOOK); + const look = lookFor("987654321098765432"); + boxer.setLook(look); + expect(boxer.look).toBe(look); + expect(boxer.skin.color.getHex()).toBe(look.tint); + expect((boxer.headMesh.material as THREE.MeshStandardMaterial).color.getHex()).toBe(look.tint); + boxer.root.traverse((object) => { + if (object instanceof THREE.SkinnedMesh && object.name === "BoxerBody") expect((object.material as THREE.MeshStandardMaterial).color.getHex()).toBe(look.tint); + }); + boxer.dispose(); + }); + + it("keeps an official's hands and cuffs out of the shadow map, where they are too small to show", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xc79b76, gear: 0x1b2230, look: OFFICIAL_LOOKS.referee, outfit: REFEREE_OUTFIT }); + const parts: THREE.Mesh[] = []; + boxer.root.traverse((object) => { if (object instanceof THREE.Mesh && /^(hand|cuff)-/u.test(object.name)) parts.push(object); }); + expect(parts).toHaveLength(4); + for (const part of parts) expect(part.castShadow).toBe(false); + boxer.dispose(); + }); + + it("gives an official bare hands in the official's skin tone", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xc79b76, gear: 0x1b2230, look: OFFICIAL_LOOKS.blueCorner, outfit: REFEREE_OUTFIT }); + const hand = boxer.rig.bones.gloveL.getObjectByName("hand-left") as THREE.Mesh; + const shown = (hand.material as THREE.MeshStandardMaterial).color; + const dark = shown.r + shown.g + shown.b; + boxer.setLook(OFFICIAL_LOOKS.redCorner); + expect(shown.r + shown.g + shown.b).toBeGreaterThan(dark * 1.5); + boxer.dispose(); + }); + + it("keeps its hair and beard on a severed head", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8, look: lookFor("dave") }); + boxer.root.updateMatrixWorld(true); + const pivot = boxer.bone("head")!; + const baked = { ...bakeSkinnedPart(boxer.headMesh, pivot.getWorldPosition(new THREE.Vector3()), pivot.getWorldQuaternion(new THREE.Quaternion()), aboveNeckCut), look: boxer.look }; + const bind = baked.geometry.getAttribute("bindPosition"); + const source = boxer.headMesh.geometry.getAttribute("position"); + expect(bind.count).toBe(source.count); + expect(bind.getY(100)).toBe(source.getY(100)); + expect(bind.array).not.toBe(source.array); + const scene = new THREE.Scene(); + const effects = new Effects3D(scene); + effects.decapitate(0, new THREE.Vector3(0, 1.5, 0), new THREE.Quaternion(), 1, 12, 0xb0703f, baked); + const head = scene.children.find((child): child is THREE.Mesh => child instanceof THREE.Mesh && child.geometry === baked.geometry)!; + const material = head.material as THREE.MeshStandardMaterial; + expect(material.color.getHex()).toBe(lookFor("dave").tint); + const { uniforms } = compile(material); + expect((uniforms.uLookHair!.value as THREE.Color).getHex()).toBe(lookFor("dave").hair); + expect((uniforms.uLookGroom!.value as THREE.Vector2).x).toBe(lookFor("dave").shaved); + effects.dispose(); + boxer.dispose(); + }); +}); + +/** Runs the shader's own text in JavaScript: its scalar GLSL is valid JavaScript once the types are dropped. */ +function shaderShape(): (position: THREE.Vector3, look: FighterLook) => { x: number; y: number; z: number } { + const js = (glsl: string): string => glsl + .replace(/^\s*(uniform|varying|attribute)\b.*$/gm, "") + .replace(/\bfloat (\w+)\(vec3 (\w+)\)/g, "function $1($2)") + .replace(/\b(float|vec3) (?=\w+\s*=)/g, "let "); + const run = new Function("position", "uLookFace", "uLookGroom", ` + const smoothstep = (a, b, x) => { const t = Math.min(1, Math.max(0, (x - a) / (b - a))); return t * t * (3 - 2 * t); }; + const mix = (a, b, t) => a + (b - a) * t; + const abs = Math.abs; + const vec3 = (x, y, z) => ({ x, y, z }); + const distance = (p, q) => Math.hypot(p.x - q.x, p.y - q.y, p.z - q.z); + const normalize = (v) => { const l = Math.hypot(v.x, v.y, v.z); return { x: v.x / l, y: v.y / l, z: v.z / l }; }; + let vLookPos; + ${js(LOOK_MASKS_GLSL)} + const transformed = { x: position.x, y: position.y, z: position.z }; + ${js(LOOK_SHAPE_GLSL)} + return transformed; + `) as (position: THREE.Vector3, face: { x: number; y: number; z: number; w: number }, groom: { x: number; y: number }) => { x: number; y: number; z: number }; + return (position, look) => run(position, { x: look.jaw, y: look.nose, z: look.brow, w: look.skull }, { x: look.shaved, y: look.beard }); +} + +describe("a head's shape off the GPU", () => { + const looks: FighterLook[] = [SCANNED_LOOK, lookFor("dave"), lookFor("erin"), ...Object.values(OFFICIAL_LOOKS), { ...SCANNED_LOOK, shaved: 1, jaw: 1, nose: -1, brow: 1, skull: -1 }]; + + it("is the shader's own reshaping, for every look and all over the head", () => { + const shader = shaderShape(); + const point = new THREE.Vector3(); + const out = new THREE.Vector3(); + let checked = 0; + for (const look of looks) { + for (let y = 104; y <= 128; y += 1.5) { + for (let x = -8; x <= 8; x += 2) { + for (let z = -8; z <= 7; z += 2.5) { + const expected = shader(point.set(x, y, z), look); + lookShape(point, look, out); + expect(out.x).toBeCloseTo(expected.x, 9); + expect(out.y).toBeCloseTo(expected.y, 9); + expect(out.z).toBeCloseTo(expected.z, 9); + checked += 1; + } + } + } + } + expect(checked).toBeGreaterThan(5000); + }); + + it("leaves the scanned man's head as it is", () => { + const out = new THREE.Vector3(); + expect(lookShape(new THREE.Vector3(4, 111, 2), SCANNED_LOOK, out).toArray()).toEqual([4, 111, 2]); + }); + + it("keeps the owner's jaw, nose, brow and skull on a severed head", () => { + const look = { ...lookFor("dave"), jaw: 1, nose: 1, brow: 1, skull: 1, shaved: 1 }; + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8, look }); + boxer.root.updateMatrixWorld(true); + const pivot = boxer.bone("head")!; + const at = pivot.getWorldPosition(new THREE.Vector3()); + const turn = pivot.getWorldQuaternion(new THREE.Quaternion()); + const shaped = bakeSkinnedPart(boxer.headMesh, at, turn, aboveNeckCut, look); + const plain = bakeSkinnedPart(boxer.headMesh, at, turn, aboveNeckCut); + const bind = boxer.headMesh.geometry.getAttribute("position"); + const inverse = turn.clone().invert(); + const expected = new THREE.Vector3(); + const baked = new THREE.Vector3(); + let moved = 0; + for (let vertex = 0; vertex < bind.count; vertex += 211) { + lookShape(expected.fromBufferAttribute(bind, vertex), look, expected); + boxer.headMesh.applyBoneTransform(vertex, expected).applyMatrix4(boxer.headMesh.matrixWorld).sub(at).applyQuaternion(inverse); + baked.fromBufferAttribute(shaped.geometry.getAttribute("position"), vertex); + expect(baked.distanceTo(expected)).toBeLessThan(1e-6); + if (baked.distanceTo(new THREE.Vector3().fromBufferAttribute(plain.geometry.getAttribute("position"), vertex)) > 1e-4) moved += 1; + } + expect(moved).toBeGreaterThan(3); + for (const part of [shaped, plain]) part.geometry.dispose(); + boxer.dispose(); + }); +}); + +describe("wounds on a reshaped head", () => { + it("are measured on the head as it is drawn, reshaped by the fighter's look", () => { + const rimFor = (look: FighterLook): Float32Array => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8, look }); + const out = { position: new THREE.Vector3(), quaternion: new THREE.Quaternion(), across: new THREE.Vector3(), scratch: new THREE.Vector3() }; + const rim = measureBurstStump(boxer, new Float32Array(0), out)!; + boxer.dispose(); + return rim; + }; + const plain = rimFor(SCANNED_LOOK); + const wide = rimFor({ ...SCANNED_LOOK, jaw: 1 }); + expect(wide.length).toBe(plain.length); + let moved = 0; + for (let index = 0; index < plain.length; index += 1) if (Math.abs(wide[index]! - plain[index]!) > 1e-4) moved += 1; + expect(moved).toBeGreaterThan(plain.length / 6); + }); +}); + +describe("a severed glove", () => { + it("keeps the blood that was on it and loses it when the hand is put back", () => { + const scene = new THREE.Scene(); + const effects = new Effects3D(scene); + const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1); + geometry.setAttribute("bindPosition", geometry.getAttribute("position").clone()); + effects.dismemberHand(0, "left", new THREE.Vector3(0, 1.2, 0), new THREE.Quaternion(), 1, 21, 0x1d4ed8, { geometry, map: null, color: 0x1d4ed8, gloveBlood: 0.7 }); + const hand = scene.children.find((child): child is THREE.Mesh => child instanceof THREE.Mesh && child.geometry === geometry)!; + const blood = () => compile(hand.material as THREE.Material).uniforms.uGearBlood!.value; + expect(blood()).toBeCloseTo(0.7, 6); + effects.restoreFighter(0); + effects.dismemberHand(0, "left", new THREE.Vector3(0, 1.2, 0), new THREE.Quaternion(), 1, 22, 0x1d4ed8); + expect(blood()).toBe(0); + effects.dispose(); + }); +}); diff --git a/web/hands/src/render/looks.ts b/web/hands/src/render/looks.ts new file mode 100644 index 00000000..476a8bbe --- /dev/null +++ b/web/hands/src/render/looks.ts @@ -0,0 +1,213 @@ +import * as THREE from "three"; + +/** + * What makes one fighter look unlike another. Every character is the same scanned man, so the look + * recolours his hair, shaves it or grows a beard, reshapes the face a little and tints the skin. + * A player's look comes from their id, so they are the same fighter in every bout and on every screen. + */ +export interface FighterLook { + /** Multiplies the skin textures; white is the scanned tone. */ + readonly tint: number; + readonly hair: number; + /** 0 keeps the hair, 1 shaves the head. */ + readonly shaved: number; + /** 0 clean shaven, 1 a full beard. */ + readonly beard: number; + /** Each -1..1. */ + readonly jaw: number; + readonly nose: number; + readonly brow: number; + readonly skull: number; +} + +export const SCANNED_LOOK: FighterLook = { tint: 0xffffff, hair: 0x4a3323, shaved: 0, beard: 0, jaw: 0, nose: 0, brow: 0, skull: 0 }; + +export const OFFICIAL_LOOKS = { + referee: { tint: 0xf2dccb, hair: 0x8c8a86, shaved: 0, beard: 0, jaw: -0.4, nose: 0.3, brow: -0.2, skull: 0.2 }, + blueCorner: { tint: 0xa98468, hair: 0x15100c, shaved: 1, beard: 0.8, jaw: 0.7, nose: 0.5, brow: 0.6, skull: 0.4 }, + redCorner: { tint: 0xffeedd, hair: 0x7a2f1c, shaved: 0, beard: 0.6, jaw: 0.2, nose: -0.5, brow: 0.1, skull: -0.3 }, + blueCutman: { tint: 0xd8b498, hair: 0x221710, shaved: 0, beard: 0, jaw: -0.6, nose: 0.8, brow: -0.5, skull: -0.5 }, + redCutman: { tint: 0x8a6650, hair: 0x15100c, shaved: 1, beard: 0.45, jaw: 0.5, nose: 0.2, brow: 0.8, skull: 0.6 }, +} as const satisfies Record; + +const TINTS = [0xffffff, 0xf4dccb, 0xd9b699, 0xbf9678, 0xa38a7c, 0x8a6c5c, 0x6f5548]; +const HAIR = [0x15100c, 0x221710, 0x3a2416, 0x5a3a20, 0x8a6a3c, 0xa88c58, 0x6e3420, 0x8c8a86]; + +function hash(text: string): number { + let value = 0x811c9dc5; + for (let index = 0; index < text.length; index += 1) { + value ^= text.charCodeAt(index); + value = Math.imul(value, 0x01000193); + } + return value >>> 0; +} + +function seeded(seed: number): () => number { + return () => { + seed = Math.imul(seed ^ (seed >>> 15), 1 | seed); + seed ^= seed + Math.imul(seed ^ (seed >>> 7), 61 | seed); + return ((seed ^ (seed >>> 14)) >>> 0) / 4294967296; + }; +} + +export function lookFor(id: string): FighterLook { + const rand = seeded(hash(id) || 1); + const pick = (from: readonly T[]): T => from[Math.floor(rand() * from.length)]!; + const spread = (): number => Math.round((rand() * 2 - 1) * 100) / 100; + const tint = pick(TINTS); + const hair = pick(HAIR); + const shaved = rand() < 0.28 ? 1 : 0; + const beard = rand() < 0.45 ? 0 : Math.round((0.35 + rand() * 0.65) * 100) / 100; + return { tint, hair, shaved, beard, jaw: spread(), nose: spread(), brow: spread(), skull: spread() }; +} + +/** The scanned face in linear light: what a shaved scalp is painted with. */ +const SKIN = "vec3(0.84, 0.39, 0.175)"; + +/** Shared with the CPU port below; exported so a test can check the two agree. */ +export const LOOK_MASKS_GLSL = /* glsl */ ` +uniform vec3 uLookHair; +uniform vec4 uLookFace; +uniform vec2 uLookGroom; +varying vec3 vLookPos; +/** Where the scanned man has hair: above a hairline that runs from the forehead past the ear to the nape. */ +float lookHairZone(vec3 p) { + float behind = mix(115.3, 120.6, smoothstep(-6.8, -4.6, p.z)); + float line = mix(behind, 125.5, smoothstep(-4.6, 1.6, p.z)); + float nape = smoothstep(116.0, 121.0, p.y); + float reach = mix(3.8, 9.0, nape); + return smoothstep(line - 0.5, line + 0.5, p.y) * (1.0 - smoothstep(reach, reach + 1.0, abs(p.x)) * (1.0 - nape)); +} +`; + +export const LOOK_SHAPE_GLSL = /* glsl */ ` +{ + vec3 p = position; + vLookPos = p; + float jaw = (1.0 - smoothstep(113.5, 118.0, p.y)) * smoothstep(108.5, 112.0, p.y) * smoothstep(-7.0, -2.0, p.z); + transformed.x += p.x * 0.12 * uLookFace.x * jaw; + float nose = 1.0 - smoothstep(0.0, 2.6, distance(p, vec3(0.0, 117.2, 5.0))); + transformed.x += p.x * 0.4 * uLookFace.y * nose; + transformed.z += 0.5 * uLookFace.y * nose; + float brow = (1.0 - smoothstep(0.0, 2.2, abs(p.y - 123.2))) * smoothstep(1.5, 3.5, p.z); + transformed.z += 0.55 * uLookFace.z * brow; + transformed.x += p.x * 0.06 * uLookFace.w * smoothstep(112.0, 116.0, p.y); + // A shaved head loses the thickness of the scanned hair. + float crown = lookHairZone(p) * uLookGroom.x; + float fringe = smoothstep(-1.0, 3.0, p.z) * smoothstep(123.5, 126.0, p.y); + float crownDepth = mix(0.9, 1.9, fringe) * crown; + vec3 crownAway = normalize(vec3(p.x, (p.y - 120.0) * 1.1, p.z + 2.5)); + transformed.x -= crownAway.x * crownDepth; + transformed.y -= crownAway.y * crownDepth; + transformed.z -= crownAway.z * crownDepth; +} +`; + +const smooth = (edge0: number, edge1: number, x: number): number => { + const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0))); + return t * t * (3 - 2 * t); +}; +const mix = (a: number, b: number, t: number): number => a + (b - a) * t; + +/** `lookHairZone` from the shader, for a bind-space point. */ +function hairZone(x: number, y: number, z: number): number { + const behind = mix(115.3, 120.6, smooth(-6.8, -4.6, z)); + const line = mix(behind, 125.5, smooth(-4.6, 1.6, z)); + const nape = smooth(116.0, 121.0, y); + const reach = mix(3.8, 9.0, nape); + return smooth(line - 0.5, line + 0.5, y) * (1 - smooth(reach, reach + 1, Math.abs(x)) * (1 - nape)); +} + +/** + * The look's reshaping of a bind-space head vertex, as LOOK_SHAPE_GLSL does it on the GPU. The CPU poses + * the head itself for a severed head and for the rims of its wounds, and they must match what is drawn. + */ +export function lookShape(bind: THREE.Vector3, look: FighterLook, out: THREE.Vector3): THREE.Vector3 { + const { x, y, z } = bind; + const jaw = (1 - smooth(113.5, 118.0, y)) * smooth(108.5, 112.0, y) * smooth(-7.0, -2.0, z); + const nose = 1 - smooth(0, 2.6, Math.hypot(x, y - 117.2, z - 5.0)); + const brow = (1 - smooth(0, 2.2, Math.abs(y - 123.2))) * smooth(1.5, 3.5, z); + let dx = x * 0.12 * look.jaw * jaw + x * 0.4 * look.nose * nose + x * 0.06 * look.skull * smooth(112.0, 116.0, y); + let dy = 0; + let dz = 0.5 * look.nose * nose + 0.55 * look.brow * brow; + const crownDepth = mix(0.9, 1.9, smooth(-1.0, 3.0, z) * smooth(123.5, 126.0, y)) * hairZone(x, y, z) * look.shaved; + const ax = x; + const ay = (y - 120) * 1.1; + const az = z + 2.5; + const length = Math.hypot(ax, ay, az) || 1; + dx -= (ax / length) * crownDepth; + dy -= (ay / length) * crownDepth; + dz -= (az / length) * crownDepth; + return out.set(x + dx, y + dy, z + dz); +} + +const VERTEX_BAKED = /* glsl */ ` +vLookPos = bindPosition; +`; + +/** + * Dyes the hair, shaves the scalp and grows the beard over the scanned texture. `diffuse` is the material + * colour, the fighter's skin tone: it tints the shaved scalp and the skin showing through a beard, never the + * dye, so a dark-skinned fighter's grey or blond hair stays grey or blond. Exported so a test can read it. + */ +export const LOOK_GROOM_GLSL = /* glsl */ ` +#ifdef USE_MAP +{ + vec3 p = vLookPos; + float scanned = dot(sampledDiffuseColor.rgb, vec3(0.2126, 0.7152, 0.0722)); + float hair = lookHairZone(p) * max(1.0 - smoothstep(0.13, 0.26, scanned), smoothstep(126.2, 127.4, p.y)); + vec3 dyed = uLookHair * clamp(scanned / 0.075, 0.45, 1.7); + vec3 scalp = mix(${SKIN}, uLookHair, 0.1) * clamp(scanned / 0.075, 0.85, 1.12) * 0.9; + diffuseColor.rgb = mix(diffuseColor.rgb, mix(dyed, scalp * diffuse, uLookGroom.x), hair); + // The beard covers the chin and runs back along the jaw to the ear, clear of the neck beneath it. + float front = mix(1.4, -4.8, smoothstep(111.2, 114.6, p.y)); + float jaw = (1.0 - smoothstep(115.4, 116.8, p.y)) * smoothstep(109.6, 110.6, p.y) * smoothstep(front - 0.6, front + 0.6, p.z); + float lips = 1.0 - smoothstep(0.8, 2.0, length(vec3(p.x * 0.55, p.y - 114.3, (p.z - 4.8) * 0.4))); + float grain = 0.8 + 0.2 * sin(p.x * 41.0 + p.z * 13.0) * sin(p.y * 47.0 + p.z * 29.0); + diffuseColor.rgb = mix(diffuseColor.rgb, mix(uLookHair * 0.8, ${SKIN} * diffuse, 0.12), jaw * (1.0 - lips) * uLookGroom.y * grain * 0.88); +} +#endif +`; + +export class LookShading { + private readonly uniforms = { + uLookHair: { value: new THREE.Color(SCANNED_LOOK.hair) }, + uLookFace: { value: new THREE.Vector4() }, + uLookGroom: { value: new THREE.Vector2() }, + }; + private current: FighterLook = SCANNED_LOOK; + + /** + * Composes with a shader patch already on the material. A `baked` material draws geometry that is + * no longer in bind space, which must carry its bind positions in a `bindPosition` attribute. + */ + constructor(private readonly material: THREE.MeshStandardMaterial, baked = false) { + const uniforms = this.uniforms; + const before = material.onBeforeCompile; + const beforeKey = Object.hasOwn(material, "customProgramCacheKey") ? material.customProgramCacheKey() : "plain"; + material.onBeforeCompile = (shader, renderer) => { + before.call(material, shader, renderer); + Object.assign(shader.uniforms, uniforms); + shader.vertexShader = shader.vertexShader + .replace("#include ", `#include \n${baked ? "attribute vec3 bindPosition;" : ""}\n${LOOK_MASKS_GLSL}`) + .replace("#include ", `${baked ? VERTEX_BAKED : LOOK_SHAPE_GLSL}\n#include `); + shader.fragmentShader = shader.fragmentShader + .replace("#include ", `#include \n${LOOK_MASKS_GLSL}`) + .replace("#include ", `#include \n${LOOK_GROOM_GLSL}`); + }; + material.customProgramCacheKey = () => `${beforeKey}-look-${baked ? "baked" : "live"}`; + material.needsUpdate = true; + } + + set(look: FighterLook): void { + this.current = look; + this.material.color.setHex(look.tint); + this.uniforms.uLookHair.value.setHex(look.hair); + this.uniforms.uLookFace.value.set(look.jaw, look.nose, look.brow, look.skull); + this.uniforms.uLookGroom.value.set(look.shaved, look.beard); + } + + get look(): FighterLook { + return this.current; + } +} diff --git a/web/hands/src/render/mouthpiece.test.ts b/web/hands/src/render/mouthpiece.test.ts new file mode 100644 index 00000000..7fd1bd32 --- /dev/null +++ b/web/hands/src/render/mouthpiece.test.ts @@ -0,0 +1,161 @@ +import * as THREE from "three"; +import { describe, expect, it } from "vitest"; +import { snapshot } from "../test/fixtures"; +import { Effects3D } from "./effects"; +import { BIG_SHOT } from "./gore"; +import { buildMouthpieceGeometry, idleShield, mouthpieceFlies, stepShield } from "./mouthpiece"; +import { FightRenderer } from "./renderer"; +import { CANVAS_TOP, ROPE_LINE } from "./world"; + +const mouth = new THREE.Vector3(0.1, 1.5, 0.05); + +describe("gum shield", () => { + it("is a horseshoe about five centimetres across, four deep and under two tall, its front toward +Z", () => { + const geometry = buildMouthpieceGeometry(); + geometry.computeBoundingBox(); + const box = geometry.boundingBox!; + expect(box.max.x - box.min.x).toBeGreaterThan(0.05); + expect(box.max.x - box.min.x).toBeLessThan(0.065); + expect(box.max.z - box.min.z).toBeGreaterThan(0.035); + expect(box.max.z - box.min.z).toBeLessThan(0.05); + expect(box.max.y - box.min.y).toBeLessThan(0.02); + // The arch closes at the front: nothing at the front corners, where a box would have them. + const position = geometry.getAttribute("position"); + const vertex = new THREE.Vector3(); + for (let index = 0; index < position.count; index += 1) { + vertex.fromBufferAttribute(position, index); + expect(Math.abs(vertex.x) > 0.02 && vertex.z > box.max.z - 0.01).toBe(false); + } + geometry.dispose(); + }); + + it("flies from the punch that floors a man and from big counters and huge shots to the head", () => { + expect(mouthpieceFlies("knockdown", 1, true)).toBe(true); + expect(mouthpieceFlies("knockdown", 1, false)).toBe(false); + expect(mouthpieceFlies("counter_hit", BIG_SHOT, true)).toBe(true); + expect(mouthpieceFlies("counter_hit", BIG_SHOT - 1, true)).toBe(false); + expect(mouthpieceFlies("hit", 110, true)).toBe(true); + expect(mouthpieceFlies("hit", 109, true)).toBe(false); + expect(mouthpieceFlies("counter_hit", 150, false)).toBe(false); + expect(mouthpieceFlies("block", 150, true)).toBe(false); + }); + + it("falls, bounces lower each time, slides to a stop and lies flat on the canvas", () => { + const state = { ...idleShield(), out: true, moving: true, x: 0, y: 1.5, z: 0, vx: 1.2, vy: 1.2, vz: 0, wx: 9, wy: 4, wz: 7, restYaw: 0.4 }; + const quaternion = new THREE.Quaternion(); + const peaks: number[] = []; + let rising = true; + let last = state.y; + for (let step = 0; step < 600 && state.moving; step += 1) { + stepShield(state, quaternion, 1 / 60, CANVAS_TOP); + if (rising && state.y < last) peaks.push(last); + rising = state.y > last; + last = state.y; + expect(state.y).toBeGreaterThanOrEqual(CANVAS_TOP); + } + expect(state.moving).toBe(false); + expect(state.bounces).toBeGreaterThan(0); + for (let index = 2; index < peaks.length; index += 1) expect(peaks[index]!).toBeLessThan(peaks[index - 1]!); + expect(state.y).toBeCloseTo(CANVAS_TOP + 0.006, 5); + expect(state.x).toBeGreaterThan(0.5); + expect(state.x).toBeLessThan(2); + const up = new THREE.Vector3(0, 1, 0).applyQuaternion(quaternion); + expect(up.y).toBeGreaterThan(0.98); + }); + + it("stays out until the round ends, unless the replay knocks it out again", () => { + const effects = new Effects3D(new THREE.Scene()); + expect(effects.mouthpieceOut(1)).toBe(false); + expect(effects.ejectMouthpiece(1, mouth, new THREE.Quaternion(), 1, 10, 0xb91c1c)).toBe(true); + expect(effects.mouthpieceOut(1)).toBe(true); + expect(effects.mouthpieceOut(0)).toBe(false); + expect(effects.ejectMouthpiece(1, mouth, new THREE.Quaternion(), 1, 11, 0xb91c1c)).toBe(false); + expect(effects.ejectMouthpiece(1, mouth, new THREE.Quaternion(), 1, 10, 0xb91c1c, true)).toBe(false); + expect(effects.ejectMouthpiece(1, mouth, new THREE.Quaternion(), 1, 1_000_013, 0xb91c1c, true)).toBe(true); + effects.clearMouthpieces(); + expect(effects.mouthpieceOut(1)).toBe(false); + expect(effects.ejectMouthpiece(1, mouth, new THREE.Quaternion(), 1, 11, 0xb91c1c)).toBe(true); + effects.dispose(); + }); + + it("lands on the canvas inside the ropes and stays there, the same at any frame rate", () => { + const land = (frame: number): THREE.Vector3 => { + const effects = new Effects3D(new THREE.Scene()); + effects.ejectMouthpiece(0, new THREE.Vector3(ROPE_LINE - 0.2, 1.5, 0), new THREE.Quaternion(), 1, 77, 0x1d4ed8); + for (let time = 0; time < 4; time += frame) effects.update(frame); + const at = new THREE.Vector3(); + expect(effects.mouthpiecePosition(0, at)).toBe(true); + effects.dispose(); + return at; + }; + const smooth = land(1 / 60); + expect(Math.abs(smooth.x)).toBeLessThan(ROPE_LINE); + expect(smooth.y).toBeLessThan(CANVAS_TOP + 0.01); + expect(land(1 / 30).distanceTo(smooth)).toBeLessThan(1e-6); + }); + + it("sprays spit, and blood only where blood is shown", () => { + const sprayed = (blood: "full" | "off"): { all: number; blood: number } => { + const effects = new Effects3D(new THREE.Scene()); + effects.setBloodLevel(blood); + effects.ejectMouthpiece(0, mouth, new THREE.Quaternion(), -1, 4, 0x1d4ed8); + const counts = { all: effects.liveParticles, blood: effects.liveBloodParticles }; + effects.dispose(); + return counts; + }; + expect(sprayed("full").blood).toBeGreaterThan(8); + expect(sprayed("off").all).toBeGreaterThan(5); + expect(sprayed("off").blood).toBe(0); + }); + + it("leaves the mouth from where the head is, along the punch", () => { + const calls: unknown[][] = []; + const head = new THREE.Vector3(1, 1.5, -0.4); + const stub = { + mouthPoint: new THREE.Vector3(), + headWorldPose: () => ({ position: head, quaternion: new THREE.Quaternion() }), + gearColor: () => 0xb91c1c, + effects: { ejectMouthpiece: (...args: unknown[]) => calls.push(args) }, + }; + (FightRenderer.prototype as unknown as { knockOutMouthpiece(this: unknown, index: number, direction: number, eventId: number, again: boolean): void }).knockOutMouthpiece.call(stub, 1, -1, 42, false); + expect(calls).toHaveLength(1); + const [index, at, , direction, eventId, color, again] = calls[0]!; + expect(index).toBe(1); + expect((at as THREE.Vector3).distanceTo(head)).toBeLessThan(0.15); + expect((at as THREE.Vector3).y).toBeLessThan(head.y); + expect([direction, eventId, color, again]).toEqual([-1, 42, 0xb91c1c, false]); + }); + + it("goes back in when the bell ends the round", () => { + const effects = new Effects3D(new THREE.Scene()); + effects.ejectMouthpiece(0, mouth, new THREE.Quaternion(), 1, 3, 0x1d4ed8); + const stub = { restStartedAt: 0, effects }; + (FightRenderer.prototype as unknown as { enterRest(this: unknown, seconds: number): void }).enterRest.call(stub, 42); + expect(stub.restStartedAt).toBe(42); + expect(effects.mouthpieceOut(0)).toBe(false); + effects.dispose(); + }); + + it("is knocked out again, in slow motion, when the replay reaches the punch that floored him", () => { + const knocked: unknown[][] = []; + const hit = { event_id: 9, tick: 40, kind: "counter_hit", actor_id: "one", target_id: "two", amount: 120, detail: "hook:head", blood: 20, direction: -1, action_id: null }; + const stub = { + lastKnockdown: { knockdown: { ...hit, kind: "knockdown", amount: 1, detail: "" }, hit, finisher: null }, + contactPoint: new THREE.Vector3(), tmpA: new THREE.Vector3(-1, 0, 0), tmpB: new THREE.Vector3(1, 0, 0), pendingContacts: [], + mapping: { x: (value: number) => value / 100, z: (value: number) => value / 100 }, + effects: { addEvent: () => undefined }, + settings: () => ({ reducedMotion: false }), + graphs: null, + onContact: null, + reapplyReplayInjuries: () => undefined, + knockOutMouthpiece: (...args: unknown[]) => knocked.push(args), + }; + const fire = (FightRenderer.prototype as unknown as { fireReplayImpact(this: unknown, snapshot: unknown): void }).fireReplayImpact; + fire.call(stub, snapshot()); + // It flies the way the punch travelled: fighter one stands at -x and fighter two at +x. + expect(knocked).toEqual([[1, { x: 1, z: 0 }, 9 + 1_000_003, true]]); + fire.call({ ...stub, settings: () => ({ reducedMotion: true }) }, snapshot()); + fire.call({ ...stub, lastKnockdown: { ...stub.lastKnockdown, hit: { ...hit, detail: "hook:body" } } }, snapshot()); + expect(knocked).toHaveLength(1); + }); +}); diff --git a/web/hands/src/render/mouthpiece.ts b/web/hands/src/render/mouthpiece.ts new file mode 100644 index 00000000..42f44917 --- /dev/null +++ b/web/hands/src/render/mouthpiece.ts @@ -0,0 +1,169 @@ +import * as THREE from "three"; +import { BIG_SHOT } from "./gore"; + +/** Half the width of the arch across the back teeth, and how far the front of it reaches forward of them. */ +const ARCH_HALF_WIDTH = 0.026; +const ARCH_DEPTH = 0.038; +const ARCH_STEPS = 22; +/** + * The shield's cross-section in centimetres, `[outward, up]` round the channel the upper teeth sit in: + * a tall outer flange, a lower inner one and a floor between them. + */ +const PROFILE: readonly (readonly [number, number])[] = [ + [0.55, 0.85], [0.55, 0.1], [0.4, -0.12], [-0.4, -0.12], [-0.55, 0.05], [-0.55, 0.6], [-0.32, 0.62], [-0.3, 0.16], [0.3, 0.16], [0.32, 0.85], +]; +export const SHIELD_RADIUS = 0.03; + +/** + * A gum shield: a horseshoe channel round the upper teeth, the open side up and the front of the arch + * toward +Z, about 5 cm across and 4 cm deep, in metres. + */ +export function buildMouthpieceGeometry(): THREE.BufferGeometry { + const positions: number[] = []; + const index: number[] = []; + const ring = PROFILE.length; + const point = new THREE.Vector3(); + const tangent = new THREE.Vector3(); + const outward = new THREE.Vector3(); + for (let step = 0; step <= ARCH_STEPS; step += 1) { + const s = (step / ARCH_STEPS) * 2 - 1; + // The arch is a parabola: front at +Z, the back teeth at the two ends. + point.set(s * ARCH_HALF_WIDTH, 0, ARCH_DEPTH * (1 - s * s) - ARCH_DEPTH * 0.5); + tangent.set(ARCH_HALF_WIDTH, 0, -2 * ARCH_DEPTH * s).normalize(); + outward.set(tangent.z, 0, -tangent.x); + if (outward.dot(point.clone().setY(0).add(new THREE.Vector3(0, 0, ARCH_DEPTH * 0.5))) < 0) outward.negate(); + for (const [across, up] of PROFILE) { + positions.push(point.x + outward.x * across * 0.01, up * 0.01, point.z + outward.z * across * 0.01); + } + } + for (let step = 0; step < ARCH_STEPS; step += 1) { + for (let corner = 0; corner < ring; corner += 1) { + const a = step * ring + corner; + const b = step * ring + ((corner + 1) % ring); + const c = a + ring; + const d = b + ring; + index.push(a, c, b, b, c, d); + } + } + // Close both ends of the arch with a fan round the middle of the profile. + for (const step of [0, ARCH_STEPS]) { + const centre = positions.length / 3; + let x = 0, y = 0, z = 0; + for (let corner = 0; corner < ring; corner += 1) { + x += positions[(step * ring + corner) * 3]!; + y += positions[(step * ring + corner) * 3 + 1]!; + z += positions[(step * ring + corner) * 3 + 2]!; + } + positions.push(x / ring, y / ring, z / ring); + for (let corner = 0; corner < ring; corner += 1) { + const a = step * ring + corner; + const b = step * ring + ((corner + 1) % ring); + if (step === 0) index.push(centre, a, b); + else index.push(centre, b, a); + } + } + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); + geometry.setIndex(index); + geometry.computeVertexNormals(); + geometry.computeBoundingSphere(); + return geometry; +} + +/** Whether a blow knocks the gum shield out: the punch that floors a man, a big counter or a huge shot, to the head. */ +export function mouthpieceFlies(kind: string, amount: number, head: boolean): boolean { + if (!head) return false; + if (kind === "knockdown") return true; + if (kind === "counter_hit") return amount >= BIG_SHOT; + return kind === "hit" && amount >= 110; +} + +const GRAVITY = 9.81; +const RESTITUTION = 0.32; +const SLIDE_FRICTION = 4.5; +const REST_SPEED = 0.04; +const MAX_BOUNCES = 3; + +export interface ShieldState { + out: boolean; + moving: boolean; + sliding: boolean; + x: number; y: number; z: number; + vx: number; vy: number; vz: number; + /** Tumbling, in radians per second about each axis. */ + wx: number; wy: number; wz: number; + bounces: number; + /** Seconds since it left the mouth, for the trail of spit and blood behind it. */ + age: number; + /** Which way it lies once it settles. */ + restYaw: number; +} + +export function idleShield(): ShieldState { + return { out: false, moving: false, sliding: false, x: 0, y: -50, z: 0, vx: 0, vy: 0, vz: 0, wx: 0, wy: 0, wz: 0, bounces: 0, age: 0, restYaw: 0 }; +} + +/** + * Advances a flying gum shield by `dt`: it falls, bounces lower each time it hits the canvas, then + * slides to a stop and lies flat. `floor` is the canvas height. Returns true while it is still moving. + */ +export function stepShield(state: ShieldState, quaternion: THREE.Quaternion, dt: number, floor: number): boolean { + if (!state.out || !state.moving) return false; + state.age += dt; + const settle = floor + 0.006; + if (!state.sliding) { + state.vy -= GRAVITY * dt; + state.x += state.vx * dt; + state.y += state.vy * dt; + state.z += state.vz * dt; + spin(quaternion, state.wx * dt, state.wy * dt, state.wz * dt); + if (state.y <= settle) { + state.y = settle; + if (state.bounces < MAX_BOUNCES && -state.vy > 0.6) { + state.bounces += 1; + state.vy = -state.vy * RESTITUTION; + state.vx *= 0.72; + state.vz *= 0.72; + state.wx *= 0.55; + state.wy *= 0.7; + state.wz *= 0.55; + } else { + state.vy = 0; + state.sliding = true; + } + } + } else { + const speed = Math.hypot(state.vx, state.vz); + const slowed = Math.max(0, speed - SLIDE_FRICTION * dt); + if (slowed <= REST_SPEED) { + state.vx = 0; + state.vz = 0; + } else { + state.vx *= slowed / speed; + state.vz *= slowed / speed; + } + state.x += state.vx * dt; + state.z += state.vz * dt; + state.y = settle; + state.wy *= Math.exp(-4 * dt); + // It drops flat on the canvas as it slides, turning slowly to a stop, and rests once it lies flat. + state.restYaw += state.wy * dt; + flat.setFromAxisAngle(UP, state.restYaw); + quaternion.slerp(flat, 1 - Math.exp(-12 * dt)); + if (state.vx === 0 && state.vz === 0 && quaternion.angleTo(flat) < 0.02) { + quaternion.copy(flat); + state.moving = false; + } + } + return state.moving; +} + +const UP = new THREE.Vector3(0, 1, 0); +const flat = new THREE.Quaternion(); +const turn = new THREE.Quaternion(); +const turnEuler = new THREE.Euler(); + +function spin(quaternion: THREE.Quaternion, x: number, y: number, z: number): void { + turn.setFromEuler(turnEuler.set(x, y, z)); + quaternion.multiply(turn).normalize(); +} diff --git a/web/hands/src/render/officials.test.ts b/web/hands/src/render/officials.test.ts index 1a436b54..adacb2cc 100644 --- a/web/hands/src/render/officials.test.ts +++ b/web/hands/src/render/officials.test.ts @@ -4,13 +4,16 @@ import type { EngineSnapshot, FighterSnapshot } from "../types"; import { BoxingGraph, SkinnedBoxer, loadBoxerGlb } from "./graph"; import { FightRenderer, refereeSpacing } from "./renderer"; import { applyHeadTrauma } from "./injury"; +import { OFFICIAL_LOOKS } from "./looks"; +import { REFEREE_OUTFIT } from "./outfit"; import { worldPosition } from "./rig"; import { worldMapping } from "./world"; const mapping = worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }); const gltf = await loadBoxerGlb(); -const official = (): SkinnedBoxer => new SkinnedBoxer(gltf, { skin: 0xc79b76, gear: 0x3b57b8, pants: 0x14161c, bodyMap: new THREE.Texture() }); +/** The referee, dressed as the renderer dresses him. */ +const official = (): SkinnedBoxer => new SkinnedBoxer(gltf, { skin: 0xc79b76, gear: 0x3b57b8, look: OFFICIAL_LOOKS.referee, outfit: REFEREE_OUTFIT }); /** Skinned world positions of a boxer's mesh vertices, optionally only those bound mostly to bones matching `bones`. */ function surface(boxer: SkinnedBoxer, meshes: RegExp, bones?: RegExp): THREE.Vector3[] { @@ -48,10 +51,13 @@ describe("referee clinch break", () => { const red = new SkinnedBoxer(gltf, { skin: 0x6e4128, gear: 0xb91c1c }); const blueGraph = new BoxingGraph(blue, mapping); const redGraph = new BoxingGraph(red, mapping); - // The renderer's referee state, driven through its own placement code. + // The renderer's referee state, driven through its own placement code: live, with no replay, ceremony, + // stoppage, body or severed head on the canvas to keep him elsewhere. const renderer = { referee: graph, mapping, tmpA: new THREE.Vector3(), tmpB: new THREE.Vector3(), refereePosition: new THREE.Vector3(Math.cos(angle) * 2, 0, Math.sin(angle) * 2), refereeAway: new THREE.Vector3(), refereeVelocity: new THREE.Vector3(), refereeYaw: 0, + replay: null, ceremony: null, stoppageWinner: -1, stoppageRaiseAt: Number.POSITIVE_INFINITY, frameSeconds: 0, blobShadows: [], graphs: null, + effects: { severedHeadPosition: () => false }, closeUpTarget: new THREE.Vector3(), bodyPoint: new THREE.Vector3(), }; let tick = 0; const step = (one: FighterSnapshot, two: FighterSnapshot): void => { diff --git a/web/hands/src/render/outfit.test.ts b/web/hands/src/render/outfit.test.ts new file mode 100644 index 00000000..169c41b3 --- /dev/null +++ b/web/hands/src/render/outfit.test.ts @@ -0,0 +1,141 @@ +import * as THREE from "three"; +import { describe, expect, it } from "vitest"; +import { SkinnedBoxer, loadBoxerGlb } from "./graph"; +import { SCANNED_LOOK } from "./looks"; +import { CUTMAN_OUTFIT, REFEREE_OUTFIT, applyOutfitShading, buildCuffGeometry, buildHandGeometry } from "./outfit"; + +const gltf = await loadBoxerGlb(); + +const SHADER = { + vertexShader: "#include \n#include \n#include ", + fragmentShader: "#include \n#include \n#include \n#include ", +}; + +function compile(material: THREE.Material): { uniforms: Record; vertexShader: string; fragmentShader: string } { + const shader = { uniforms: {}, ...SHADER }; + material.onBeforeCompile(shader as unknown as THREE.WebGLProgramParametersWithUniforms, null as unknown as THREE.WebGLRenderer); + return shader; +} + +function bounds(geometry: THREE.BufferGeometry): THREE.Box3 { + geometry.computeBoundingBox(); + return geometry.boundingBox!; +} + +describe("official hands", () => { + it("is hand sized, starts at the wrist and curls toward the palm", () => { + const box = bounds(buildHandGeometry("left")); + expect(box.min.y).toBeGreaterThan(-3.5); + expect(box.max.y).toBeGreaterThan(14.6); + expect(box.max.y).toBeLessThan(19.6); + expect(box.max.x - box.min.x).toBeGreaterThan(9); + expect(box.max.x - box.min.x).toBeLessThan(15); + expect(box.max.z).toBeGreaterThan(3); + expect(box.min.z).toBeGreaterThan(-2.5); + }); + + it("puts the thumb on opposite sides of the two hands", () => { + const left = bounds(buildHandGeometry("left")); + const right = bounds(buildHandGeometry("right")); + expect(-left.min.x).toBeGreaterThan(left.max.x + 1); + expect(right.max.x).toBeGreaterThan(-right.min.x + 1); + expect(right.max.x).toBeCloseTo(-left.min.x, 5); + expect(right.min.x).toBeCloseTo(-left.max.x, 5); + }); + + it("keeps smooth normals on every vertex", () => { + const geometry = buildHandGeometry("right"); + const normal = geometry.getAttribute("normal"); + expect(normal.count).toBe(geometry.getAttribute("position").count); + const n = new THREE.Vector3(); + for (let index = 0; index < normal.count; index += 97) expect(n.fromBufferAttribute(normal, index).length()).toBeCloseTo(1, 3); + }); + + it("is cheap enough to give five officials two hands each", () => { + for (const side of ["left", "right"] as const) { + const geometry = buildHandGeometry(side); + expect(geometry.getAttribute("position").count / 3).toBeLessThan(700); + geometry.dispose(); + } + }); + + it("covers the end of the forearm up to the wrist with the cuff", () => { + const box = bounds(buildCuffGeometry()); + expect(box.min.y).toBeLessThan(19); + expect(box.max.y).toBeGreaterThan(25); + }); +}); + +describe("official outfit shading", () => { + it("keeps a shader patch that was already on the material", () => { + const material = new THREE.MeshStandardMaterial(); + material.onBeforeCompile = (shader) => { + shader.fragmentShader = shader.fragmentShader.replace("#include ", "#include \n// earlier patch"); + }; + material.customProgramCacheKey = () => "earlier"; + applyOutfitShading(material, "body", REFEREE_OUTFIT); + const shader = compile(material); + expect(shader.fragmentShader).toContain("// earlier patch"); + expect(shader.fragmentShader).toContain("outfitShirtTone"); + expect(shader.fragmentShader.indexOf("outfitCloth = ")).toBeLessThan(shader.fragmentShader.indexOf("// earlier patch")); + expect(shader.vertexShader).toContain("outfitDraped"); + expect(Object.keys(shader.uniforms).sort()).toEqual(["uOutfitAccent", "uOutfitShape", "uOutfitShirt", "uOutfitTrousers"]); + expect(material.customProgramCacheKey()).toBe("earlier-outfit-body"); + }); + + it("compiles a different program for every part", () => { + const keys = (["head", "body", "shoes", "pants"] as const).map((part) => { + const material = new THREE.MeshStandardMaterial(); + applyOutfitShading(material, part, REFEREE_OUTFIT); + return material.customProgramCacheKey(); + }); + expect(new Set(keys).size).toBe(4); + }); + + it("ends short sleeves above the elbow and long sleeves past the wrist", () => { + const shape = (outfit: typeof REFEREE_OUTFIT): THREE.Vector4 => { + const material = new THREE.MeshStandardMaterial(); + applyOutfitShading(material, "body", outfit); + return (compile(material).uniforms.uOutfitShape as { value: THREE.Vector4 }).value; + }; + expect(shape(CUTMAN_OUTFIT).x).toBeLessThan(31); + expect(shape(REFEREE_OUTFIT).x).toBeGreaterThan(43.4); + expect(shape(REFEREE_OUTFIT).y).toBe(1); + expect(shape(CUTMAN_OUTFIT).y).toBe(0); + }); +}); + +describe("dressed official", () => { + it("wears hands and cuffs instead of boxing gloves", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xc79b76, gear: 0x1b2230, outfit: REFEREE_OUTFIT }); + for (const side of ["left", "right"] as const) { + expect(boxer.gloveMesh(side).visible).toBe(false); + boxer.setHandDismembered(side, false); + expect(boxer.gloveMesh(side).visible).toBe(false); + const suffix = side === "left" ? "L" : "R"; + expect(boxer.rig.bones[`glove${suffix}`].getObjectByName(`hand-${side}`)).toBeInstanceOf(THREE.Mesh); + expect(boxer.rig.bones[`elbow${suffix}`].getObjectByName(`cuff-${side}`)).toBeInstanceOf(THREE.Mesh); + } + boxer.dispose(); + }); + + it("leaves a fighter in gloves with the scanned skin tone", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + expect(boxer.gloveMesh("left").visible).toBe(true); + expect(boxer.rig.bones.gloveL.getObjectByName("hand-left")).toBeUndefined(); + expect(boxer.skin.color.getHex()).toBe(0xffffff); + boxer.setHandDismembered("left", true); + expect(boxer.gloveMesh("left").visible).toBe(false); + boxer.setHandDismembered("left", false); + expect(boxer.gloveMesh("left").visible).toBe(true); + boxer.dispose(); + }); + + it("gives the cutman examination gloves in the glove colour", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb98c66, gear: 0x1b2230, look: { ...SCANNED_LOOK, tint: 0xd8b498 }, outfit: CUTMAN_OUTFIT }); + const hand = boxer.rig.bones.gloveR.getObjectByName("hand-right") as THREE.Mesh; + expect((hand.material as THREE.MeshStandardMaterial).color.getHex()).toBe(CUTMAN_OUTFIT.gloves); + expect(boxer.skin.color.getHex()).toBe(0xd8b498); + boxer.dispose(); + }); +}); diff --git a/web/hands/src/render/outfit.ts b/web/hands/src/render/outfit.ts new file mode 100644 index 00000000..0e998193 --- /dev/null +++ b/web/hands/src/render/outfit.ts @@ -0,0 +1,360 @@ +import * as THREE from "three"; +import { RoundedBoxGeometry } from "three/examples/jsm/geometries/RoundedBoxGeometry.js"; +import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js"; +import type { Hand } from "../types"; + +/** + * Clothing for the ring officials, who share the fighter's body. + * + * The cloth is drawn in the mesh's bind space, like the injuries: a collar line on the neck, sleeves + * measured along the arms, trousers below the waist. The vertex stage rounds off the muscle shading + * under the cloth, turns the lower legs and boot shafts into trouser legs and narrows the trunks to + * meet them. Hands replace the boxing gloves. + */ + +export type OutfitPart = "head" | "body" | "shoes" | "pants"; + +export interface OfficialOutfit { + readonly shirt: number; + readonly trousers: number; + readonly shoes: number; + /** Bow tie, buttons and belt. */ + readonly accent: number; + readonly sleeves: "long" | "short"; + readonly bowTie: boolean; + /** Examination glove colour, or null for bare hands. */ + readonly gloves: number | null; +} + +export const REFEREE_OUTFIT: OfficialOutfit = { shirt: 0xa9c1e6, trousers: 0x111318, shoes: 0x0b0b0d, accent: 0x08080a, sleeves: "long", bowTie: true, gloves: null }; +export const BLUE_CORNER_OUTFIT: OfficialOutfit = { shirt: 0x23408f, trousers: 0x14161c, shoes: 0x0b0b0d, accent: 0x0c0d12, sleeves: "long", bowTie: false, gloves: null }; +export const RED_CORNER_OUTFIT: OfficialOutfit = { shirt: 0x8f2323, trousers: 0x14161c, shoes: 0x0b0b0d, accent: 0x0c0d12, sleeves: "long", bowTie: false, gloves: null }; +export const CUTMAN_OUTFIT: OfficialOutfit = { shirt: 0xb4b8c0, trousers: 0x14161c, shoes: 0x0b0b0d, accent: 0x0c0d12, sleeves: "short", bowTie: false, gloves: 0x4f8fd0 }; + +/** Where the forearm mesh ends, along the arm in bind space. */ +const FOREARM_END = 43.4; +const SHORT_SLEEVE_END = 25; +const SHIRT_LIFT = 0.35; + +const SHARED = /* glsl */ ` +uniform vec4 uOutfitShape; +float outfitCollar(vec3 p) { return mix(111.0, 109.2, smoothstep(-8.0, 0.5, p.z)); } +vec2 outfitLegAxis(float y, float side) { + float x = mix(9.2, 8.1, smoothstep(30.0, 40.0, y)); + x = mix(x, 7.8, smoothstep(40.0, 47.0, y)); + float z = mix(-10.3, -9.0, smoothstep(21.0, 30.0, y)); + z = mix(z, -4.7, smoothstep(30.0, 40.0, y)); + z = mix(z, -3.4, smoothstep(40.0, 47.0, y)); + return vec2(side * x, z); +} +vec2 outfitArmAxis(float along) { + float upper = clamp((along - 14.1) / 16.9, 0.0, 1.0); + float lower = clamp((along - 31.0) / 16.0, 0.0, 1.0); + return vec2(mix(103.9, 102.8, upper) - 3.6 * lower, mix(-5.1, -5.2, upper) + 3.8 * lower); +} +/** Centre (z) and half axes of the torso's cross-section at a height. */ +vec3 outfitTorso(float y) { + float centre = mix(mix(-0.2, -2.8, smoothstep(80.0, 97.0, y)), -4.2, smoothstep(98.0, 107.0, y)); + float wide = mix(11.6, 15.4, smoothstep(80.0, 100.0, y)); + float deep = mix(mix(7.5, 9.1, smoothstep(80.0, 98.0, y)), 7.5, smoothstep(100.0, 108.0, y)); + return vec3(centre, wide, deep); +} +float outfitNeckReach(vec3 p) { return length(vec2(p.x, p.z + 4.6)); } +`; + +const CLOTH_MASK: Record = { + head: "1.0 - smoothstep(outfitCollar(p) - 0.3, outfitCollar(p) + 0.3, p.y)", + body: "p.y < 60.0 ? 1.0 : 1.0 - smoothstep(uOutfitShape.x - 0.4, uOutfitShape.x + 0.4, abs(p.x))", + shoes: "smoothstep(9.0, 11.0, p.y)", + pants: "1.0", +}; + +const vertexDeclarations = (part: OutfitPart): string => /* glsl */ ` +${SHARED} +varying vec3 vOutfitPos; +vec3 outfitBindNormal = vec3(0.0, 1.0, 0.0); +float outfitClothAt(vec3 p) { return ${CLOTH_MASK[part]}; } +vec3 outfitRounded(vec3 p, vec3 n) { + if (p.y < 60.0) { + vec2 radial = normalize(p.xz - outfitLegAxis(p.y, sign(p.x)) + vec2(1e-4)); + return normalize(vec3(radial.x, n.y * 0.3, radial.y)); + } + vec3 shape = outfitTorso(p.y); + float planar = length(n.xz); + vec2 ring = vec2(p.x, p.z - shape.x) / shape.yz; + ring = normalize(ring * ring * ring / shape.yz + vec2(0.0, 1e-5)); + float lift = smoothstep(99.0, 106.0, p.y); + vec3 torso = normalize(vec3(ring.x * mix(1.0, planar, lift), n.y * lift, ring.y * mix(1.0, planar, lift))); + vec2 centre = outfitArmAxis(abs(p.x)); + vec2 around = normalize(vec2(p.y - centre.x, p.z - centre.y) + vec2(1e-4)); + vec3 arm = normalize(vec3(n.x * 0.25, around.x, around.y)); + return normalize(mix(torso, arm, smoothstep(11.0, 17.0, abs(p.x)))); +} +/** Cloth hangs from the high points: hollows in the torso and arms are filled out to a smooth drape. */ +vec3 outfitDraped(vec3 p, vec3 at, float cloth) { + vec3 shape = outfitTorso(p.y); + vec2 offset = vec2(p.x, p.z - shape.x); + float reach = max(length(offset), 1e-3); + vec2 toward = offset / reach; + vec2 squared = toward * toward / (shape.yz * shape.yz); + float rim = 1.0 / sqrt(sqrt(dot(squared, squared))); + float torso = (1.0 - smoothstep(10.5, 15.0, abs(p.x))) * (1.0 - smoothstep(104.0, 108.0, p.y)) * cloth; + at.xz += toward * (max(reach, rim) - reach) * torso; + float along = abs(p.x); + vec2 centre = outfitArmAxis(along); + vec2 around = vec2(p.y - centre.x, p.z - centre.y); + float span = max(length(around), 1e-3); + float sleeve = mix(5.1, 3.3, smoothstep(30.0, 43.0, along)); + float arm = smoothstep(15.0, 19.0, along) * cloth; + at.yz += around / span * (max(span, sleeve) - span) * arm; + return at; +} +`; + +const VERTEX_NORMAL = /* glsl */ ` +{ + vec3 p = position; + float cloth = outfitClothAt(p); + objectNormal = normalize(mix(objectNormal, outfitRounded(p, objectNormal), 0.97 * cloth)); + outfitBindNormal = objectNormal; +} +`; + +const TROUSER_LEG = /* glsl */ ` + float side = sign(p.x); + vec2 axis = outfitLegAxis(p.y, side); + vec2 offset = transformed.xz - axis; + float reach = max(length(offset), 1e-3); + float tube = mix(5.2, 6.0, smoothstep(12.0, 40.0, p.y)); +`; + +const VERTEX_POSITION: Record = { + head: /* glsl */ ` +{ + vec3 p = position; + vOutfitPos = p; + float cloth = outfitClothAt(p); + transformed = outfitDraped(p, transformed, cloth) + outfitBindNormal * uOutfitShape.z * cloth; +} +`, + body: /* glsl */ ` +{ + vec3 p = position; + vOutfitPos = p; + if (p.y < 60.0) { + ${TROUSER_LEG} + float weight = 1.0 - smoothstep(40.5, 45.0, p.y); + transformed.xz = axis + offset / reach * mix(reach, max(reach, tube), weight); + } else { + float cloth = outfitClothAt(p); + transformed = outfitDraped(p, transformed, cloth) + outfitBindNormal * uOutfitShape.z * cloth; + } +} +`, + shoes: /* glsl */ ` +{ + vec3 p = position; + vOutfitPos = p; + ${TROUSER_LEG} + float weight = smoothstep(9.0, 11.5, p.y); + transformed.xz = axis + offset / reach * mix(reach, max(reach, tube), weight); +} +`, + pants: /* glsl */ ` +{ + vec3 p = position; + vOutfitPos = p; + vec2 axis = vec2(sign(p.x) * 8.4, -4.9); + float slim = smoothstep(38.0, 56.0, p.y); + vec2 narrow = vec2(mix(0.76, 1.0, slim), mix(0.62, 1.0, slim)); + transformed.xz = mix(transformed.xz, axis + (transformed.xz - axis) * narrow, smoothstep(0.5, 4.0, abs(p.x))); +} +`, +}; + +const FRAGMENT_DECLARATIONS = /* glsl */ ` +${SHARED} +uniform vec3 uOutfitShirt; +uniform vec3 uOutfitTrousers; +uniform vec3 uOutfitAccent; +varying vec3 vOutfitPos; +float outfitCloth = 0.0; +float outfitGloss = 0.0; +float outfitBowTie(vec3 p) { + if (uOutfitShape.y < 0.5 || p.z < -2.5) return 0.0; + float dx = abs(p.x); + float dy = abs(p.y - 106.6); + float wing = step(dx, 3.7) * step(dy, 0.4 + 1.2 * dx / 3.7); + float knot = step(dx, 0.8) * step(dy, 0.95); + return max(wing, knot); +} +/** The shirt above the waist, the same on the neck mesh and the body mesh so the pattern has no seam. */ +vec3 outfitShirtTone(vec3 p) { + float along = abs(p.x); + float fold = 0.5 + 0.5 * sin(p.y * 0.9 + sin(p.x * 0.7 + p.z * 0.8) * 2.4); + fold *= 0.6 + 0.4 * sin(p.x * 1.9 - p.y * 0.35 + p.z * 1.3); + float reach = outfitNeckReach(p); + float band = (1.0 - smoothstep(7.0, 7.2, reach)) * step(104.0, p.y); + vec3 tone = uOutfitShirt * mix(mix(0.9, 1.0, fold), 1.07, band); + tone *= 1.0 - 0.4 * (1.0 - smoothstep(0.0, 0.3, abs(reach - 7.25))) * step(104.0, p.y); + float hem = min(uOutfitShape.x, ${FOREARM_END.toFixed(1)}) - 2.4; + tone *= mix(1.0, 0.86, smoothstep(hem - 0.1, hem + 0.1, along)); + if (along < 1.0 && p.z > 0.5 && p.y < 106.0) { + tone *= 0.92; + float button = 1.0 - smoothstep(0.32, 0.46, length(vec2(p.x, mod(p.y - 81.0, 4.6) - 2.3))); + tone = mix(tone, uOutfitAccent, button * 0.85); + } + float tie = outfitBowTie(p); + outfitGloss = tie * 0.4; + return mix(tone, uOutfitAccent, tie); +} +`; + +const FRAGMENT_COLOR: Record = { + head: /* glsl */ ` +{ + vec3 p = vOutfitPos; + float collar = outfitCollar(p); + outfitCloth = 1.0 - smoothstep(collar - 0.12, collar + 0.12, p.y); + diffuseColor.rgb = mix(diffuseColor.rgb, outfitShirtTone(p), outfitCloth); + outfitGloss *= outfitCloth; +} +`, + body: /* glsl */ ` +{ + vec3 p = vOutfitPos; + if (p.y < 60.0) { + diffuseColor.rgb = uOutfitTrousers; + outfitCloth = 1.0; + } else { + outfitCloth = 1.0 - smoothstep(uOutfitShape.x - 0.12, uOutfitShape.x + 0.12, abs(p.x)); + diffuseColor.rgb = mix(diffuseColor.rgb, outfitShirtTone(p), outfitCloth); + outfitGloss *= outfitCloth; + } +} +`, + shoes: /* glsl */ ` +{ + float cloth = smoothstep(9.3, 9.7, vOutfitPos.y); + diffuseColor.rgb = mix(diffuseColor.rgb, uOutfitTrousers, cloth); + outfitCloth = cloth; + outfitGloss = 1.0 - cloth; +} +`, + pants: /* glsl */ ` +{ + vec3 p = vOutfitPos; + float belt = smoothstep(76.3, 76.5, p.y); + float buckle = belt * step(abs(p.x), 1.8) * step(6.0, p.z) * step(76.9, p.y) * step(p.y, 79.2); + diffuseColor.rgb = mix(mix(uOutfitTrousers, uOutfitAccent, belt), vec3(0.72, 0.71, 0.66), buckle); + outfitCloth = 1.0 - belt; + outfitGloss = belt; +} +`, +}; + +const FRAGMENT_ROUGHNESS = /* glsl */ ` +roughnessFactor = mix(mix(roughnessFactor, 0.88, outfitCloth), 0.32, outfitGloss); +`; + +const FRAGMENT_CLEARCOAT = /* glsl */ ` +#ifdef USE_CLEARCOAT +material.clearcoat *= 1.0 - outfitCloth; +#endif +`; + +/** Dresses one of the fighter materials. Composes with a shader patch already on the material. */ +export function applyOutfitShading(material: THREE.MeshStandardMaterial, part: OutfitPart, outfit: OfficialOutfit): void { + const uniforms = { + uOutfitShirt: { value: new THREE.Color(outfit.shirt) }, + uOutfitTrousers: { value: new THREE.Color(outfit.trousers) }, + uOutfitAccent: { value: new THREE.Color(outfit.accent) }, + uOutfitShape: { value: new THREE.Vector4(outfit.sleeves === "long" ? FOREARM_END + 20 : SHORT_SLEEVE_END, outfit.bowTie ? 1 : 0, SHIRT_LIFT, 0) }, + }; + const before = material.onBeforeCompile; + const beforeKey = Object.hasOwn(material, "customProgramCacheKey") ? material.customProgramCacheKey() : "plain"; + material.onBeforeCompile = (shader, renderer) => { + before.call(material, shader, renderer); + Object.assign(shader.uniforms, uniforms); + shader.vertexShader = shader.vertexShader + .replace("#include ", `#include \n${vertexDeclarations(part)}`) + .replace("#include ", `#include \n${VERTEX_NORMAL}`) + .replace("#include ", `#include \n${VERTEX_POSITION[part]}`); + shader.fragmentShader = shader.fragmentShader + .replace("#include ", `#include \n${FRAGMENT_DECLARATIONS}`) + .replace("#include ", `#include \n${FRAGMENT_COLOR[part]}`) + .replace("#include ", `#include \n${FRAGMENT_ROUGHNESS}`) + .replace("#include ", `#include \n${FRAGMENT_CLEARCOAT}`); + }; + material.customProgramCacheKey = () => `${beforeKey}-outfit-${part}`; + material.needsUpdate = true; +} + +const UP = new THREE.Vector3(0, 1, 0); + +function segment(from: THREE.Vector3, direction: THREE.Vector3, length: number, radius: number): THREE.BufferGeometry { + const geometry = new THREE.CapsuleGeometry(radius, length, 1, 6); + geometry.applyQuaternion(new THREE.Quaternion().setFromUnitVectors(UP, direction)); + geometry.translate(from.x + direction.x * length * 0.5, from.y + direction.y * length * 0.5, from.z + direction.z * length * 0.5); + return geometry; +} + +/** + * A relaxed hand in the hand bone's frame, in centimetres: wrist at the origin, fingers along +Y, + * palm facing +Z. The thumb is on -X for the left hand and +X for the right. + */ +export function buildHandGeometry(side: Hand): THREE.BufferGeometry { + const thumb = side === "left" ? -1 : 1; + const parts: THREE.BufferGeometry[] = []; + // From the broadcast camera a hand covers a few pixels, so it is built to a few hundred triangles. + const palm = new RoundedBoxGeometry(8.4, 9.2, 2.8, 1, 1.25); + palm.translate(0, 5.2, 0); + parts.push(palm); + const wrist = new THREE.CapsuleGeometry(2.2, 2.2, 1, 6); + wrist.scale(1.3, 1, 0.9); + wrist.translate(0, 0.4, 0); + parts.push(wrist); + const fingers = [ + { x: 3.2 * thumb, y: 9.1, scale: 0.95, curl: 0.22, splay: 0.12 * thumb }, + { x: 1.08 * thumb, y: 9.5, scale: 1.05, curl: 0.34, splay: 0.03 * thumb }, + { x: -1.05 * thumb, y: 9.3, scale: 0.98, curl: 0.46, splay: -0.05 * thumb }, + { x: -3.05 * thumb, y: 8.7, scale: 0.8, curl: 0.6, splay: -0.16 * thumb }, + ]; + for (const finger of fingers) { + const at = new THREE.Vector3(finger.x, finger.y, 0); + let bend = finger.curl; + for (const [length, radius, more] of [[4.3, 1.04, 0.55], [2.7, 0.95, 0.5], [2.2, 0.86, 0]] as const) { + const direction = new THREE.Vector3(Math.sin(finger.splay) * Math.cos(bend), Math.cos(finger.splay) * Math.cos(bend), Math.sin(bend)); + const reach = length * finger.scale; + parts.push(segment(at, direction, reach, radius * (0.88 + 0.12 * finger.scale))); + at.addScaledVector(direction, reach); + bend += more; + } + } + const heel = new THREE.SphereGeometry(1.9, 6, 4); + heel.scale(1.15, 1.5, 0.95); + heel.translate(2.7 * thumb, 3.3, 0.9); + parts.push(heel); + const base = new THREE.Vector3(3.7 * thumb, 3.4, 0.9); + const first = new THREE.Vector3(0.62 * thumb, 0.64, 0.45).normalize(); + parts.push(segment(base, first, 3.9, 1.2)); + base.addScaledVector(first, 3.9); + parts.push(segment(base, new THREE.Vector3(0.3 * thumb, 0.82, 0.48).normalize(), 3, 1.05)); + const plain = parts.map((part) => { + const flat = part.index === null ? part : part.toNonIndexed(); + flat.deleteAttribute("uv"); + return flat; + }); + const merged = mergeGeometries(plain, false); + for (const part of new Set([...parts, ...plain])) part.dispose(); + if (merged === null) throw new Error("hand geometry could not be merged"); + return merged; +} + +/** Covers the end of the forearm mesh up to the wrist: a shirt cuff, or the cuff of an examination glove. */ +export function buildCuffGeometry(): THREE.BufferGeometry { + const geometry = new THREE.CylinderGeometry(3.9, 6, 9.2, 20, 1, false); + geometry.scale(1, 1, 0.96); + geometry.translate(0, 21.1, 0); + return geometry; +} diff --git a/web/hands/src/render/poser.test.ts b/web/hands/src/render/poser.test.ts index 2ea5e085..127b6686 100644 --- a/web/hands/src/render/poser.test.ts +++ b/web/hands/src/render/poser.test.ts @@ -1,12 +1,14 @@ import * as THREE from "three"; -import { punchTiming } from "../manifest"; +import { FIGHTER_RADIUS, RING_HALF_WIDTH, punchTiming } from "../manifest"; import { fighter as baseFighter } from "../test/fixtures"; import type { FighterSnapshot } from "../types"; import { BoxingGraph, SkinnedBoxer, loadBoxerGlb, remapPunchAge } from "./graph"; import { applyHeadTrauma } from "./injury"; import { STANCE } from "./poser"; import { worldPosition, worldQuaternion, type CanonicalBone } from "./rig"; -import { worldMapping } from "./world"; +import { aboveNeckCut } from "./renderer"; +import { ROPE_BACK, ROPE_MAX_GIVE } from "./ring"; +import { ROPE_HEIGHTS, ROPE_LINE, worldMapping } from "./world"; const mapping = worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }); const gltf = await loadBoxerGlb(); @@ -101,20 +103,39 @@ describe("celebration", () => { }); describe("fall direction", () => { + it("goes down the way the blow drove him: a hook to the side, an uppercut or a straight onto his back", () => { + for (const [punchClass, hand] of [["hook", "left"], ["hook", "right"], ["uppercut", "left"], ["straight", "right"]] as const) { + const { boxer, graph } = makeGraph(); + const fighter = facingOpponent(baseFighter("one")); + const opponent = opponentFor("two"); + run(graph, fighter, opponent, 10, undefined); + graph.react("hit", "head", 1, punchClass, hand, 420); + run(graph, { ...fighter, is_downed: true }, opponent, 70, undefined, 5, 10 / 60); + const head = bone(boxer, "head"); + const hips = bone(boxer, "hips"); + expect(head.y).toBeLessThan(0.45); + // The fighter faces +z; a left hook drives the head toward his own left (+x), a right hook toward -x. + if (punchClass === "hook") expect(Math.sign(head.x - hips.x)).toBe(hand === "left" ? 1 : -1); + else expect(head.z).toBeLessThan(hips.z); + } + }); + // The engine's hardest hit does about 153 damage, and the knockdown event that follows the hit carries - // the knockdown count (1-3) as its amount, so the fall must not depend on the amount at all. + // the knockdown count (1-3) as its amount, so the fall must not depend on the amount at all. Live, the + // knockout physics plays the fall (above); these are the animated falls that reduced motion plays. const fall = (punchClass: "hook" | "uppercut" | "straight", target: "head" | "body", hand: "left" | "right", damage: number): { head: THREE.Vector3; hips: THREE.Vector3 } => { const { boxer, graph } = makeGraph(); const fighter = facingOpponent(baseFighter("one")); const opponent = opponentFor("two"); run(graph, fighter, opponent, 10, undefined); + graph.useAuthoredFall(); graph.react("hit", target, 1, punchClass, hand, damage); graph.react("hit", target, 1, punchClass, hand, 1); run(graph, { ...fighter, is_downed: true }, opponent, 70, undefined, 5, 10 / 60); return { head: bone(boxer, "head"), hips: bone(boxer, "hips") }; }; - it("drops face down after a hook or a body shot and onto the back after an uppercut, whatever the damage", () => { + it("animates a face-down fall after a hook or a body shot and onto the back after an uppercut, whatever the damage", () => { for (const [punchClass, target, faceDown] of [["hook", "head", true], ["uppercut", "head", false], ["straight", "body", true]] as const) { for (const damage of [40, 153]) { const { head, hips } = fall(punchClass, target, "left", damage); @@ -181,13 +202,15 @@ function kneeBend(boxer: SkinnedBoxer, side: "L" | "R"): number | null { describe("down and get-up poses", () => { /** * Knocks a fighter down with `punchClass`, fills his get-up meter in two good presses while he is down, - * then lets him up, sampling every frame. + * then lets him up, sampling every frame. These are the animated falls (reduced motion, and a body shot to + * one knee); the knockout physics' falls hand over to the same get-up (see "hand-over to the get-up"). */ const knockdown = (punchClass: "hook" | "uppercut", stance: "orthodox" | "southpaw", sample: (boxer: SkinnedBoxer, frame: number) => void): void => { const { boxer, graph } = makeGraph(); const fighter = facingOpponent({ ...baseFighter("one"), stance, get_up_required: 60 }); const opponent = opponentFor("two"); run(graph, fighter, opponent, 20, undefined); + graph.useAuthoredFall(); graph.react("hit", "head", 1, punchClass, "left", 120); let tick = 10; for (let frame = 0; frame < 240; frame += 1) { @@ -233,7 +256,7 @@ describe("down and get-up poses", () => { describe("get-up", () => { // The engine puts a fighter straight back in the fight when he beats the count: he can walk at once and // act once a 20-tick (0.67 s) stun runs out. - const knockedDown = (): { boxer: SkinnedBoxer; step: (fighter: FighterSnapshot, frames: number) => number; standing: FighterSnapshot; downed: FighterSnapshot; guardZ: number } => { + const knockedDown = (): { boxer: SkinnedBoxer; graph: BoxingGraph; step: (fighter: FighterSnapshot, frames: number) => number; standing: FighterSnapshot; downed: FighterSnapshot; guardZ: number } => { const { boxer, graph } = makeGraph(); const opponent = opponentFor("two"); const head = new THREE.Vector3(0, 1.5, mapping.z(-150)); @@ -247,11 +270,12 @@ describe("get-up", () => { return tick; }; step(standing, 20); - const guardZ = bone(boxer, "gloveL").z; + // Measured from where he stands: after the physics' fall he gets up where his body lies and steps back as he rises. + const guardZ = bone(boxer, "gloveL").z - boxer.root.position.z; graph.react("hit", "head", 1, "uppercut", "right", 120); const downed = { ...standing, is_downed: true }; step(downed, 60); - return { boxer, step, standing, downed, guardZ }; + return { boxer, graph, step, standing, downed, guardZ }; }; it("rises with the get-up meter while down, then stands inside the stun and throws at full reach", () => { @@ -273,16 +297,18 @@ describe("get-up", () => { let reach = -Infinity; for (let frame = 0; frame < (timing.startup + timing.active) * 2; frame += 1) { step(jab, 1); - reach = Math.max(reach, bone(boxer, "gloveL").z); + reach = Math.max(reach, bone(boxer, "gloveL").z - boxer.root.position.z); } expect(reach).toBeGreaterThan(guardZ + 0.25); }); it("falls again from where the get-up had him when he is knocked down mid-rise", () => { - const { boxer, step, standing, downed } = knockedDown(); + const { boxer, graph, step, standing, downed } = knockedDown(); step({ ...downed, get_up_meter: 22 }, 30); step({ ...downed, get_up_meter: 44 }, 30); step({ ...standing, get_up_meter: 66, stunned_ticks: 20 }, 8); + // The animated fall (reduced motion); the knockout physics starts its fall from the pose on screen too. + graph.useAuthoredFall(); const names = Object.keys(boxer.rig.bones) as CanonicalBone[]; let previous = names.map((name) => bone(boxer, name)); expect(previous[names.indexOf("head")]!.y).toBeLessThan(1.3); @@ -351,6 +377,8 @@ describe("falls near the ropes", () => { const opponent = { ...baseFighter("two"), x: 0, y: 0 }; run(graph, fighter, opponent, 20, undefined); const start = furthest(boxer); + // The animated landing (reduced motion): the knockout physics has the ropes and posts in its own world. + graph.useAuthoredFall(); graph.react("hit", "head", 1, punchClass, "left", 120); let widest = 0; for (let frame = 0; frame < 70; frame += 5) { @@ -528,6 +556,28 @@ describe("taunt", () => { }); }); +describe("a spent guard", () => { + it("drops the gloves toward the chest once the guard is worn under what stops a punch, held up or not", async () => { + const { GUARD_BLOCK_MINIMUM } = await import("../manifest"); + const gloves = (guard: number, defense: FighterSnapshot["defense"]) => { + const { boxer, graph } = makeGraph(); + run(graph, { ...facingOpponent(baseFighter("one")), guard, defense }, opponentFor("two"), 45, undefined); + return { left: bone(boxer, "gloveL"), right: bone(boxer, "gloveR"), head: bone(boxer, "head") }; + }; + for (const defense of ["guard_high", "none"] as const) { + const fresh = gloves(80, defense); + const spent = gloves(40, defense); + expect(fresh.left.y - spent.left.y, defense).toBeGreaterThan(0.1); + expect(fresh.right.y - spent.right.y, defense).toBeGreaterThan(0.1); + } + // Held up but spent, the gloves sit under the chin: the opening shows. + const opening = gloves(40, "guard_high"); + expect(Math.max(opening.left.y, opening.right.y)).toBeLessThan(gloves(80, "guard_high").head.y - 0.15); + // At the line itself the guard still stops punches, and stays up. + expect(gloves(GUARD_BLOCK_MINIMUM, "guard_high").left.y).toBeCloseTo(gloves(80, "guard_high").left.y, 3); + }); +}); + describe("cutman", () => { it("crouches before the seated fighter, presses the enswell on the eye, and stands back up when done", () => { const { boxer, graph } = makeGraph(); @@ -691,6 +741,223 @@ describe("own punch prediction", () => { expect(active(graph)).toBe(true); }); + it("shows an opponent's punch as soon as the newest snapshot carries it and still lands it on the server's contact tick", () => { + const { graph } = makeGraph(); + const idle = facingOpponent(baseFighter("one")); + const timing = punchTiming("hook", "head", "normal"); + run(graph, idle, opponentFor("two"), 10, undefined); + let tick = 1000; + const startTick = tick + 2; + const thrown = serverPunch(idle, "theirs-early", "hook", startTick); + const ages: number[] = []; + let contactAt: number | null = null; + for (let frame = 0; frame < 80; frame += 1) { + tick += 0.5; + graph.anticipate(thrown, startTick - tick); + graph.update(tick >= startTick ? thrown : idle, opponentFor("two"), 1 / 60, tick / 30, false, "full", tick, undefined); + if (frame === 0) expect(active(graph)).toBe(true); + if (!active(graph)) break; + ages.push(age(graph)); + if (contactAt === null && age(graph) >= timing.startup) contactAt = tick; + } + for (let index = 1; index < ages.length; index += 1) expect(ages[index]!).toBeGreaterThanOrEqual(ages[index - 1]!); + expect(contactAt).not.toBeNull(); + expect(Math.abs(contactAt! - (startTick + timing.startup))).toBeLessThanOrEqual(1); + }); + + it("holds back a punch that is too far ahead until it can be shown at half speed or faster", () => { + const { graph } = makeGraph(); + const idle = facingOpponent(baseFighter("one")); + const timing = punchTiming("jab", "head", "normal"); + run(graph, idle, opponentFor("two"), 10, undefined); + let tick = 3000; + const startTick = tick + 6; + const thrown = serverPunch(idle, "theirs-far", "jab", startTick); + let startedAt: number | null = null; + let contactAt: number | null = null; + let previous = 0; + for (let frame = 0; frame < 80; frame += 1) { + tick += 0.5; + graph.anticipate(thrown, startTick - tick); + graph.update(tick >= startTick ? thrown : idle, opponentFor("two"), 1 / 60, tick / 30, false, "full", tick, undefined); + if (!active(graph)) { + if (startedAt !== null) break; + continue; + } + startedAt ??= tick; + if (age(graph) < timing.startup) expect(age(graph) - previous).toBeGreaterThanOrEqual(0.5 * 0.5 - 1e-6); + previous = age(graph); + if (contactAt === null && age(graph) >= timing.startup) contactAt = tick; + } + expect(startedAt).not.toBeNull(); + expect(startTick - startedAt!).toBeLessThanOrEqual(timing.startup); + expect(startTick - startedAt!).toBeGreaterThan(timing.startup - 1); + expect(Math.abs(contactAt! - (startTick + timing.startup))).toBeLessThanOrEqual(1); + }); + + it("starts an early punch once even when the delayed clock never reaches it", () => { + const { graph } = makeGraph(); + const idle = facingOpponent(baseFighter("one")); + run(graph, idle, opponentFor("two"), 10, undefined); + const thrown = serverPunch(idle, "theirs-stalled", "hook", 4002); + let starts = 0; + let wasActive = false; + for (let frame = 0; frame < 400; frame += 1) { + graph.anticipate(thrown, 2); + graph.update(idle, opponentFor("two"), 1 / 60, frame / 60, false, "full", 4000, undefined); + if (active(graph) && !wasActive) starts += 1; + wasActive = active(graph); + } + expect(starts).toBe(1); + expect(active(graph)).toBe(false); + }); + + it("does not start a punch early twice or bring back one that has finished", () => { + const { graph } = makeGraph(); + const idle = facingOpponent(baseFighter("one")); + run(graph, idle, opponentFor("two"), 10, undefined); + let tick = 2000; + const thrown = serverPunch(idle, "theirs-once", "jab", tick + 2); + let starts = 0; + let wasActive = false; + for (let frame = 0; frame < 120; frame += 1) { + tick += 0.5; + graph.anticipate(thrown, thrown.action_start_tick - tick); + const retained = tick < thrown.action_start_tick + 28; + graph.update(tick >= thrown.action_start_tick && retained ? thrown : idle, opponentFor("two"), 1 / 60, tick / 30, false, "full", tick, undefined); + if (active(graph) && !wasActive) starts += 1; + wasActive = active(graph); + } + expect(starts).toBe(1); + }); + + it("leaves the viewer's own punch alone when the newest snapshot brings the server's copy of it", () => { + const { graph } = makeGraph(); + const idle = facingOpponent(baseFighter("one")); + const timing = punchTiming("hook", "head", "normal"); + run(graph, idle, opponentFor("two"), 10, undefined); + graph.predict({ kind: "punch", id: "own-early", class: "hook", hand: "left", target: "head", power: "normal" }, 0, 30, 4); + let tick = 100; + const startTick = 103; + const confirmed = serverPunch(idle, "own-early", "hook", startTick); + const ages: number[] = []; + let contactAt: number | null = null; + let starts = 0; + let wasActive = true; + for (let frame = 0; frame < 80; frame += 1) { + tick += 0.5; + if (tick + 2 >= startTick) graph.anticipate(confirmed, startTick - tick); + graph.update(tick > startTick - 1 ? confirmed : idle, opponentFor("two"), 1 / 60, tick / 30, false, "full", tick, undefined); + if (active(graph) && !wasActive) starts += 1; + wasActive = active(graph); + if (!active(graph)) break; + ages.push(age(graph)); + if (contactAt === null && age(graph) >= timing.startup) contactAt = tick; + } + for (let index = 1; index < ages.length; index += 1) expect(ages[index]!).toBeGreaterThanOrEqual(ages[index - 1]!); + expect(starts).toBe(0); + expect(Math.abs(contactAt! - (startTick + timing.startup))).toBeLessThanOrEqual(1); + }); + + const followUp = (startTick: number, lead: number): { startedAt: number; progress: number; starts: number } => { + const { graph } = makeGraph(); + const idle = facingOpponent(baseFighter("one")); + const jab = punchTiming("jab", "head", "normal"); + const total = jab.startup + jab.active + jab.recovery; + run(graph, idle, opponentFor("two"), 10, undefined); + const first = serverPunch(idle, "theirs-first", "jab", 201); + const second = serverPunch(idle, "theirs-second", "hook", startTick); + let tick = 200; + let startedAt = Infinity; + let progress = 0; + let starts = 0; + let previous = ""; + let wasActive = false; + for (let frame = 0; frame < 120; frame += 1) { + tick += 0.5; + const newest = tick + lead >= startTick ? second : tick + lead >= 201 ? first : idle; + const before = age(graph) / total; + if (newest !== idle) graph.anticipate(newest, newest.action_start_tick - tick); + graph.update(tick > startTick - 1 ? second : tick > 200 ? first : idle, opponentFor("two"), 1 / 60, tick / 30, false, "full", tick, undefined); + const shown = active(graph) ? state(graph).punchClass : ""; + if (shown === "hook" && (previous !== "hook" || !wasActive)) { + starts += 1; + if (starts === 1) { + startedAt = tick; + progress = before; + } + } + previous = shown; + wasActive = active(graph); + } + return { startedAt, progress, starts }; + }; + + it("starts a follow-up early once the punch in flight is past half way, and only once", () => { + const late = followUp(211, 3); + expect(late.startedAt).toBeLessThan(210); + expect(late.progress).toBeGreaterThan(0.55); + expect(late.starts).toBe(1); + }); + + it("leaves a follow-up to the server's clock when the punch in flight is not yet half way", () => { + // The server cancels the jab's recovery into a hook, and the news of it is five ticks ahead of the screen. + const soon = followUp(208, 5); + expect(soon.startedAt).toBeGreaterThanOrEqual(207); + expect(soon.startedAt).toBeLessThanOrEqual(208.5); + expect(soon.starts).toBe(1); + }); + + it("does not throw the viewer's own punch again when the server's copy arrives after it has finished", () => { + const { graph } = makeGraph(); + const idle = facingOpponent(baseFighter("one")); + run(graph, idle, opponentFor("two"), 10, undefined); + graph.predict({ kind: "punch", id: "own-late", class: "jab", hand: "left", target: "head", power: "normal" }, 0, 30, 4); + let tick = 500; + for (let frame = 0; frame < 80 && active(graph); frame += 1) { + tick += 0.5; + graph.update(idle, opponentFor("two"), 1 / 60, tick / 30, false, "full", tick, undefined); + } + expect(active(graph)).toBe(false); + graph.anticipate(serverPunch(idle, "own-late", "jab", tick + 2), 2); + expect(active(graph)).toBe(false); + }); + + it("finishes an early punch at full speed when the server stops reporting it", () => { + const timing = punchTiming("hook", "head", "normal"); + const total = timing.startup + timing.active + timing.recovery; + const endsAt = (early: boolean): number => { + const { graph } = makeGraph(); + const idle = facingOpponent(baseFighter("one")); + run(graph, idle, opponentFor("two"), 10, undefined); + const startTick = 303; + const thrown = serverPunch(idle, "theirs-cleared", "hook", startTick); + let tick = 300; + for (let frame = 0; frame < 200; frame += 1) { + tick += 0.5; + if (early && tick + 3 >= startTick && tick < startTick + 4) graph.anticipate(thrown, startTick - tick); + graph.update(tick >= startTick && tick < startTick + 4 ? thrown : idle, opponentFor("two"), 1 / 60, tick / 30, false, "full", tick, undefined); + if (tick > startTick && !active(graph)) return tick - startTick; + } + return Infinity; + }; + expect(endsAt(false)).toBeLessThanOrEqual(total); + expect(endsAt(true)).toBeLessThanOrEqual(total + 1); + }); + + it("forgets a punch it started early when the fighter is reset", () => { + const { graph } = makeGraph(); + const idle = facingOpponent(baseFighter("one")); + run(graph, idle, opponentFor("two"), 10, undefined); + const thrown = serverPunch(idle, "theirs-again", "hook", 403); + graph.anticipate(thrown, 2); + expect(active(graph)).toBe(true); + graph.resetTransient(); + expect(active(graph)).toBe(false); + graph.anticipate(thrown, 2); + expect(active(graph)).toBe(true); + }); + it("plays an opponent's punch on the server's timeline", () => { const { graph } = makeGraph(); const idle = facingOpponent(baseFighter("one")); @@ -702,12 +969,123 @@ describe("own punch prediction", () => { }); }); +describe("decision ceremony", () => { + const square = (id: string): FighterSnapshot => ({ ...baseFighter(id), x: 0, y: 0, facing_x: 0, facing_y: -1000 }); + const beside = (): FighterSnapshot => ({ ...baseFighter("other"), x: 204, y: 0, facing_x: 0, facing_y: -1000 }); + const settle = (prepare: (graph: BoxingGraph) => void, frames = 150): { boxer: SkinnedBoxer; graph: BoxingGraph } => { + const made = makeGraph(); + prepare(made.graph); + run(made.graph, square("one"), beside(), frames, undefined); + return made; + }; + + it("stands the fighter square to the camera with the gloves down while the cards are read", () => { + const { boxer } = settle((graph) => graph.awaitVerdict(1)); + const left = bone(boxer, "gloveL"); + const right = bone(boxer, "gloveR"); + expect(left.y).toBeLessThan(1.05); + expect(right.y).toBeLessThan(1.05); + expect(Math.abs(left.y - right.y)).toBeLessThan(0.05); + expect(Math.abs(left.z - right.z)).toBeLessThan(0.06); + expect(left.x).toBeGreaterThan(0.15); + expect(right.x).toBeLessThan(-0.15); + expect(Math.abs(bone(boxer, "ankleL").z - bone(boxer, "ankleR").z)).toBeLessThan(0.1); + }); + + it("raises the winner's arm on the referee's side and leaves the other down", () => { + for (const side of [1, -1] as const) { + const { boxer } = settle((graph) => { + graph.awaitVerdict(side); + graph.announce("winner"); + }); + const raised = bone(boxer, side === 1 ? "gloveL" : "gloveR"); + const lowered = bone(boxer, side === 1 ? "gloveR" : "gloveL"); + expect(raised.y).toBeGreaterThan(bone(boxer, "head").y + 0.2); + expect(Math.sign(raised.x)).toBe(side); + expect(lowered.y).toBeLessThan(1.05); + } + }); + + it("raises both fighters' arms after a draw and bows the loser's head", () => { + const level = settle((graph) => { + graph.awaitVerdict(-1); + graph.announce("level"); + }); + expect(bone(level.boxer, "gloveR").y).toBeGreaterThan(bone(level.boxer, "head").y + 0.2); + const waiting = settle((graph) => graph.awaitVerdict(1)); + const loser = settle((graph) => { + graph.awaitVerdict(1); + graph.announce("loser"); + }); + expect(bone(loser.boxer, "gloveL").y).toBeLessThan(1.05); + expect(bone(loser.boxer, "gloveR").y).toBeLessThan(1.05); + const chin = (boxer: SkinnedBoxer): number => new THREE.Vector3(0, 0, 1).applyQuaternion(boxer.rig.bones.head.getWorldQuaternion(new THREE.Quaternion())).y; + expect(chin(loser.boxer)).toBeLessThan(chin(waiting.boxer) - 0.2); + }); + + it("puts the referee's hand around the wrist it is given, on that side", () => { + const made = new SkinnedBoxer(gltf, { skin: 0xc79b76, gear: 0x1b2230 }); + const referee = new BoxingGraph(made, mapping, { referee: true }); + const wrist = new THREE.Vector3(-0.3, 1.9, 0.3); + referee.raise(null, wrist); + run(referee, square("referee"), { ...square("focus"), y: -300 }, 150, undefined); + const hand = bone(made, "gloveR"); + expect(hand.distanceTo(new THREE.Vector3(-0.3, 1.73, 0.3))).toBeLessThan(0.04); + expect(bone(made, "gloveL").y).toBeLessThan(1.05); + referee.raise(null, null); + run(referee, square("referee"), { ...square("focus"), y: -300 }, 150, undefined); + expect(bone(made, "gloveR").y).toBeLessThan(1.05); + }); + + it("ends with the bout", () => { + const { boxer, graph } = settle((made) => { + made.awaitVerdict(1); + made.announce("winner"); + }); + expect(bone(boxer, "gloveL").y).toBeGreaterThan(1.8); + graph.awaitVerdict(null); + run(graph, facingOpponent(baseFighter("one")), opponentFor("two"), 150, undefined); + expect(bone(boxer, "gloveL").y).toBeLessThan(1.7); + graph.awaitVerdict(1); + graph.announce("winner"); + graph.resetTransient(); + run(graph, facingOpponent(baseFighter("one")), opponentFor("two"), 5, undefined); + expect(bone(boxer, "gloveL").y).toBeLessThan(1.7); + }); +}); + +describe("infighting", () => { + const settle = (gapUnits: number): { glove: number; head: THREE.Vector3 } => { + const { boxer, graph } = makeGraph(); + const fighter = { ...facingOpponent(baseFighter("one")), defense: "guard_high" as const }; + const opponent = { ...opponentFor("two"), y: -gapUnits }; + run(graph, fighter, opponent, 90, undefined); + return { glove: bone(boxer, "gloveL").z - boxer.root.position.z, head: bone(boxer, "head").sub(boxer.root.position) }; + }; + + it("tucks the guard in and takes the head off the centre line when the opponent is on top of the fighter", () => { + const open = settle(300); + const inside = settle(76); + const toward = Math.sign(open.glove); + expect(toward * (open.glove - inside.glove)).toBeGreaterThan(0.08); + expect(Math.abs(inside.head.x - open.head.x)).toBeGreaterThan(0.04); + expect(toward * (open.head.z - inside.head.z)).toBeGreaterThan(0.03); + }); + + it("stands in the open stance at punching range", () => { + const open = settle(300); + const ranged = settle(170); + expect(Math.abs(open.glove - ranged.glove)).toBeLessThan(0.005); + expect(open.head.distanceTo(ranged.head)).toBeLessThan(0.005); + }); +}); + describe("clinch hold", () => { it("ties up over the arms for the first-sorted fighter and under them for the other, heads to the right", () => { const clinched = (id: string): FighterSnapshot => ({ ...facingOpponent(baseFighter(id)), clinch_ticks: 30 }); const held = (id: string): FighterSnapshot => ({ ...opponentFor(id), y: -100, clinch_ticks: 30 }); const idle = makeGraph(); - run(idle.graph, facingOpponent(baseFighter("one")), held("two"), 60, undefined); + run(idle.graph, facingOpponent(baseFighter("one")), { ...opponentFor("two"), y: -300 }, 60, undefined); const over = makeGraph(); run(over.graph, clinched("one"), held("two"), 60, undefined); const under = makeGraph(); @@ -732,21 +1110,169 @@ describe("clinch hold", () => { } expect(Math.max(...sway) - Math.min(...sway)).toBeGreaterThan(0.008); }); + + /** The skinned skull above the neck cut, sampled, and the ellipsoid its bounding box holds in the head bone's frame. */ + const skull = (boxer: SkinnedBoxer): { inverse: THREE.Matrix4; centre: THREE.Vector3; radii: THREE.Vector3; points: THREE.Vector3[] } => { + boxer.root.updateMatrixWorld(true); + const mesh = boxer.headMesh; + const inverse = boxer.bone("head")!.matrixWorld.clone().invert(); + const position = mesh.geometry.getAttribute("position"); + const points: THREE.Vector3[] = []; + const box = new THREE.Box3(); + for (let index = 0; index < position.count; index += 7) { + if (!aboveNeckCut(skinnedVertex.fromBufferAttribute(position, index))) continue; + const point = mesh.getVertexPosition(index, new THREE.Vector3()).applyMatrix4(mesh.matrixWorld); + points.push(point); + box.expandByPoint(point.clone().applyMatrix4(inverse)); + } + return { inverse, centre: box.getCenter(new THREE.Vector3()), radii: box.getSize(new THREE.Vector3()).multiplyScalar(0.5), points }; + }; + /** How many sampled points of one skull are inside the other's. */ + const inside = (one: SkinnedBoxer, other: SkinnedBoxer): number => { + const from = skull(one); + const into = skull(other); + const local = new THREE.Vector3(); + return from.points.filter((point) => { + local.copy(point).applyMatrix4(into.inverse).sub(into.centre); + return Math.hypot(local.x / into.radii.x, local.y / into.radii.y, local.z / into.radii.z) < 1; + }).length; + }; + + it("rests the two heads on opposite shoulders whatever the stances, clear of each other through the struggle", () => { + for (const [first, second] of [["orthodox", "orthodox"], ["orthodox", "southpaw"], ["southpaw", "orthodox"], ["southpaw", "southpaw"]] as const) { + const one = makeGraph(); + const two = makeGraph(); + // Held at the engine's clinch distance, 60 units, as the hold draws them in. + const a: FighterSnapshot = { ...facingOpponent(baseFighter("one")), stance: first, clinch_ticks: 30 }; + const b: FighterSnapshot = { ...opponentFor("two"), stance: second, y: -60, clinch_ticks: 30 }; + const headA = new THREE.Vector3(); + const headB = new THREE.Vector3(); + let closest = Infinity; + let overlapping = 0; + for (let frame = 0; frame < 150; frame += 1) { + const time = 1 + frame / 60; + one.graph.update(a, b, 1 / 60, time, false, "full", 10 + frame * 0.5, frame > 0 ? headB : undefined); + two.graph.update(b, a, 1 / 60, time, false, "full", 10 + frame * 0.5, frame > 0 ? headA : undefined); + bone(one.boxer, "head", headA); + bone(two.boxer, "head", headB); + if (frame < 30) continue; + closest = Math.min(closest, headA.distanceTo(headB)); + if (frame % 15 === 0) overlapping += inside(one.boxer, two.boxer) + inside(two.boxer, one.boxer); + } + expect(closest, `${first} v ${second}`).toBeGreaterThan(0.3); + expect(overlapping, `${first} v ${second}`).toBe(0); + // Each to his own right: one faces -z here, so his right is -x, and two's is +x. + expect(headA.x).toBeLessThan(-0.1); + expect(headB.x).toBeGreaterThan(0.1); + one.boxer.dispose(); + two.boxer.dispose(); + } + }); +}); + +describe("on the ropes", () => { + /** Fighter one with his back to the +x ropes, facing the middle, and two on top of him: `units` is one's engine x. */ + const pinned = (units: number): [FighterSnapshot, FighterSnapshot] => [ + { ...baseFighter("one"), x: units, y: 0, facing_x: -1000, facing_y: 0, defense: "guard_high" }, + { ...baseFighter("two"), x: units - 115, y: 0, facing_x: 1000, facing_y: 0 }, + ]; + const fight = (one: { boxer: SkinnedBoxer; graph: BoxingGraph }, two: { boxer: SkinnedBoxer; graph: BoxingGraph }, [a, b]: [FighterSnapshot, FighterSnapshot], frames: number, from = 0): void => { + const headA = new THREE.Vector3(); + const headB = new THREE.Vector3(); + for (let frame = 0; frame < frames; frame += 1) { + const time = 1 + (from + frame) / 60; + one.graph.update(a, b, 1 / 60, time, false, "full", 10 + (from + frame) * 0.5, from + frame > 0 ? headB : undefined); + two.graph.update(b, a, 1 / 60, time, false, "full", 10 + (from + frame) * 0.5, from + frame > 0 ? headA : undefined); + bone(one.boxer, "head", headA); + bone(two.boxer, "head", headB); + } + }; + /** How far past the line `ropeX` the skin reaches, near each rope's height and across the fighter's back. */ + const pastTheRope = (boxer: SkinnedBoxer, height: number, ropeX: number): number => { + boxer.root.updateMatrixWorld(true); + let furthest = -Infinity; + boxer.root.traverse((object) => { + if (!(object instanceof THREE.SkinnedMesh)) return; + const count = object.geometry.getAttribute("position").count; + for (let index = 0; index < count; index += 2) { + object.getVertexPosition(index, skinnedVertex).applyMatrix4(object.matrixWorld); + if (Math.abs(skinnedVertex.y - (height - 0.02)) < 0.03 && Math.abs(skinnedVertex.z) < 0.35) furthest = Math.max(furthest, skinnedVertex.x - ropeX); + } + }); + return furthest; + }; + const lean = (boxer: SkinnedBoxer): number => bone(boxer, "upperChest").x - bone(boxer, "hips").x; + + it("draws a fighter the engine has past the ropes' give in, his back resting on the top rope, leaning back on it", () => { + const limit = RING_HALF_WIDTH - FIGHTER_RADIUS; + const open = [makeGraph(), makeGraph()] as const; + fight(open[0], open[1], pinned(300), 90); + const one = makeGraph(); + const two = makeGraph(); + fight(one, two, pinned(limit), 90); + // Drawn in so that his back is no further out than the ropes give. + expect(one.boxer.root.position.x + ROPE_BACK).toBeLessThanOrEqual(ROPE_LINE + ROPE_MAX_GIVE + 1e-6); + // The man on top of him comes in with him: the gap between them is the engine's. + expect(one.boxer.root.position.x - two.boxer.root.position.x).toBeCloseTo(mapping.x(115), 2); + // No rope passes through him, and his back rests against the top one rather than standing off it. + const rope = ROPE_LINE + ROPE_MAX_GIVE; + const ropeRadius = 0.028; + for (const height of [ROPE_HEIGHTS[1], ROPE_HEIGHTS[2]]) expect(pastTheRope(one.boxer, height, rope)).toBeLessThan(ropeRadius); + expect(pastTheRope(one.boxer, ROPE_HEIGHTS[2], rope)).toBeGreaterThan(-0.06); + // Leaning back toward the ropes from the hips. + expect(lean(one.boxer) - lean(open[0].boxer)).toBeGreaterThan(0.05); + // He eases back upright once he comes off the ropes. + fight(one, two, pinned(300), 90, 90); + expect(Math.abs(lean(one.boxer) - lean(open[0].boxer))).toBeLessThan(0.01); + expect(one.boxer.root.position.x).toBeCloseTo(mapping.x(300), 2); + for (const made of [...open, one, two]) made.boxer.dispose(); + }); + + it("leaves the ropes alone short of their furthest give, where they bow behind him", () => { + const one = makeGraph(); + const two = makeGraph(); + fight(one, two, pinned(380), 90); + expect(one.boxer.root.position.x).toBeCloseTo(mapping.x(380), 3); + expect(Math.abs(lean(one.boxer))).toBeLessThan(0.1); + for (const made of [one, two]) made.boxer.dispose(); + }); +}); + +describe("two heads at close range", () => { + it("leans the head away from the other's rather than through it", () => { + const { boxer, graph } = makeGraph(); + const fighter = facingOpponent(baseFighter("one")); + const opponent = opponentFor("two"); + run(graph, fighter, opponent, 40, undefined); + const own = bone(boxer, "head").clone(); + const forward = new THREE.Vector3(0, 0, 1).applyQuaternion(boxer.root.getWorldQuaternion(new THREE.Quaternion())); + forward.y = 0; + const other = own.clone().addScaledVector(forward.normalize(), 0.06); + for (let frame = 0; frame < 60; frame += 1) run(graph, fighter, opponent, 1, other, 20 + frame * 0.5, (40 + frame) / 60); + expect(bone(boxer, "head").distanceTo(other)).toBeGreaterThan(0.15); + // Clear of it, he stands as he did. + for (let frame = 0; frame < 60; frame += 1) run(graph, fighter, opponent, 1, other.clone().addScaledVector(forward, 1), 50 + frame * 0.5, (100 + frame) / 60); + expect(bone(boxer, "head").distanceTo(own)).toBeLessThan(0.03); + }); }); describe("wave-off", () => { - it("sweeps both gloves across overhead while waving the fight off", () => { + it("sweeps both gloves wide across the chest, below the face, while waving the fight off", () => { const { boxer, graph } = makeGraph(); const fighter = facingOpponent(baseFighter("one")); const opponent = opponentFor("two"); graph.waveOff(4); - run(graph, fighter, opponent, 60, undefined); - const first = bone(boxer, "gloveL").clone(); - expect(first.y).toBeGreaterThan(1.45); - run(graph, fighter, opponent, 12, undefined, 30, 1); - const later = bone(boxer, "gloveL"); - expect(later.y).toBeGreaterThan(1.45); - expect(Math.abs(later.x - first.x)).toBeGreaterThan(0.15); + const xs: number[] = []; + for (let frame = 0; frame < 120; frame += 1) { + run(graph, fighter, opponent, 1, undefined, frame * 0.5, frame / 60); + if (frame < 40) continue; + const glove = bone(boxer, "gloveL"); + const head = bone(boxer, "head"); + xs.push(glove.x); + expect(glove.y).toBeGreaterThan(0.95); + expect(glove.y).toBeLessThan(head.y - 0.05); + } + expect(Math.max(...xs) - Math.min(...xs)).toBeGreaterThan(0.6); }); }); @@ -995,6 +1521,98 @@ describe("runtime boxing graph", () => { }); }); +describe("body-shot knockdown", () => { + const kneelAfterBodyShot = (): { boxer: SkinnedBoxer; graph: BoxingGraph; fighter: FighterSnapshot; opponent: FighterSnapshot } => { + const { boxer, graph } = makeGraph(); + const fighter = facingOpponent(baseFighter("one")); + const opponent = opponentFor("two"); + run(graph, fighter, opponent, 10, undefined); + graph.windedFor(0.35, -1); + run(graph, { ...fighter, stunned_ticks: 10 }, opponent, 20, undefined, 5, 10 / 60); + graph.fallToKnee(true); + run(graph, { ...fighter, is_downed: true }, opponent, 90, undefined, 15, 30 / 60); + return { boxer, graph, fighter, opponent }; + }; + + it("takes a knee: the rear knee on the canvas, the glove on the struck side clamped on the ribs, the other on the front knee", () => { + const { boxer } = kneelAfterBodyShot(); + const rearKnee = bone(boxer, "kneeR"); + const frontKnee = bone(boxer, "kneeL"); + const hips = bone(boxer, "hips"); + expect(rearKnee.y).toBeLessThan(0.16); + expect(frontKnee.y).toBeGreaterThan(0.4); + expect(bone(boxer, "ankleL").y).toBeLessThan(0.2); + expect(hips.y).toBeGreaterThan(0.4); + expect(hips.y).toBeLessThan(0.65); + const head = bone(boxer, "head"); + expect(head.y).toBeGreaterThan(0.75); + expect(head.y).toBeLessThan(1.25); + expect(bone(boxer, "gloveL").distanceTo(frontKnee)).toBeLessThan(0.25); + const clutch = bone(boxer, "gloveR"); + expect(clutch.x).toBeLessThan(hips.x); + expect(clutch.y).toBeGreaterThan(0.5); + expect(clutch.y).toBeLessThan(0.85); + }); + + it("gets up off the knee, and the next knockdown that is not a body shot is a fall again", () => { + const { boxer, graph, fighter, opponent } = kneelAfterBodyShot(); + run(graph, fighter, opponent, 140, undefined, 60, 2); + expect(bone(boxer, "hips").y).toBeGreaterThan(0.72); + graph.react("hit", "head", 1, "uppercut", "right", 420); + run(graph, { ...fighter, is_downed: true }, opponent, 70, undefined, 140, 5); + expect(bone(boxer, "head").y).toBeLessThan(0.45); + }); + + it("folds over the shot before he goes down", () => { + const { boxer, graph } = makeGraph(); + const fighter = facingOpponent(baseFighter("one")); + const opponent = opponentFor("two"); + run(graph, fighter, opponent, 30, undefined); + const upright = bone(boxer, "head").clone(); + graph.windedFor(1, 1); + run(graph, { ...fighter, stunned_ticks: 20 }, opponent, 20, undefined, 15, 0.5); + const folded = bone(boxer, "head"); + expect(folded.y).toBeLessThan(upright.y - 0.12); + const clutch = bone(boxer, "gloveL"); + expect(clutch.y).toBeLessThan(1.1); + expect(clutch.x).toBeGreaterThan(bone(boxer, "hips").x); + }); +}); + +describe("parry", () => { + it("knocks the puncher back with his guard thrown off", () => { + const { boxer, graph } = makeGraph(); + const fighter = facingOpponent(baseFighter("one")); + const opponent = opponentFor("two"); + run(graph, fighter, opponent, 40, undefined); + const head = bone(boxer, "head").clone(); + const lean = head.z - bone(boxer, "hips").z; + const glove = bone(boxer, "gloveL").clone(); + graph.stagger(); + run(graph, fighter, opponent, 6, undefined, 20, 40 / 60); + expect(bone(boxer, "head").z).toBeLessThan(head.z - 0.03); + expect(bone(boxer, "head").z - bone(boxer, "hips").z).toBeLessThan(lean - 0.02); + expect(bone(boxer, "gloveL").y).toBeLessThan(glove.y - 0.02); + }); +}); + +describe("cutman's bottle", () => { + it("holds a bottle at the mouth for the breath and the enswell for a cut", () => { + const { boxer, graph } = makeGraph(); + const mouth = new THREE.Vector3(0.03, 1.1, 0.5); + graph.treat(mouth, new THREE.Vector3(0, 0, -1), 1, "bottle"); + run(graph, facingOpponent(baseFighter("one")), opponentFor("two"), 90, undefined); + const glove = boxer.rig.bones.gloveL; + expect(glove.getObjectByName("bottle")?.visible).toBe(true); + expect(glove.getObjectByName("enswell")?.visible).toBe(false); + expect(bone(boxer, "gloveL").distanceTo(new THREE.Vector3(mouth.x, mouth.y + 0.02, mouth.z - 0.3))).toBeLessThan(0.12); + graph.treat(mouth, new THREE.Vector3(0, 0, -1), 1, "enswell"); + run(graph, facingOpponent(baseFighter("one")), opponentFor("two"), 30, undefined, 45, 1.5); + expect(glove.getObjectByName("bottle")?.visible).toBe(false); + expect(glove.getObjectByName("enswell")?.visible).toBe(true); + }); +}); + describe("per-frame allocations", () => { /** Counts the vectors, quaternions and Euler angles built (clones included) while `body` runs. */ const constructions = (body: () => void): number => { @@ -1037,8 +1655,9 @@ describe("per-frame allocations", () => { describe("transition sweep", () => { const PUNCHES = ["jab", "straight", "hook", "uppercut"] as const; - // 9 m/s: a straight's glove peaks near 7 m/s, while the snaps this guards against moved bones 0.5 m in a frame. - const MAX_STEP = 0.15; + // 9.6 m/s: a straight's glove peaks near 7 m/s and a hook's elbow from the crowded guard of fighters this close + // near 9.2 m/s, while the snaps this guards against moved bones 0.5 m in a frame. + const MAX_STEP = 0.16; it("moves every bone smoothly, keeps the skin above the canvas and bends the knees forward from idle through punches, falls, get-ups, the corner stool and a taunt", () => { const { boxer, graph } = makeGraph(); @@ -1101,14 +1720,18 @@ describe("transition sweep", () => { step("idle", idle, 30); for (const punchClass of PUNCHES) step(punchClass, punch(punchClass, punchClass === "jab" || punchClass === "hook" ? "left" : "right"), 40); + // The animated falls (reduced motion); "hand-over to the get-up" sweeps the knockout physics' ones. + graph.useAuthoredFall(); graph.react("hit", "head", 1, "hook", "left", 140); step("fall face down", downed, 70); getUp("first"); step("stand", idle, 30); + graph.useAuthoredFall(); graph.react("hit", "head", 1, "uppercut", "right", 120); step("fall on the back", downed, 70); step("get-up meter", (frame) => ({ ...downed, get_up_meter: frame < 30 ? 22 : 44 }), 60); step("get-up", released, 8); + graph.useAuthoredFall(); step("knocked down rising", downed, 70); getUp("second"); step("fight on", idle, 30); @@ -1124,3 +1747,66 @@ describe("transition sweep", () => { expect(knee.along, knee.at).toBeGreaterThan(0.25); }); }); + +describe("hand-over to the get-up", () => { + // Live, the knockout physics plays the fall. As the fighter starts to get up it hands the body to the animated + // get-up, which begins from the pose the fall left (hips, spine, gloves, ankles and the way each joint bends) + // and follows the meter from there. + const knockedDown = (punchClass: "hook" | "uppercut", hand: "left" | "right") => { + const { boxer, graph } = makeGraph(); + const opponent = opponentFor("two"); + const head = new THREE.Vector3(0, 1.5, mapping.z(-150)); + const standing = facingOpponent({ ...baseFighter("one"), get_up_required: 66 }); + const downed = { ...standing, is_downed: true }; + const names = Object.keys(boxer.rig.bones) as CanonicalBone[]; + let previous: THREE.Vector3[] = []; + let tick = 0; + let jump = { distance: 0, at: "" }; + let under = Infinity; + const step = (label: string, fighter: FighterSnapshot, frames: number, measure = true): void => { + for (let frame = 0; frame < frames; frame += 1) { + tick += 0.5; + graph.update(fighter, opponent, 1 / 60, tick / 30, false, "full", tick, head); + const now = names.map((name) => bone(boxer, name)); + if (measure) { + for (const [index, position] of now.entries()) { + const distance = position.distanceTo(previous[index]!); + if (distance > jump.distance) jump = { distance, at: `${label} frame ${frame} ${names[index]}` }; + } + if (frame % 3 === 0) under = Math.min(under, lowestVertex(boxer, "MHeadMat0"), lowestVertex(boxer, "GlovesMat0")); + } + previous = now; + } + }; + step("guard", standing, 20, false); + graph.react("hit", "head", 1, punchClass, hand, 120); + step("fall", downed, 70, false); + return { boxer, graph, step, standing, downed, jump: () => jump, lowest: () => under }; + }; + + it.each([["hook", "left"], ["uppercut", "right"]] as const)("takes the body a %s left on the canvas into the get-up with no snap", (punchClass, hand) => { + const { boxer, graph, step, standing, downed, jump, lowest } = knockedDown(punchClass, hand); + expect(graph.fallBody).not.toBeNull(); + step("first press", { ...downed, get_up_meter: 22 }, 30); + // The physics follows the get-up off the canvas, and lets go once he is on all fours. + expect(graph.fallBody?.handingOver).toBe(true); + step("second press", { ...downed, get_up_meter: 44 }, 30); + expect(graph.fallBody).toBeNull(); + step("up", { ...standing, get_up_meter: 66, stunned_ticks: 20 }, 40); + // The body used to freeze where the physics left it and then snap 1.3 m into the get-up; turning the bones + // straight from the fall's pose to the get-up's then swung limbs up to 0.85 m in a frame and 0.5 m through the canvas. + expect(jump().distance, jump().at).toBeLessThan(0.16); + expect(lowest(), "skin under the canvas").toBeGreaterThan(-0.03); + expect(bone(boxer, "head").y).toBeGreaterThan(1.4); + }); + + it("leaves a fighter the bout is over for where the physics left him, whatever his meter says", () => { + const { graph, step, downed } = knockedDown("hook", "left"); + graph.stayDown(true); + step("counted out", { ...downed, get_up_meter: 44 }, 30, false); + expect(graph.fallBody).not.toBeNull(); + graph.stayDown(false); + step("pressing", { ...downed, get_up_meter: 44 }, 2, false); + expect(graph.fallBody?.handingOver).toBe(true); + }); +}); diff --git a/web/hands/src/render/punch-aim.test.ts b/web/hands/src/render/punch-aim.test.ts index cd56601a..a383e042 100644 --- a/web/hands/src/render/punch-aim.test.ts +++ b/web/hands/src/render/punch-aim.test.ts @@ -180,8 +180,9 @@ describe("punch onset", () => { if (tick >= 100) moves.push(step); } expect(moves[0], `${hand} ${punchClass} first frame`).toBeLessThan(0.05); - // No faster than the punches themselves move near contact, and building up rather than starting there. - expect(Math.max(...moves), `${hand} ${punchClass} windup`).toBeLessThan(0.15); + // No faster than the punches themselves move near contact, and building up rather than starting there. (From + // the tucked guard of fighters this close a hook's windup peaks near 9.5 m/s.) + expect(Math.max(...moves), `${hand} ${punchClass} windup`).toBeLessThan(0.16); expect(moves[0]!, `${hand} ${punchClass} eases in`).toBeLessThan(Math.max(...moves)); } } diff --git a/web/hands/src/render/ragdoll.test.ts b/web/hands/src/render/ragdoll.test.ts new file mode 100644 index 00000000..e0e43fd1 --- /dev/null +++ b/web/hands/src/render/ragdoll.test.ts @@ -0,0 +1,772 @@ +import { readFileSync } from "node:fs"; +import * as THREE from "three"; +import { fighter as baseFighter } from "../test/fixtures"; +import type { DefensivePose, FighterSnapshot } from "../types"; +import { BoxingGraph, PRESS_ONTO_CANVAS_GLSL, SkinnedBoxer, loadBoxerGlb } from "./graph"; +import { NECK_CUT_DEPTH, NECK_CUT_HEIGHT, NECK_CUT_SLOPE } from "./injury"; +import { KnockoutRagdoll, P, PARTICLES, RADIUS, RagdollBody, blowImpulse, fallStyleFor, type ImpulseRecord } from "./ragdoll"; +import { closeUpAngle } from "./renderer"; +import { worldPosition, worldQuaternion } from "./rig"; +import { ROPE_LINE, RING_FIGHT_HALF, worldMapping } from "./world"; + +const mapping = worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }); +const gltf = await loadBoxerGlb(); + +const facing = (fighter: FighterSnapshot, x = 0, y = 0): FighterSnapshot => ({ ...fighter, facing_x: 0, facing_y: -1000, x, y }); +const opponentAt = (x = 0, y = -150): FighterSnapshot => ({ ...baseFighter("two"), x, y, facing_x: 0, facing_y: 1000 }); + +function standing(x = 0, y = 0, defense?: DefensivePose): { boxer: SkinnedBoxer; graph: BoxingGraph; fighter: FighterSnapshot; opponent: FighterSnapshot; time: number } { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + const graph = new BoxingGraph(boxer, mapping); + const fighter = { ...facing(baseFighter("one"), x, y), ...(defense === undefined ? {} : { defense }) }; + const opponent = opponentAt(x, y - 150); + for (let frame = 0; frame < 12; frame += 1) graph.update(fighter, opponent, 1 / 60, frame / 60, false, "full", frame / 2); + return { boxer, graph, fighter, opponent, time: 12 / 60 }; +} + +function frames(graph: BoxingGraph, fighter: FighterSnapshot, opponent: FighterSnapshot, count: number, from: number, reducedMotion = false): number { + let time = from; + for (let frame = 0; frame < count; frame += 1) { + time += 1 / 60; + graph.update(fighter, opponent, 1 / 60, time, reducedMotion, "full", time * 30); + } + return time; +} + +/** A particle body started from a fighter standing in guard, with the stance's own particle positions. */ +function bodyFromStance(): { body: RagdollBody; start: Float64Array; ragdoll: KnockoutRagdoll } { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + const ragdoll = new KnockoutRagdoll(boxer.rig, boxer.root); + const graph = new BoxingGraph(boxer, mapping); + frames(graph, facing(baseFighter("one")), opponentAt(), 12, 0); + const start = new Float64Array(PARTICLES * 3); + (ragdoll as unknown as { read(out: Float64Array): void }).read(start); + return { body: ragdoll.body, start, ragdoll }; +} + +const at = (positions: Float64Array, index: number): THREE.Vector3 => new THREE.Vector3(positions[index * 3]!, positions[index * 3 + 1]!, positions[index * 3 + 2]!); +const blow = (x: number, y: number, z: number, target: "head" | "body" = "head", drive: [number, number, number] = [0, 0, 0]): ImpulseRecord => ({ step: 0, x, y, z, driveX: drive[0], driveY: drive[1], driveZ: drive[2], target, twist: 0.3 }); + +describe("knockout physics", () => { + it("keeps every bone its length through a fall", () => { + const { body, start } = bodyFromStance(); + body.start(start, new Float64Array(PARTICLES * 3), "timber"); + body.impulse(blow(0, 2.6, -2.1, "head", [0, 0.3, -1])); + const bones: [number, number][] = [[P.hipL, P.kneeL], [P.kneeL, P.ankleL], [P.hipR, P.kneeR], [P.kneeR, P.ankleR], [P.shoulderL, P.elbowL], [P.elbowL, P.wristL], [P.shoulderR, P.elbowR], [P.elbowR, P.wristR], [P.neck, P.head], [P.pelvis, P.chest], [P.chest, P.upper]]; + const lengths = bones.map(([i, j]) => at(start, i).distanceTo(at(start, j))); + let worst = 0; + for (let step = 0; step < 360; step += 1) { + body.step(); + for (const [index, [i, j]] of bones.entries()) worst = Math.max(worst, Math.abs(at(body.position, i).distanceTo(at(body.position, j)) / lengths[index]! - 1)); + } + expect(worst).toBeLessThan(0.03); + }); + + it("bends knees and elbows only the way they bend", () => { + const { body, start } = bodyFromStance(); + body.start(start, new Float64Array(PARTICLES * 3), "crumple"); + body.impulse(blow(2.2, 0.2, -0.8, "head", [0.5, 0, 0.6])); + const axis = new THREE.Vector3(); + let least = Infinity; + let most = -Infinity; + for (let step = 0; step < 360; step += 1) { + body.step(); + for (const side of [0, 1] as const) { + for (const [root, joint, end, hinge] of [ + [side === 0 ? P.hipL : P.hipR, side === 0 ? P.kneeL : P.kneeR, side === 0 ? P.ankleL : P.ankleR, body.kneeAxis(side, body.position, axis).clone()], + [side === 0 ? P.shoulderL : P.shoulderR, side === 0 ? P.elbowL : P.elbowR, side === 0 ? P.wristL : P.wristR, body.elbowAxis(side, body.position, axis).clone()], + ] as const) { + const upper = at(body.position, joint).sub(at(body.position, root)).normalize(); + const lower = at(body.position, end).sub(at(body.position, joint)).normalize(); + const inPlane = lower.clone().addScaledVector(hinge, -lower.dot(hinge)); + if (inPlane.length() < 0.2) continue; + inPlane.normalize(); + const flex = Math.atan2(new THREE.Vector3().crossVectors(upper, inPlane).dot(hinge), upper.dot(inPlane)); + least = Math.min(least, flex); + most = Math.max(most, flex); + } + } + } + // A little past straight is allowed, and the joint softly corrects; never bent backwards. + expect(least).toBeGreaterThan(-0.35); + expect(most).toBeLessThan(2.7); + }); + + it("comes to rest on the canvas inside the ring", () => { + const { body, start } = bodyFromStance(); + body.start(start, new Float64Array(PARTICLES * 3), "crumple"); + body.impulse(blow(0, 0.3, -2.8, "head", [0, 0, -1.2])); + body.settle(8); + expect(body.asleep).toBe(true); + for (let i = 0; i < PARTICLES; i += 1) { + expect(body.position[i * 3 + 1]!).toBeGreaterThanOrEqual(RADIUS[i]! - 0.001); + expect(Math.abs(body.position[i * 3]!)).toBeLessThan(RING_FIGHT_HALF + 0.5); + expect(Math.abs(body.position[i * 3 + 2]!)).toBeLessThan(RING_FIGHT_HALF + 0.5); + } + // Lying down: the pelvis and the chest are on the canvas, not propped up. + expect(body.position[P.pelvis * 3 + 1]!).toBeLessThan(0.25); + expect(body.position[P.upper * 3 + 1]!).toBeLessThan(0.3); + }); + + it("comes to rest whatever the style and the blow", () => { + for (const style of ["timber", "crumple", "sag", "fold"] as const) { + for (const [x, y, z, target] of [[0, 0, 0, "head"], [0, 2.4, -2, "head"], [2.6, 0.2, -0.8, "head"], [0.4, 0, -1.8, "body"]] as const) { + const { body, start } = bodyFromStance(); + body.start(start, new Float64Array(PARTICLES * 3), style); + body.impulse(blow(x, y, z, target, [x * 0.2, 0, z * 0.3])); + body.settle(8); + expect(body.asleep, `${style} ${x},${y},${z} ${target}`).toBe(true); + expect(body.position[P.pelvis * 3 + 1]!).toBeLessThan(0.45); + } + } + }); + + it("stops a fighter against the ropes instead of letting him fall out of the ring", () => { + const { body, start } = bodyFromStance(); + // Move the whole stance next to the ropes on the +x side and drive him into them. + const shifted = Float64Array.from(start); + for (let i = 0; i < PARTICLES; i += 1) shifted[i * 3] = shifted[i * 3]! + ROPE_LINE - 0.45; + body.start(shifted, new Float64Array(PARTICLES * 3), "timber"); + body.impulse(blow(3, 0.2, 0, "head", [1.4, 0, 0])); + body.settle(8); + for (const i of [P.pelvis, P.hipL, P.hipR, P.belly, P.chest, P.upper]) expect(body.position[i * 3]!).toBeLessThan(ROPE_LINE); + }); + + it("falls the same way every time from the same start", () => { + const run = (): Float64Array => { + const { body, start } = bodyFromStance(); + body.start(start, new Float64Array(PARTICLES * 3), "sag"); + body.impulse(blow(-1.8, 0.4, -1.4, "head", [-0.4, 0, 0.3])); + for (let step = 0; step < 240; step += 1) body.step(); + return Float64Array.from(body.position); + }; + expect(run()).toEqual(run()); + }); + + it("takes a blow at the step it was recorded when the fall is run again", () => { + const { body, start } = bodyFromStance(); + body.start(start, new Float64Array(PARTICLES * 3), "crumple"); + for (let step = 0; step < 3; step += 1) body.step(); + const recorded = body.impulse(blow(0, 1.5, -2, "head", [0, 0, -0.8])); + expect(recorded.step).toBe(3); + for (let step = 0; step < 200; step += 1) body.step(); + const live = Float64Array.from(body.position); + body.start(start, new Float64Array(PARTICLES * 3), "crumple"); + for (let step = 0; step < 203; step += 1) body.step([recorded]); + expect(Float64Array.from(body.position)).toEqual(live); + }); + + it("drives the body the way the blow lands", () => { + const straight = blowImpulse({ target: "head", punchClass: "straight", hand: "right", lateral: -1, amount: 120 }); + expect(straight.z).toBeLessThan(0); + expect(straight.driveZ).toBeLessThan(0); + const uppercut = blowImpulse({ target: "head", punchClass: "uppercut", hand: "right", lateral: -1, amount: 120 }); + expect(uppercut.y).toBeGreaterThan(Math.abs(uppercut.x)); + expect(uppercut.driveZ).toBeLessThan(0); + for (const lateral of [1, -1]) { + const hook = blowImpulse({ target: "head", punchClass: "hook", hand: lateral > 0 ? "left" : "right", lateral, amount: 120 }); + expect(Math.sign(hook.x)).toBe(lateral); + expect(Math.sign(hook.driveX)).toBe(lateral); + } + const body = blowImpulse({ target: "body", punchClass: "hook", hand: "left", lateral: 1, amount: 120 }); + // A body shot drives the belly back and the rest of him forward over it: he folds. + expect(body.z).toBeLessThan(0); + expect(body.driveZ).toBeGreaterThan(0); + // Harder blows hit harder, up to a limit. + const soft = blowImpulse({ target: "head", punchClass: "straight", hand: "right", lateral: -1, amount: 40 }); + expect(Math.abs(soft.z)).toBeLessThan(Math.abs(straight.z)); + expect(Math.abs(blowImpulse({ target: "head", punchClass: "straight", hand: "right", lateral: -1, amount: 5000 }).z)).toBeLessThan(3.5); + }); + + it("doubles him over a body shot onto his knees, where a crumple drops him in a heap", () => { + // fold had crumple's stiffness, so a body shot that dropped him played the same fall as any other. + const knees = (style: "fold" | "crumple"): number => { + const { body, start } = bodyFromStance(); + body.start(start, new Float64Array(PARTICLES * 3), style); + const shot = blowImpulse({ target: "body", punchClass: "hook", hand: "left", lateral: 1, amount: 120 }); + // The fighter faces -z: his left is -x, the puncher is toward -z. + body.impulse({ step: 0, x: -shot.x, y: shot.y, z: -shot.z, driveX: -shot.driveX, driveY: shot.driveY, driveZ: -shot.driveZ, target: "body", twist: 0 }); + for (let step = 0; step < 36; step += 1) body.step(); + return Math.max(body.position[P.kneeL * 3 + 1]!, body.position[P.kneeR * 3 + 1]!); + }; + // A third of a second in, the folded fighter is down on one knee with the other still up under him, while the + // crumpled one's legs have gone from under him. + expect(knees("fold")).toBeGreaterThan(knees("crumple") + 0.08); + }); + + it("chooses how he goes down from the blow", () => { + expect(fallStyleFor("hook", "body", 200, 1)).toBe("fold"); + expect(fallStyleFor("straight", "head", 60, 1)).toBe("sag"); + for (let seed = 0; seed < 6; seed += 1) { + expect(["timber", "crumple"]).toContain(fallStyleFor("uppercut", "head", 200, seed)); + expect(fallStyleFor("hook", "head", 200, seed)).toBe(fallStyleFor("hook", "head", 200, seed)); + } + }); + + it("builds nothing while it steps or draws", () => { + const source = readFileSync("src/render/ragdoll.ts", "utf8"); + const hot = ["step", "carryHinges", "solveRigid", "solveRanges", "solveKnees", "solveTwist", "spinInertia", "solveElbows", "solveNeck", "solveFeet", "footNormal", "cone", "rotateAbout", "hinge", "satisfy", "hingeAxis", "solveArmsAgainstTorso", "closestOnSegment", "solveEnvironment", "applyFriction", "limitSpeed", "interpolate", "keepRest", "restore", "replayLosses", "runAhead", "followPose", "drive", "tiltHips", "prepareFrames", "segment", "frameFor", "blend", "elbowAxis", "kneeAxis"]; + for (const name of hot) { + const start = source.search(new RegExp(`\\n (private )?(get )?${name}\\(`)); + expect(start, name).toBeGreaterThan(0); + const end = source.indexOf("\n }\n", start); + const body = source.slice(start, end); + // Constructors, iterator helpers, spreads, closures and array literals all allocate. + expect(body, name).not.toMatch(/new |\.entries\(\)|\.map\(|\.slice\(|\.\.\.|of \[|=> |[=(,:]\s*\[\s*P\./); + } + }); +}); + +describe("knockouts on the fighter", () => { + it("falls under physics from the blow and lies where the fall ends", () => { + const { boxer, graph, fighter, opponent, time } = standing(); + graph.react("hit", "head", 1, "uppercut", "right", 420); + frames(graph, { ...fighter, is_downed: true }, opponent, 6, time); + expect(graph.fallBody).not.toBeNull(); + frames(graph, { ...fighter, is_downed: true }, opponent, 240, time + 0.1); + const head = worldPosition(boxer.rig.bones.head, new THREE.Vector3()); + const hips = worldPosition(boxer.rig.bones.hips, new THREE.Vector3()); + expect(head.y).toBeLessThan(0.4); + expect(hips.y).toBeLessThan(0.3); + // The skeleton follows the particles: the head bone sits on the head particle. + const particle = graph.fallBody!.bodyPoint(0, new THREE.Vector3()); + expect(head.distanceTo(particle)).toBeLessThan(0.06); + }); + + it("turns the body into the fall from its first frame instead of holding the pose and then snapping", () => { + // The blend-in from the standing pose used to slerp each bone into itself, so the bones held that pose for the + // whole blend and then snapped: the chest turned 15 degrees and the head jumped 21 cm in one frame. + for (const punchClass of ["straight", "hook"] as const) { + const { boxer, graph, fighter, opponent, time } = standing(); + const bones = boxer.rig.bones; + graph.react("hit", "head", 1, punchClass, "right", 120); + let now = time; + let chest = worldQuaternion(bones.chest, new THREE.Quaternion()); + let head = worldPosition(bones.head, new THREE.Vector3()); + let early = 0; + let turn = 0; + let move = 0; + for (let frame = 1; frame <= 12; frame += 1) { + now = frames(graph, { ...fighter, is_downed: true }, opponent, 1, now); + const turned = worldQuaternion(bones.chest, new THREE.Quaternion()); + const moved = worldPosition(bones.head, new THREE.Vector3()); + const angle = THREE.MathUtils.radToDeg(turned.angleTo(chest)); + if (frame <= 5) early += angle; + turn = Math.max(turn, angle); + move = Math.max(move, moved.distanceTo(head)); + chest = turned; + head = moved; + } + expect(early, punchClass).toBeGreaterThan(2); + expect(turn, punchClass).toBeLessThan(8); + expect(move, punchClass).toBeLessThan(0.12); + } + }, 30_000); + + it("brings the head back onto the neck when the fall starts with it thrown off by a slip or a blow", () => { + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + const ragdoll = new KnockoutRagdoll(boxer.rig, boxer.root); + boxer.rig.resetToRest(); + boxer.root.updateMatrixWorld(true); + const bones = boxer.rig.bones; + const onTheNeck = bones.head.position.clone(); + // The poser moves the head itself for slips and big blows; the fall takes over from that frame. + bones.head.position.add(new THREE.Vector3(-4.2, -0.9, -7.4)); + boxer.root.updateMatrixWorld(true); + expect(ragdoll.start("crumple")).toBe(true); + for (let frame = 0; frame < 150; frame += 1) ragdoll.update(1 / 60, null); + const where = bones.neck.localToWorld(onTheNeck.clone()); + expect(worldPosition(bones.head, new THREE.Vector3()).distanceTo(where)).toBeLessThan(0.001); + }); + + it("keeps the head on the neck, the belly its length and the skull on the canvas however the fall starts", () => { + // Falls that start mid-slip or mid-weave, where the poser has moved the head off the neck. The physics' neck used to + // keep that stretched length (15-21 cm against 10.7) and the skull ended up to 14.5 cm under the canvas, the head + // bone up to 16 cm off its particle; pinning the chest to its particle then stretched the belly by up to 16 cm. + const aboveNeckCut = (bind: THREE.Vector3): boolean => bind.y > NECK_CUT_HEIGHT - NECK_CUT_SLOPE * (bind.z - NECK_CUT_DEPTH) - 0.3; + const vertex = new THREE.Vector3(); + const rest = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + rest.rig.resetToRest(); + rest.root.updateMatrixWorld(true); + const neck = worldPosition(rest.rig.bones.neck, new THREE.Vector3()).distanceTo(worldPosition(rest.rig.bones.head, vertex)); + const spine = worldPosition(rest.rig.bones.spine, new THREE.Vector3()).distanceTo(worldPosition(rest.rig.bones.chest, vertex)); + for (const defense of ["slip_left", "slip_right", "weave"] as const) { + for (const punchClass of ["jab", "hook"] as const) { + const { boxer, graph, fighter, opponent, time } = standing(0, 0, defense); + const bones = boxer.rig.bones; + graph.react("hit", "head", 1, punchClass, "right", 110); + frames(graph, { ...fighter, is_downed: true, defense: "none" }, opponent, 300, time); + const body = graph.fallBody!.body; + const label = `${defense} ${punchClass}`; + expect(at(body.position, P.neck).distanceTo(at(body.position, P.head)), label).toBeCloseTo(neck, 2); + expect(worldPosition(bones.head, vertex).distanceTo(at(body.position, P.head)), label).toBeLessThan(0.01); + expect(worldPosition(bones.spine, new THREE.Vector3()).distanceTo(worldPosition(bones.chest, vertex)), label).toBeCloseTo(spine, 2); + const mesh = boxer.headMesh; + mesh.updateMatrixWorld(true); + const position = mesh.geometry.getAttribute("position"); + let lowest = Infinity; + for (let index = 0; index < position.count; index += 1) { + if (!aboveNeckCut(vertex.fromBufferAttribute(position, index))) continue; + lowest = Math.min(lowest, mesh.getVertexPosition(index, vertex).applyMatrix4(mesh.matrixWorld).y); + } + expect(lowest, label).toBeGreaterThan(-0.035); + } + } + }, 30_000); + + it("rolls the feet only as far as an ankle goes and never flicks them, in the air or on the canvas", () => { + // Nothing bounded a foot's roll onto its edge: feet lay 60-90 degrees on their sides against the shin in 20 of + // 21 falls, and the turn about the shin measured from a nearly degenerate projection (and only off the canvas) + // flicked toes 39 cm in a 1/120 s step and toe bones 47 cm in a frame. + let tilt = 0; + let rest = 0; + let stepMove = 0; + let frameMove = 0; + const tip = new THREE.Vector3(); + for (const punchClass of ["jab", "straight", "hook", "uppercut"] as const) { + for (const hand of ["left", "right"] as const) { + for (const amount of [60, 150]) { + const { boxer, graph, fighter, opponent, time } = standing(); + graph.react("hit", "head", 1, punchClass, hand, amount); + let now = frames(graph, { ...fighter, is_downed: true }, opponent, 12, time); + const body = graph.fallBody!.body; + const before = new Float64Array(PARTICLES * 3); + const step = body.step.bind(body); + const feet = (into: (value: number) => void): void => { + for (const [toe, heel, ankle, knee] of [[P.toeL, P.heelL, P.ankleL, P.kneeL], [P.toeR, P.heelR, P.ankleR, P.kneeR]] as const) { + const side = at(body.position, toe).sub(at(body.position, heel)).cross(at(body.position, ankle).sub(at(body.position, heel))).normalize(); + const shin = at(body.position, ankle).sub(at(body.position, knee)).normalize(); + into(THREE.MathUtils.radToDeg(Math.asin(Math.min(1, Math.abs(side.dot(shin)))))); + } + }; + body.step = (replay) => { + before.set(body.position); + step(replay); + feet((value) => { tilt = Math.max(tilt, value); }); + for (const toe of [P.toeL, P.toeR]) stepMove = Math.max(stepMove, at(body.position, toe).distanceTo(at(before, toe))); + }; + const toes = [boxer.rig.bones.toeL, boxer.rig.bones.toeR]; + const last = toes.map((bone) => worldPosition(bone, new THREE.Vector3())); + for (let frame = 0; frame < 228; frame += 1) { + now = frames(graph, { ...fighter, is_downed: true }, opponent, 1, now); + for (const [index, bone] of toes.entries()) { + frameMove = Math.max(frameMove, worldPosition(bone, tip).distanceTo(last[index]!)); + last[index]!.copy(tip); + } + } + feet((value) => { rest = Math.max(rest, value); }); + } + } + } + // A foot pinned under a falling leg can be held past its range for a few frames, but it lies within it. + expect(tilt).toBeLessThan(80); + expect(rest).toBeLessThan(40); + expect(stepMove).toBeLessThan(0.1); + expect(frameMove).toBeLessThan(0.2); + }, 30_000); + + it("lands the head on the canvas without the skull flipping or skidding across it", () => { + // The skull's lean on the neck was corrected by turning only the skull, into the canvas when the head lay on it; + // the canvas lifted it back out and the head skidded 11-30 cm a step, the head bone turning up to 95 degrees in a frame. + let crownStep = 0; + let headTurn = 0; + let skullRest = 0; + let carried = 0; + const turned = new THREE.Quaternion(); + for (const punchClass of ["jab", "straight", "hook", "uppercut"] as const) { + for (const hand of ["left", "right"] as const) { + for (const amount of [60, 105, 150]) { + const { boxer, graph, fighter, opponent, time } = standing(); + graph.react("hit", "head", 1, punchClass, hand, amount); + let now = frames(graph, { ...fighter, is_downed: true }, opponent, 12, time); + const body = graph.fallBody!.body; + const before = new Float64Array(PARTICLES * 3); + const step = body.step.bind(body); + body.step = (replay) => { + before.set(body.position); + step(replay); + crownStep = Math.max(crownStep, at(body.position, P.crown).distanceTo(at(before, P.crown))); + }; + let head = worldQuaternion(boxer.rig.bones.head, new THREE.Quaternion()); + for (let frame = 0; frame < 228; frame += 1) { + now = frames(graph, { ...fighter, is_downed: true }, opponent, 1, now); + worldQuaternion(boxer.rig.bones.head, turned); + headTurn = Math.max(headTurn, THREE.MathUtils.radToDeg(turned.angleTo(head))); + head.copy(turned); + } + const neck = at(body.position, P.head).sub(at(body.position, P.neck)).normalize(); + const skull = at(body.position, P.crown).sub(at(body.position, P.head)).normalize(); + skullRest = Math.max(skullRest, THREE.MathUtils.radToDeg(neck.angleTo(skull))); + carried = Math.max(carried, Math.hypot(body.position[P.pelvis * 3]!, body.position[P.pelvis * 3 + 2]!)); + } + } + } + expect(crownStep).toBeLessThan(0.1); + expect(headTurn).toBeLessThan(60); + // The skull leans on the neck no further than its 32 degrees, give or take a pass. + expect(skullRest).toBeLessThan(36); + // Bringing the head back never drags the body across the ring: a turn handed to the neck alone once slid it 2.3 m. + expect(carried).toBeLessThan(0.8); + }, 30_000); + + it("lies with his shoulders turned on his hips no further than a spine turns, however he went down", () => { + let worst = 0; + for (const punch of ["jab", "hook"] as const) { + for (const target of ["head", "body"] as const) { + const { graph, fighter, opponent, time } = standing(); + graph.react("hit", target, 1, punch, "left", 300); + frames(graph, { ...fighter, is_downed: true }, opponent, 240, time); + const p = graph.fallBody!.body.position; + const spine = at(p, P.upper).sub(at(p, P.pelvis)).normalize(); + const hips = at(p, P.hipL).sub(at(p, P.hipR)); + hips.addScaledVector(spine, -hips.dot(spine)); + const shoulders = at(p, P.shoulderL).sub(at(p, P.shoulderR)); + shoulders.addScaledVector(spine, -shoulders.dot(spine)); + worst = Math.max(worst, Math.abs(Math.atan2(new THREE.Vector3().crossVectors(hips, shoulders).dot(spine), hips.dot(shoulders)))); + } + } + expect(THREE.MathUtils.radToDeg(worst)).toBeLessThan(45); + }); + + it("falls at his place after a gap in drawing, already down if the count has started", () => { + // Last drawn standing at x = -1.1 m, the next frame drawn has him down at +0.91 m (the Activity was hidden, or a + // spectator joined mid-count). The fall used to start where he was last drawn, 1.9 m from his place: the referee + // counted over empty canvas and the get-up later slid him across the ring. + for (const count of [3, 0]) { + const { boxer, graph, opponent, time } = standing(-180, 0); + const downed = { ...facing(baseFighter("one"), 149, 0), is_downed: true, get_up_count: count }; + graph.react("hit", "head", 1, "straight", "right", 120); + frames(graph, downed, { ...opponent, x: 149 }, 1, time + 30); + // With the count running he is already down; otherwise he falls from there. + if (count > 0) expect(graph.fallBody!.body.asleep).toBe(true); + frames(graph, downed, { ...opponent, x: 149 }, 300, time + 30 + 1 / 60); + const pelvis = graph.fallBody!.pelvis(new THREE.Vector3()); + expect(boxer.root.position.x, `count ${count}`).toBeCloseTo(mapping.x(149), 2); + expect(Math.hypot(pelvis.x - boxer.root.position.x, pelvis.z - boxer.root.position.z), `count ${count}`).toBeLessThan(0.7); + } + }, 30_000); + + it("keeps the authored fall under reduced motion", () => { + const { boxer, graph, fighter, opponent, time } = standing(); + graph.react("hit", "head", 1, "straight", "right", 420); + frames(graph, { ...fighter, is_downed: true }, opponent, 70, time, true); + expect(graph.fallBody).toBeNull(); + expect(worldPosition(boxer.rig.bones.head, new THREE.Vector3()).y).toBeLessThan(0.45); + }); + + it("gets up from where the fall left him", () => { + const { boxer, graph, fighter, opponent, time } = standing(); + graph.react("hit", "head", 1, "hook", "left", 420); + let now = frames(graph, { ...fighter, is_downed: true }, opponent, 150, time); + const pelvis = graph.fallBody!.pelvis(new THREE.Vector3()); + now = frames(graph, fighter, opponent, 1, now); + // The physics carries the body into the get-up (until he is on all fours), from where it lay. + expect(graph.fallBody?.handingOver).toBe(true); + expect(worldPosition(boxer.rig.bones.hips, new THREE.Vector3()).distanceTo(pelvis)).toBeLessThan(0.08); + frames(graph, fighter, opponent, 130, now); + expect(graph.isDown).toBe(false); + expect(worldPosition(boxer.rig.bones.head, new THREE.Vector3()).y).toBeGreaterThan(1.45); + }); + + it("starts the get-up where the body lies, and steps back to his spot as he rises", () => { + const { boxer, graph, fighter, opponent, time } = standing(); + graph.react("hit", "head", 1, "straight", "right", 420); + let now = frames(graph, { ...fighter, is_downed: true }, opponent, 200, time); + const pelvis = graph.fallBody!.pelvis(new THREE.Vector3()); + // The fighter's spot moves well away from where his body lies (the rendered root follows it). + const moved = { ...fighter, x: fighter.x + 200 }; + now = frames(graph, { ...moved, is_downed: true }, opponent, 30, now); + expect(Math.hypot(boxer.root.position.x - pelvis.x, boxer.root.position.z - pelvis.z)).toBeGreaterThan(0.9); + // A third of the way into the get-up he is still rising where he fell, not at his spot. + now = frames(graph, moved, opponent, 30, now); + const hips = worldPosition(boxer.rig.bones.hips, new THREE.Vector3()); + expect(Math.hypot(hips.x - pelvis.x, hips.z - pelvis.z)).toBeLessThan(0.5); + frames(graph, moved, opponent, 120, now); + const standingHips = worldPosition(boxer.rig.bones.hips, new THREE.Vector3()); + expect(Math.hypot(standingHips.x - boxer.root.position.x, standingHips.z - boxer.root.position.z)).toBeLessThan(0.2); + }); + + it("is carried by the physics into the get-up and stands up on his place, smoothly and above the canvas", () => { + // Handed to the get-up at the first press, the body froze and then snapped 1.3 m; turning each bone from the fall's + // pose to the get-up's then swung limbs up to 0.85 m in a frame and 0.5 m through the canvas, knees flipped across + // the leg 0.6 m in a frame, and once up he walked back to his place for up to half a second. + const vertex = new THREE.Vector3(); + const lowest = (boxer: SkinnedBoxer): number => { + boxer.root.updateMatrixWorld(true); + let height = Infinity; + for (const mesh of [boxer.headMesh, boxer.gloveMesh("left"), boxer.gloveMesh("right")]) { + const count = mesh.geometry.getAttribute("position").count; + for (let index = 0; index < count; index += 2) height = Math.min(height, mesh.getVertexPosition(index, vertex).applyMatrix4(mesh.matrixWorld).y); + } + return height; + }; + let move = { distance: 0, at: "" }; + let under = { height: Infinity, at: "" }; + let away = 0; + for (const defense of ["guard_high", "slip_left"] as const) { + for (const punchClass of ["jab", "straight", "hook", "uppercut"] as const) { + for (const hand of ["left", "right"] as const) { + const { boxer, graph, fighter, opponent, time } = standing(0, 0, defense); + const ready = { ...fighter, get_up_required: 66 }; + graph.react("hit", "head", 1, punchClass, hand, 110); + let now = frames(graph, { ...ready, is_downed: true, defense: "none" }, opponent, 160, time); + const names = Object.keys(boxer.rig.bones) as (keyof typeof boxer.rig.bones)[]; + let last = names.map((name) => worldPosition(boxer.rig.bones[name], new THREE.Vector3())); + let frame = 0; + for (const [stage, state, count] of [ + ["first press", { ...ready, is_downed: true, defense: "none" as const, get_up_meter: 22 }, 30], + ["second press", { ...ready, is_downed: true, defense: "none" as const, get_up_meter: 44 }, 30], + ["up", { ...ready, get_up_meter: 66, stunned_ticks: 20 }, 40], + ] as const) { + for (let index = 0; index < count; index += 1) { + now = frames(graph, state, opponent, 1, now); + const label = `${defense} ${punchClass} ${hand} ${stage} frame ${index}`; + const here = names.map((name) => worldPosition(boxer.rig.bones[name], new THREE.Vector3())); + for (const [bone, position] of here.entries()) { + const distance = position.distanceTo(last[bone]!); + if (distance > move.distance) move = { distance, at: `${label} ${names[bone]}` }; + } + last = here; + if (frame % 3 === 0) { + const height = lowest(boxer); + if (height < under.height) under = { height, at: label }; + } + frame += 1; + } + } + away = Math.max(away, Math.hypot(boxer.root.position.x - mapping.x(0), boxer.root.position.z - mapping.z(0))); + } + } + } + expect(move.distance, move.at).toBeLessThan(0.16); + // A glove's padding reaches a few centimetres past the particles it lies on; no limb swings through the canvas. + expect(under.height, under.at).toBeGreaterThan(-0.06); + // Up off his knee he is on his place: the opponent's punches aim there. + expect(away).toBeLessThan(0.01); + }, 60_000); + + it("draws the skin and kit a body lying on the canvas reaches under it pressed flat on the canvas", () => { + // The body lies on spheres at its joints, which the calves, the loose trunks and the gloves' padding reach past: + // drawn as the skeleton put them, they went 5-10 cm under the canvas. + const { boxer } = standing(); + const materials = new Set(); + boxer.root.traverse((object) => { + if (object instanceof THREE.SkinnedMesh) materials.add(object.material as THREE.MeshStandardMaterial); + }); + expect(materials.size).toBe(5); + for (const material of materials) { + const shader = { uniforms: {}, vertexShader: THREE.ShaderLib.physical.vertexShader, fragmentShader: THREE.ShaderLib.physical.fragmentShader }; + material.onBeforeCompile(shader as unknown as THREE.WebGLProgramParametersWithUniforms, null as unknown as THREE.WebGLRenderer); + // Once, on the vertex as posed: after the skeleton and every other patch move it, just before it is projected. + const press = shader.vertexShader.indexOf(PRESS_ONTO_CANVAS_GLSL); + expect(press, material.name).toBeGreaterThan(shader.vertexShader.indexOf("#include ")); + expect(shader.vertexShader.indexOf(PRESS_ONTO_CANVAS_GLSL, press + 1), material.name).toBe(-1); + expect(shader.vertexShader.slice(press + PRESS_ONTO_CANVAS_GLSL.length).trimStart().startsWith("#include "), material.name).toBe(true); + // A program of its own, not one cached for the same material without it. + expect(material.customProgramCacheKey(), material.name).toContain("on-canvas"); + } + }); + + it("runs the recorded fall again for the replay and ends where the live fall ended", () => { + const { graph, fighter, opponent, time } = standing(); + graph.react("hit", "head", 1, "straight", "right", 420); + let now = frames(graph, { ...fighter, is_downed: true }, opponent, 400, time); + const live = Float64Array.from(graph.fallBody!.body.position); + graph.resetTransient(false); + graph.primeReplayFall(); + now = frames(graph, fighter, opponent, 20, now); + expect(graph.fallBody).toBeNull(); + // The replayed blow arrives as the live one did; the recorded fall already holds it. + graph.react("hit", "head", 1, "straight", "right", 420); + frames(graph, { ...fighter, is_downed: true }, opponent, 400, now); + expect(Float64Array.from(graph.fallBody!.body.position)).toEqual(live); + }); + + it("replays a knockout that loses its head in the replay the way the count showed it", () => { + // A knockout by the count loses the head only at the replay's impact. The replay used to fall with the severed + // head's weightless particles and ended 4-18 cm off at the pelvis and up to 1.3 m off at a glove or a toe. + for (const punchClass of ["straight", "hook", "uppercut"] as const) { + const { boxer, graph, fighter, opponent, time } = standing(); + graph.react("hit", "head", 1, punchClass, "right", 120); + let now = frames(graph, { ...fighter, is_downed: true }, opponent, 400, time); + const live = Float64Array.from(graph.fallBody!.body.position); + graph.resetTransient(false); + graph.primeReplayFall(); + now = frames(graph, fighter, opponent, 20, now); + graph.react("hit", "head", 1, punchClass, "right", 120); + boxer.setDecapitated(true); + frames(graph, { ...fighter, is_downed: true }, opponent, 400, now); + expect(Float64Array.from(graph.fallBody!.body.position), punchClass).toEqual(live); + graph.resetTransient(true); + expect(Float64Array.from(graph.fallBody!.body.position), punchClass).toEqual(live); + } + }, 30_000); + + it("puts the body where the fall ends without running it when the replay cuts back to live", () => { + // settle() ran the rest of the fall in the frame of the cut to the close-up (up to 280 steps, about 20 ms on a + // desktop and several times that on a phone) and restoreSettled() ran all of it again. + const replay = (graph: BoxingGraph, fighter: FighterSnapshot, opponent: FighterSnapshot, from: number, count: number): number => { + let time = from; + for (let frame = 0; frame < count; frame += 1) { + time += 1 / 60; + graph.update({ ...fighter, is_downed: true }, opponent, 0.4 / 60, time, false, "full", time * 30); + } + return time; + }; + for (const liveFrames of [400, 18]) { + // Counted out, the live fall has come to rest; a flash knockout's replay starts before it has. + const { graph, fighter, opponent, time } = standing(); + graph.react("hit", "head", 1, "hook", "left", 120); + let now = frames(graph, { ...fighter, is_downed: true }, opponent, liveFrames, time); + graph.resetTransient(false); + graph.primeReplayFall(); + now = frames(graph, fighter, opponent, 20, now); + graph.react("hit", "head", 1, "hook", "left", 120); + // The slow-motion replay of the punch and the first second of the fall. + replay(graph, fighter, opponent, now, liveFrames === 400 ? 15 : 160); + const body = graph.fallBody!.body; + let steps = 0; + const step = body.step.bind(body); + body.step = (impulses) => { + steps += 1; + step(impulses); + }; + graph.resetTransient(true); + const settled = Float64Array.from(body.position); + graph.resetTransient(false); + graph.resetTransient(true); + expect(steps, `${liveFrames}`).toBe(0); + expect(body.asleep, `${liveFrames}`).toBe(true); + expect(Float64Array.from(body.position), `${liveFrames}`).toEqual(settled); + } + }, 30_000); + + it("settles the recorded fall at once when the replay cuts back to live", () => { + const { graph, fighter, opponent, time } = standing(); + graph.react("hit", "head", 1, "uppercut", "left", 420); + const now = frames(graph, { ...fighter, is_downed: true }, opponent, 400, time); + const live = Float64Array.from(graph.fallBody!.body.position); + graph.resetTransient(false); + graph.resetTransient(true); + expect(graph.fallBody).not.toBeNull(); + expect(Float64Array.from(graph.fallBody!.body.position)).toEqual(live); + frames(graph, { ...fighter, is_downed: true }, opponent, 2, now); + expect(graph.fallBody!.body.position[P.pelvis * 3 + 1]!).toBeLessThan(0.3); + }); + + it("leaves a body shot that takes him to one knee to the animation, with nothing for the replay to run again", () => { + const { graph, fighter, opponent, time } = standing(); + // An earlier knockdown under physics leaves a recorded fall behind. + graph.react("hit", "head", 1, "straight", "right", 420); + let now = frames(graph, { ...fighter, is_downed: true }, opponent, 120, time); + now = frames(graph, fighter, opponent, 120, now); + expect(graph.isDown).toBe(false); + graph.useAuthoredFall(); + graph.react("hit", "body", 1, "hook", "left", 420); + now = frames(graph, { ...fighter, is_downed: true }, opponent, 60, now); + expect(graph.isDown).toBe(true); + expect(graph.fallBody).toBeNull(); + // The replay cuts back to him lying where the animation put him, not to the earlier fall. + graph.resetTransient(false); + graph.primeReplayFall(); + graph.useAuthoredFall(); + now = frames(graph, { ...fighter, is_downed: true }, opponent, 30, now); + expect(graph.fallBody).toBeNull(); + graph.resetTransient(true); + expect(graph.fallBody).toBeNull(); + // The next knockdown is physics again. + now = frames(graph, fighter, opponent, 130, now); + frames(graph, { ...fighter, is_downed: true }, opponent, 3, now); + expect(graph.fallBody).not.toBeNull(); + }); + + it("frames the close-up of a fallen head from beyond the head, not across the body", () => { + // Head at the origin, the body stretched out toward +x; with no blockers the shot would come from +x. + const fallback = Math.PI / 2; + expect(closeUpAngle(0, 0, null, 1.05, 2.2, fallback)).toBe(fallback); + const body = [0.45, 0.8, 1.2].map((x) => ({ x, z: 0, radius: 0.28 })); + const side = closeUpAngle(0, 0, null, 1.05, 2.2, fallback, body); + expect(Math.sin(side)).toBeLessThan(0.5); + }); + + it("goes down for a flash knockout although the engine never counts him", () => { + const { boxer, graph, fighter, opponent, time } = standing(); + graph.react("hit", "head", 1, "hook", "right", 420); + graph.knockOut(); + frames(graph, fighter, opponent, 150, time); + expect(graph.isDown).toBe(true); + expect(worldPosition(boxer.rig.bones.head, new THREE.Vector3()).y).toBeLessThan(0.45); + graph.resetTransient(false); + frames(graph, fighter, opponent, 3, time + 3); + expect(graph.isDown).toBe(false); + }); + + it("takes a blow presented just after the fall began, but not one long after", () => { + const { graph, fighter, opponent, time } = standing(); + let now = frames(graph, { ...fighter, is_downed: true }, opponent, 2, time); + const before = Float64Array.from(graph.fallBody!.body.position); + graph.react("hit", "head", 1, "straight", "right", 420); + now = frames(graph, { ...fighter, is_downed: true }, opponent, 20, now); + const record = graph.fallBody!.record!; + expect(record.impulses).toHaveLength(1); + void before; + frames(graph, { ...fighter, is_downed: true }, opponent, 30, now); + graph.react("hit", "head", 1, "straight", "right", 420); + expect(graph.fallBody!.record!.impulses).toHaveLength(1); + }); + + it("comes to rest within two and a half seconds of the punch, with or without the opponent over him", () => { + for (const obstacle of [false, true]) { + for (const punch of ["jab", "straight", "hook", "uppercut"] as const) { + for (const hand of ["left", "right"] as const) { + for (const target of ["head", "body"] as const) { + for (const amount of [140, 420]) { + const { graph, fighter, opponent, time } = standing(); + if (obstacle) graph.setObstacle(0, mapping.z(-100)); + graph.react("hit", target, 1, punch, hand, amount); + frames(graph, { ...fighter, is_downed: true }, opponent, 150, time); + expect(graph.fallBody?.body.asleep, `${punch} ${hand} ${target} ${amount}${obstacle ? " with the opponent over him" : ""}`).toBe(true); + } + } + } + } + } + }, 30_000); + + it("replays the fall exactly when the opponent walks off another way", () => { + const fallWith = (graph: BoxingGraph, fighter: FighterSnapshot, opponent: FighterSnapshot, from: number, walk: (frame: number) => number): number => { + let time = from; + for (let frame = 0; frame < 240; frame += 1) { + graph.setObstacle(0, mapping.z(-100) + walk(frame)); + time += 1 / 60; + graph.update({ ...fighter, is_downed: true }, opponent, 1 / 60, time, false, "full", time * 30); + } + return time; + }; + const { graph, fighter, opponent, time } = standing(); + graph.react("hit", "body", 1, "hook", "right", 140); + let now = fallWith(graph, fighter, opponent, time, (frame) => -frame * 0.012); + const live = Float64Array.from(graph.fallBody!.body.position); + graph.resetTransient(false); + graph.primeReplayFall(); + now = frames(graph, fighter, opponent, 20, now); + graph.react("hit", "body", 1, "hook", "right", 140); + fallWith(graph, fighter, opponent, now, () => 0); + expect(Float64Array.from(graph.fallBody!.body.position)).toEqual(live); + }); + + it("does not fall through the opponent standing over him", () => { + const { graph, fighter, opponent, time } = standing(); + // The opponent stands right in front; a body shot folds him forward toward that spot. + const ahead = new THREE.Vector3(0, 0, mapping.z(-150)); + graph.setObstacle(ahead.x, ahead.z); + graph.react("hit", "body", 1, "hook", "left", 420); + frames(graph, { ...fighter, is_downed: true }, opponent, 300, time); + const body = graph.fallBody!.body; + for (let i = 0; i < PARTICLES; i += 1) { + if (body.position[i * 3 + 1]! > 1.75) continue; + const distance = Math.hypot(body.position[i * 3]! - ahead.x, body.position[i * 3 + 2]! - ahead.z); + expect(distance).toBeGreaterThan(0.24 + RADIUS[i]! - 0.02); + } + }); +}); diff --git a/web/hands/src/render/ragdoll.ts b/web/hands/src/render/ragdoll.ts new file mode 100644 index 00000000..0f74c143 --- /dev/null +++ b/web/hands/src/render/ragdoll.ts @@ -0,0 +1,1913 @@ +import * as THREE from "three"; +import { frameFromAxes, type CanonicalBone, type SolvedRig } from "./rig"; +import { RING_FIGHT_HALF, ROPE_HEIGHTS, ROPE_LINE } from "./world"; +import type { Hand, PunchClass, Target } from "../types"; + +/** + * Knockout physics: a Verlet particle body over the fighter's skeleton. It starts from the posed + * skeleton with its recent velocities, takes the punch as an impulse along the punch line, falls + * under gravity against the canvas, the ropes and the posts, and is mapped back onto the bones. + * World space, metres, seconds; every step has the same fixed length, so a given start always + * produces the same fall, which is what lets the knockout replay show the fall the players saw. + */ + +export const P = { + pelvis: 0, hipL: 1, hipR: 2, belly: 3, chest: 4, upper: 5, neck: 6, shoulderL: 7, shoulderR: 8, + head: 9, crown: 10, face: 11, + elbowL: 12, wristL: 13, fistL: 14, elbowR: 15, wristR: 16, fistR: 17, + kneeL: 18, ankleL: 19, toeL: 20, heelL: 21, kneeR: 22, ankleR: 23, toeR: 24, heelR: 25, +} as const; +export const PARTICLES = 26; + +const MASS = [9, 4, 4, 2, 10, 7, 2, 3.5, 3.5, 2.5, 1.5, 1, 1.6, 1, 1, 1.6, 1, 1, 3.6, 1.6, 0.4, 0.6, 3.6, 1.6, 0.4, 0.6]; +export const RADIUS = [0.12, 0.09, 0.09, 0.11, 0.14, 0.13, 0.06, 0.07, 0.07, 0.09, 0.085, 0.055, 0.05, 0.065, 0.08, 0.05, 0.065, 0.08, 0.06, 0.05, 0.035, 0.04, 0.06, 0.05, 0.035, 0.04]; + +/** Helpers fixed to a bone, in metres along the bone's own axes. */ +const BELLY_FORWARD = 0.12; +const CROWN_UP = 0.16; +const FACE_FORWARD = 0.09; +const FACE_UP = 0.06; +const FIST_REACH = 0.11; +const HEEL_BACK = 0.07; +const HEEL_DOWN = 0.09; + +const RIGID: readonly (readonly [number, number])[] = [ + [P.pelvis, P.hipL], [P.pelvis, P.hipR], [P.hipL, P.hipR], [P.belly, P.hipL], [P.belly, P.hipR], [P.belly, P.pelvis], + [P.pelvis, P.chest], [P.chest, P.upper], + [P.upper, P.neck], [P.upper, P.shoulderL], [P.upper, P.shoulderR], [P.neck, P.shoulderL], [P.neck, P.shoulderR], [P.shoulderL, P.shoulderR], + [P.neck, P.head], [P.head, P.crown], [P.head, P.face], [P.crown, P.face], + [P.shoulderL, P.elbowL], [P.elbowL, P.wristL], [P.wristL, P.fistL], + [P.shoulderR, P.elbowR], [P.elbowR, P.wristR], [P.wristR, P.fistR], + [P.hipL, P.kneeL], [P.kneeL, P.ankleL], [P.ankleL, P.toeL], [P.ankleL, P.heelL], [P.toeL, P.heelL], + [P.hipR, P.kneeR], [P.kneeR, P.ankleR], [P.ankleR, P.toeR], [P.ankleR, P.heelR], [P.toeR, P.heelR], +]; + +/** Joint ranges as shares of the distance in the rest pose: the spine and neck bend but do not stretch. */ +const RANGES: readonly (readonly [number, number, number, number])[] = [ + [P.hipL, P.shoulderL, 0.8, 1.05], [P.hipR, P.shoulderR, 0.8, 1.05], + [P.hipL, P.shoulderR, 0.84, 1.08], [P.hipR, P.shoulderL, 0.84, 1.08], + [P.belly, P.upper, 0.7, 1.06], [P.pelvis, P.upper, 0.86, 1.0], [P.hipL, P.chest, 0.84, 1.08], [P.hipR, P.chest, 0.84, 1.08], + // The head turns no further than this; the neck cones bound its nod and tilt. + [P.shoulderL, P.face, 0.76, 1.2], [P.shoulderR, P.face, 0.76, 1.2], + // Thighs swing forward freely, outward and back a little, and do not cross far. + [P.kneeL, P.hipR, 0.8, 1.3], [P.kneeR, P.hipL, 0.8, 1.3], [P.kneeL, P.belly, 0.5, 1.12], [P.kneeR, P.belly, 0.5, 1.12], +]; + +/** The first ranges hold the spine; a stiff body keeps them near where they started. */ +const SPINE_RANGES = 8; + +/** Absolute minimum distances: limbs do not pass through each other or fold completely. */ +const MINIMA: readonly (readonly [number, number, number])[] = [ + [P.kneeL, P.kneeR, 0.13], [P.ankleL, P.ankleR, 0.11], [P.kneeL, P.ankleR, 0.1], [P.kneeR, P.ankleL, 0.1], + [P.hipL, P.ankleL, 0.2], [P.hipR, P.ankleR, 0.2], [P.shoulderL, P.wristL, 0.14], [P.shoulderR, P.wristR, 0.14], + [P.face, P.chest, 0.2], [P.crown, P.chest, 0.24], [P.kneeL, P.upper, 0.3], [P.kneeR, P.upper, 0.3], +]; + +const ARM_PARTS = [P.elbowL, P.wristL, P.fistL, P.elbowR, P.wristR, P.fistR] as const; +/** How much of a blow's carry each part takes: the upper body most, the feet nothing (they stay planted). */ +const DRIVE_SHARE = [0.7, 0.6, 0.6, 0.7, 0.9, 1, 1, 1, 1, 1, 1, 1, 0.9, 0.8, 0.8, 0.9, 0.8, 0.8, 0.35, 0.1, 0, 0, 0.35, 0.1, 0, 0]; +const TORSO_CAPSULES: readonly (readonly [number, number, number])[] = [[P.pelvis, P.chest, 0.12], [P.chest, P.upper, 0.13]]; + +/** The same tables flattened, so the step reads them without building anything. */ +const RIGID_PAIRS = Int32Array.from(RIGID.flat()); +const RANGE_PAIRS = Int32Array.from(RANGES.flatMap(([i, j]) => [i, j])); +const MINIMUM_PAIRS = Int32Array.from(MINIMA.flatMap(([i, j]) => [i, j])); +const MINIMUM_DISTANCES = Float64Array.from(MINIMA.map(([, , distance]) => distance)); +const CAPSULES = Int32Array.from(TORSO_CAPSULES.flatMap(([from, to]) => [from, to])); +const CAPSULE_RADII = Float64Array.from(TORSO_CAPSULES.map(([, , radius]) => radius)); +const HEAD_SHARES = Float64Array.from([P.head, 1, P.crown, 0.9, P.face, 1, P.neck, 0.45]); +const BODY_SHARES = Float64Array.from([P.belly, 1, P.chest, 0.6, P.pelvis, 0.4, P.hipL, 0.3, P.hipR, 0.3]); +const HEAD_PARTS = [P.head, P.crown, P.face] as const; +const SKULL_PARTS = [P.crown, P.face] as const; +const FOOT_PARTS = [[P.toeL, P.heelL], [P.toeR, P.heelR]] as const; +const HEEL_PARTS = [[P.heelL], [P.heelR]] as const; +const LOST_HEAD = [P.head, P.crown, P.face] as const; +const LOST_LEFT_HAND = [P.fistL] as const; +const LOST_RIGHT_HAND = [P.fistR] as const; + +export const STEP_SECONDS = 1 / 120; +const ITERATIONS = 12; +const GRAVITY = 9.81; +const AIR_DAMPING = 0.998; +const GROUND_FRICTION = 0.5; +const ROPE_RADIUS = 0.03; +const ROPE_SOFTNESS = 0.35; +const POST_RADIUS_WITH_PAD = 0.12; +/** Torso parts never leave the ring; a head or limb may hang out between the ropes. */ +const TORSO_PARTS = [P.pelvis, P.hipL, P.hipR, P.belly, P.chest, P.upper] as const; +/** What turns with the shoulders when the spine is wrung past its turn. */ +const UPPER_BODY = [P.upper, P.neck, P.shoulderL, P.shoulderR, P.head, P.crown, P.face, P.elbowL, P.wristL, P.fistL, P.elbowR, P.wristR, P.fistR] as const; +/** What turns with the hips: the pelvis and both legs. */ +const LOWER_BODY = [P.pelvis, P.hipL, P.hipR, P.belly, P.kneeL, P.ankleL, P.toeL, P.heelL, P.kneeR, P.ankleR, P.toeR, P.heelR] as const; +/** The shoulders turn on the hips no further than this, either way about the spine. */ +const SPINE_TWIST = (40 * Math.PI) / 180; +const THIGH_PARTS = [[P.kneeL, P.ankleL, P.toeL, P.heelL], [P.kneeR, P.ankleR, P.toeR, P.heelR]] as const; +const SHIN_PARTS = [[P.ankleL, P.toeL, P.heelL], [P.ankleR, P.toeR, P.heelR]] as const; +/** How fast a limp thigh or shin left standing up topples once the body is down, per step and per unit it points up past `TOPPLE_FROM`. */ +const TOPPLE_RATE = 0.08; +const TOPPLE_FROM = 0.35; +const TORSO_SET: ReadonlySet = new Set(TORSO_PARTS); +const APRON_LIMIT = RING_FIGHT_HALF + 0.45; +const OBSTACLE_RADIUS = 0.24; +const OBSTACLE_HEIGHT = 1.75; +const SLEEP_SPEED = 0.012; +const SLEEP_SECONDS = 1; +/** Once the torso has come to rest, a limb held off the canvas by a joint limit is damped out instead of swinging on. */ +const LANDED_SPEED = 0.35; +const LANDED_SECONDS = 0.15; +/** A body still sliding this long into the fall (draped on the ropes or the opponent) settles all the same. */ +const LANDED_BY_SECONDS = 1.1; +const SETTLE_DAMPING = 0.975; +const SETTLE_RAMP_SECONDS = 0.4; +/** A landed body is put to sleep this long after it landed, whatever is still twitching. */ +const SETTLE_SECONDS = 1.2; +/** Steps of the opponent's place kept with a recorded fall (ten seconds). */ +const TRACKED_STEPS = 1200; +const MAX_START_SPEED = 3; +const MAX_SPEED = 7; +/** Below this speed a part loses a further share of its speed each step (the body at rest settles instead of creeping). */ +const CREEP_SPEED = 0.08; +const CREEP_DAMPING = 0.97; +const KNEE_MAX_FLEX = 2.55; +/** Knees and elbows straighten a little past straight, which keeps the limit away from the straight limb. */ +const HYPEREXTENSION = 0.12; +const HINGE_STIFFNESS = 0.5; +/** The neck leans this far from the line of the spine, and the skull this far again on the neck. */ +const NECK_CONE = (30 * Math.PI) / 180; +const SKULL_CONE = (32 * Math.PI) / 180; +/** The foot turns in or out on the shin only this far from the plane the knee bends in. */ +const FOOT_ROLL = (28 * Math.PI) / 180; +/** And rolls onto its outer edge (inversion) or its inner edge (eversion) only this far, as an ankle does. */ +const FOOT_INVERSION = (35 * Math.PI) / 180; +const FOOT_EVERSION = (20 * Math.PI) / 180; +/** + * A foot on the canvas is held to its range more gently, and no foot is turned further than this in one pass: + * onto its edge a little faster, since a falling leg rolls the foot quickly and that measure has no blind spot. + */ +const FOOT_GROUNDED = 0.5; +const FOOT_TURN = 0.01; +const FOOT_ROLL_TURN = 0.04; +const CONE_STIFFNESS = 0.4; +/** No pass turns a neck or a skull back further than this, and only a quarter of that into the canvas it lies on. */ +const NECK_TURN = 0.02; +const NECK_PRESSED = 0.25; + +interface StyleStiffness { + /** How much further the knees may bend, in radians, while the legs still hold. */ + readonly knee: number; + /** How far the spine may bend while the body still holds, as a share of its usual range. */ + readonly spine: number; + /** Seconds the body holds before it goes limp. */ + readonly seconds: number; +} + +/** + * How the body holds for a moment after the blow before going limp: locked knees and a stiff + * spine topple like a tree, legs that give at once drop in a heap, half-locked legs sag to the seat, and a body + * shot doubles him over on knees that bend under him into a kneel before he pitches onto his face. + */ +const STIFFNESS: Readonly> = { + timber: { knee: 0.12, spine: 0.2, seconds: 0.55 }, + crumple: { knee: KNEE_MAX_FLEX, spine: 1, seconds: 0 }, + sag: { knee: 0.55, spine: 0.6, seconds: 0.3 }, + fold: { knee: 0.9, spine: 1, seconds: 0.5 }, +}; + +const ELBOW_MAX_FLEX = 2.5; +const WRIST_LIMIT = (35 * Math.PI) / 180; +const ANKLE_MIN = (50 * Math.PI) / 180; +const ANKLE_MAX = (135 * Math.PI) / 180; +/** + * Joints bounded by the distance across them: the ankle between flat and pointed, the gloved + * wrist nearly straight. [outer, joint, end, smallest bend, largest bend] in radians from straight. + */ +const ANGLE_LIMITS: readonly (readonly [number, number, number, number, number])[] = [ + [P.kneeL, P.ankleL, P.toeL, ANKLE_MIN, ANKLE_MAX], [P.kneeR, P.ankleR, P.toeR, ANKLE_MIN, ANKLE_MAX], + [P.elbowL, P.wristL, P.fistL, 0, WRIST_LIMIT], [P.elbowR, P.wristR, P.fistR, 0, WRIST_LIMIT], +]; +const ANGLE_PAIRS = Int32Array.from(ANGLE_LIMITS.flatMap(([outer, , end]) => [outer, end])); + +export type FallStyle = "timber" | "crumple" | "sag" | "fold"; + + +export function fallStyleFor(punchClass: PunchClass | null, target: Target, amount: number, seed: number): FallStyle { + if (target === "body") return "fold"; + if (amount < 90) return "sag"; + const pick = Math.abs(Math.floor(seed)) % 3; + if (punchClass === "hook") return pick === 0 ? "timber" : "crumple"; + return pick === 0 ? "crumple" : "timber"; +} + +export interface ImpulseRecord { + step: number; + /** Velocity change of the struck part (the head, or the belly for a body shot). */ + x: number; + y: number; + z: number; + /** Velocity change of the whole body, carried along by the blow. */ + driveX: number; + driveY: number; + driveZ: number; + target: Target; + twist: number; + /** The blow that decides how the fighter goes down, when it lands just after the fall began. */ + style?: FallStyle; +} + +/** Where the opponent stood at each step of a fall: [active, x, z] per step, and how many steps are written. */ +export interface ObstacleTrack { + readonly data: Float64Array; + steps: number; +} + +/** Where a fall came to rest: its positions, its previous positions and the step it slept at (-1 until it has). */ +export interface FallRest { + readonly position: Float64Array; + readonly previous: Float64Array; + steps: number; +} + +/** + * Everything needed to run a fall again: the start, the style, each blow at the step it landed, where the opponent + * stood, what parts were lost from which step (as [step, mask] pairs), and where the fall came to rest. + */ +export interface FallRecord { + readonly positions: Float64Array; + readonly velocities: Float64Array; + readonly style: FallStyle; + readonly impulses: ImpulseRecord[]; + readonly offsets: THREE.Quaternion[]; + readonly obstacles: ObstacleTrack; + readonly losses: number[]; + readonly rest: FallRest; +} + +const v = (a: Float64Array, i: number, out: THREE.Vector3): THREE.Vector3 => out.set(a[i * 3]!, a[i * 3 + 1]!, a[i * 3 + 2]!); +/** An angle brought within half a turn of zero. */ +const wrapAngle = (angle: number): number => angle - Math.round(angle / (Math.PI * 2)) * Math.PI * 2; +const setV = (a: Float64Array, i: number, value: THREE.Vector3): void => { + a[i * 3] = value.x; + a[i * 3 + 1] = value.y; + a[i * 3 + 2] = value.z; +}; + +/** The particle body. Pure simulation: it knows nothing about bones. */ +export class RagdollBody { + readonly position = new Float64Array(PARTICLES * 3); + readonly previous = new Float64Array(PARTICLES * 3); + /** Positions before the latest step, for drawing between steps. */ + readonly before = new Float64Array(PARTICLES * 3); + readonly invMass = new Float64Array(PARTICLES); + private readonly rigidLength = new Float64Array(RIGID.length); + private readonly rangeMin = new Float64Array(RANGES.length); + private readonly rangeMax = new Float64Array(RANGES.length); + private readonly restRange = new Float64Array(RANGES.length); + private readonly rangeStart = new Float64Array(RANGES.length); + /** Each knee's bend at the start, so a held knee keeps its bend rather than snapping straight. */ + private readonly kneeStart = new Float64Array(2); + /** Which way each foot's own side-to-side axis points relative to its knee's hinge at the start. */ + private readonly footSign = new Float64Array(2); + private readonly turnAxis = new THREE.Vector3(); + private readonly turnQuaternion = new THREE.Quaternion(); + private readonly turnPoint = new THREE.Vector3(); + /** Angle limits written as distance ranges across a joint: [from, to, min, max]. */ + private readonly angleRanges = new Float64Array(ANGLE_LIMITS.length * 2); + /** Elbow hinge axes at the start, left then right, in world space (for the record). */ + readonly elbowAxes = new Float64Array(6); + /** Knee hinge axes at the start, in world space. */ + readonly kneeAxes = new Float64Array(6); + /** + * Each hinge in its body frame (pelvis for knees, upper torso for elbows): the parent segment's + * direction at the last step and the hinge axis. Every step the axis turns with the parent's + * small swing since the last, so it follows the limb without drifting against the body or + * flipping when the limb ends up far from where it started (an arm flung overhead). + * [knee L, knee R, elbow L, elbow R] x (direction xyz, axis xyz). + */ + private readonly hingeStart = new Float64Array(24); + private readonly hingeNow = new Float64Array(24); + private readonly swing = new THREE.Quaternion(); + private readonly local = new THREE.Vector3(); + private readonly localAxis = new THREE.Vector3(); + private readonly ropeLines = new Float64Array(4); + private style: FallStyle = "crumple"; + steps = 0; + private stillSeconds = 0; + private torsoStillSeconds = 0; + private fallSeconds = 0; + /** Seconds since the torso came to rest, or -1 while it is still falling. */ + private landedSeconds = -1; + asleep = false; + private obstacleActive = false; + private obstacleX = 0; + private obstacleZ = 0; + private obstacleTrack: ObstacleTrack | null = null; + private obstacleTrackLive = false; + /** Particle positions the body is drawn toward as it is handed to the get-up, and the share of the way each step. */ + private pull: Float64Array | null = null; + private pullShare = 0; + + /** + * A live fall writes where the opponent stood into `track` at every step; a replayed one reads each + * step's place back from it, so the replay falls exactly as the live fall did whatever the opponent does now. + */ + trackObstacle(track: ObstacleTrack | null, replay: boolean): void { + this.obstacleTrack = track; + this.obstacleTrackLive = track !== null && !replay; + } + private readonly a = new THREE.Vector3(); + private readonly b = new THREE.Vector3(); + private readonly c = new THREE.Vector3(); + private readonly d = new THREE.Vector3(); + private readonly e = new THREE.Vector3(); + private readonly fx = new THREE.Vector3(); + private readonly fy = new THREE.Vector3(); + private readonly fz = new THREE.Vector3(); + private readonly gx = new THREE.Vector3(); + private readonly gy = new THREE.Vector3(); + private readonly gz = new THREE.Vector3(); + private readonly pivot = new THREE.Vector3(); + private readonly segmentA = new THREE.Vector3(); + private readonly segmentB = new THREE.Vector3(); + + constructor() { + for (let i = 0; i < PARTICLES; i += 1) this.invMass[i] = 1 / MASS[i]!; + } + + /** Measures the joint ranges on the rest pose; call once with the rig's rest positions. */ + calibrate(rest: Float64Array): void { + for (const [index, [i, j]] of RANGES.entries()) this.restRange[index] = this.distance(rest, i, j); + } + + /** + * Starts a fall. `positions` and `velocities` are world-space particle states; ranges widen to + * take in the start so a twisted or crouched fighter does not snap straight at the first step. + */ + start(positions: Float64Array, velocities: Float64Array, style: FallStyle): void { + this.position.set(positions); + this.before.set(positions); + for (let i = 0; i < PARTICLES * 3; i += 1) this.previous[i] = positions[i]! - velocities[i]! * STEP_SECONDS; + for (const [index, [i, j]] of RIGID.entries()) this.rigidLength[index] = this.distance(positions, i, j); + for (const [index, [outer, joint, end, least, most]] of ANGLE_LIMITS.entries()) { + // Across a joint bent by `bend` from straight: d^2 = a^2 + b^2 + 2ab cos(bend). + const a = this.distance(positions, outer, joint); + const b = this.distance(positions, joint, end); + const now = this.distance(positions, outer, end); + const across = (bend: number): number => Math.sqrt(a * a + b * b + 2 * a * b * Math.cos(bend)); + this.angleRanges[index * 2] = Math.min(across(most), now); + this.angleRanges[index * 2 + 1] = Math.max(across(least), now); + } + for (const [index, [i, j, low, high]] of RANGES.entries()) { + const now = this.distance(positions, i, j); + const rest = this.restRange[index]! > 0 ? this.restRange[index]! : now; + this.rangeMin[index] = Math.min(rest * low, now * 0.98); + this.rangeMax[index] = Math.max(rest * high, now * 1.02); + this.rangeStart[index] = now; + } + this.style = style; + this.steps = 0; + this.stillSeconds = 0; + this.torsoStillSeconds = 0; + this.fallSeconds = 0; + this.landedSeconds = -1; + this.asleep = false; + this.upperFrame(this.position); + this.pelvisFrame(this.position); + for (const side of [0, 1] as const) { + // A bent joint's own plane gives its hinge; a straight one bends the way a person's does. + this.startHinge(this.elbowAxes, side, side === 0 ? P.shoulderL : P.shoulderR, side === 0 ? P.elbowL : P.elbowR, side === 0 ? P.wristL : P.wristR, this.gx, -1); + this.startHinge(this.kneeAxes, side, side === 0 ? P.hipL : P.hipR, side === 0 ? P.kneeL : P.kneeR, side === 0 ? P.ankleL : P.ankleR, this.fx, 1); + this.storeHingeStart(side, side === 0 ? P.hipL : P.hipR, side === 0 ? P.kneeL : P.kneeR, this.kneeAxes, this.fx, this.fy, this.fz); + this.storeHingeStart(2 + side, side === 0 ? P.shoulderL : P.shoulderR, side === 0 ? P.elbowL : P.elbowR, this.elbowAxes, this.gx, this.gy, this.gz); + const thigh = this.dir(side === 0 ? P.hipL : P.hipR, side === 0 ? P.kneeL : P.kneeR, this.a); + const shin = this.dir(side === 0 ? P.kneeL : P.kneeR, side === 0 ? P.ankleL : P.ankleR, this.b); + this.kneeStart[side] = Math.acos(Math.min(1, Math.max(-1, thigh.dot(shin)))); + const kneeAxis = this.c.set(this.kneeAxes[side * 3]!, this.kneeAxes[side * 3 + 1]!, this.kneeAxes[side * 3 + 2]!); + this.footSign[side] = this.footNormal(side, this.d).dot(kneeAxis) >= 0 ? 1 : -1; + } + // The ropes sit where they are, unless the fighter starts against them and has already pushed them out. + for (let side = 0; side < 4; side += 1) { + const axis = side < 2 ? 0 : 2; + const sign = side % 2 === 0 ? 1 : -1; + let extent = 0; + for (const i of TORSO_PARTS) extent = Math.max(extent, sign * positions[i * 3 + axis]! + RADIUS[i]!); + this.ropeLines[side] = Math.max(ROPE_LINE, extent + 0.04); + } + } + + private startHinge(axes: Float64Array, side: 0 | 1, root: number, joint: number, end: number, sideways: THREE.Vector3, sign: number): void { + const upper = this.dir(root, joint, this.a); + const lower = this.dir(joint, end, this.b); + const axis = this.c.crossVectors(upper, lower); + if (axis.length() < 0.2) axis.copy(sideways).multiplyScalar(sign); + axis.addScaledVector(upper, -axis.dot(upper)); + if (axis.lengthSq() < 1e-8) axis.copy(sideways).multiplyScalar(sign).addScaledVector(upper, -sign * sideways.dot(upper)); + axis.normalize(); + axes[side * 3] = axis.x; + axes[side * 3 + 1] = axis.y; + axes[side * 3 + 2] = axis.z; + } + + private storeHingeStart(slot: number, root: number, joint: number, axes: Float64Array, x: THREE.Vector3, y: THREE.Vector3, z: THREE.Vector3): void { + const side = slot % 2; + const direction = this.dir(root, joint, this.a); + const axis = this.b.set(axes[side * 3]!, axes[side * 3 + 1]!, axes[side * 3 + 2]!); + const o = slot * 6; + this.hingeStart[o] = direction.dot(x); + this.hingeStart[o + 1] = direction.dot(y); + this.hingeStart[o + 2] = direction.dot(z); + this.hingeStart[o + 3] = axis.dot(x); + this.hingeStart[o + 4] = axis.dot(y); + this.hingeStart[o + 5] = axis.dot(z); + for (let k = 0; k < 6; k += 1) this.hingeNow[o + k] = this.hingeStart[o + k]!; + } + + /** + * The hinge axis now: the start axis turned by the parent segment's swing since the start, both + * measured in the body frame (x, y, z), back in world space. + */ + private hingeAxis(slot: number, parent: THREE.Vector3, x: THREE.Vector3, y: THREE.Vector3, z: THREE.Vector3, out: THREE.Vector3): THREE.Vector3 { + const o = slot * 6; + const state = this.hingeNow; + this.local.set(parent.dot(x), parent.dot(y), parent.dot(z)).normalize(); + this.localAxis.set(state[o]!, state[o + 1]!, state[o + 2]!); + this.swing.setFromUnitVectors(this.localAxis, this.local); + this.localAxis.set(state[o + 3]!, state[o + 4]!, state[o + 5]!).applyQuaternion(this.swing); + out.copy(x).multiplyScalar(this.localAxis.x).addScaledVector(y, this.localAxis.y).addScaledVector(z, this.localAxis.z); + return out.addScaledVector(parent, -out.dot(parent)).normalize(); + } + + /** Moves each hinge's reference on to where its parent segment is now (once per step, so each swing is small). */ + private carryHinges(): void { + this.pelvisFrame(this.position); + this.upperFrame(this.position); + for (let slot = 0; slot < 4; slot += 1) { + const side = slot % 2; + const knee = slot < 2; + const root = knee ? (side === 0 ? P.hipL : P.hipR) : (side === 0 ? P.shoulderL : P.shoulderR); + const joint = knee ? (side === 0 ? P.kneeL : P.kneeR) : (side === 0 ? P.elbowL : P.elbowR); + const x = knee ? this.fx : this.gx; + const y = knee ? this.fy : this.gy; + const z = knee ? this.fz : this.gz; + const parent = this.dir(root, joint, this.a); + const axis = this.hingeAxis(slot, parent, x, y, z, this.b); + const o = slot * 6; + this.hingeNow[o] = parent.dot(x); + this.hingeNow[o + 1] = parent.dot(y); + this.hingeNow[o + 2] = parent.dot(z); + this.hingeNow[o + 3] = axis.dot(x); + this.hingeNow[o + 4] = axis.dot(y); + this.hingeNow[o + 5] = axis.dot(z); + } + } + + /** Whether the body still holds itself after the blow. */ + private get holding(): boolean { + return this.steps * STEP_SECONDS < STIFFNESS[this.style].seconds; + } + + get fallStyle(): FallStyle { + return this.style; + } + + /** The other fighter as a standing capsule the fall cannot pass through. */ + setObstacle(x: number, z: number, active: boolean): void { + this.obstacleX = x; + this.obstacleZ = z; + this.obstacleActive = active; + } + + /** + * Draws the body `share` of the way toward `targets` each step, keeping it awake, until called with null. The + * particles are moved rather than thrown: the pull leaves no speed behind it. + */ + pullToward(targets: Float64Array | null, share: number): void { + this.pull = targets; + this.pullShare = share; + if (targets === null) return; + this.asleep = false; + this.stillSeconds = 0; + } + + /** Takes a blow now: a velocity change on the struck part. Returns it stamped with the step it landed before. */ + impulse(record: ImpulseRecord): ImpulseRecord { + this.applyImpulse(record); + this.asleep = false; + this.stillSeconds = 0; + return { ...record, step: this.steps }; + } + + private applyImpulse(record: ImpulseRecord): void { + if (record.style !== undefined) this.style = record.style; + const shares = record.target === "head" ? HEAD_SHARES : BODY_SHARES; + for (let k = 0; k < shares.length; k += 2) { + const i = shares[k]!; + // The face takes more of a head blow than the skull, which turns the head along the punch. + const share = shares[k + 1]! + (i === P.face ? record.twist : 0); + this.kick(i, record.x * share, record.y * share, record.z * share); + } + // The rest of the body is carried along, the upper body more than the legs. + for (let i = 0; i < PARTICLES; i += 1) { + const share = DRIVE_SHARE[i]!; + this.kick(i, record.driveX * share, record.driveY * share, record.driveZ * share); + } + } + + private kick(i: number, x: number, y: number, z: number): void { + this.previous[i * 3] = this.previous[i * 3]! - x * STEP_SECONDS; + this.previous[i * 3 + 1] = this.previous[i * 3 + 1]! - y * STEP_SECONDS; + this.previous[i * 3 + 2] = this.previous[i * 3 + 2]! - z * STEP_SECONDS; + } + + /** Detaches a lost part's weight (a severed head or hand) so the rest of the body falls without it. */ + setLimp(indices: readonly number[], weightless: boolean): void { + for (let k = 0; k < indices.length; k += 1) { + const i = indices[k]!; + this.invMass[i] = weightless ? 1 / 0.05 : 1 / MASS[i]!; + } + } + + step(replay?: readonly ImpulseRecord[]): void { + if (this.asleep) return; + if (replay !== undefined) { + for (let k = 0; k < replay.length; k += 1) { + const record = replay[k]!; + if (record.step !== this.steps) continue; + this.applyImpulse(record); + this.stillSeconds = 0; + } + } + const track = this.obstacleTrack; + if (track !== null && this.obstacleTrackLive) { + if (this.steps < TRACKED_STEPS) { + track.data[this.steps * 3] = this.obstacleActive ? 1 : 0; + track.data[this.steps * 3 + 1] = this.obstacleX; + track.data[this.steps * 3 + 2] = this.obstacleZ; + track.steps = this.steps + 1; + } + } else if (track !== null && track.steps > 0) { + const k = Math.min(this.steps, track.steps - 1) * 3; + this.obstacleActive = track.data[k] === 1; + this.obstacleX = track.data[k + 1]!; + this.obstacleZ = track.data[k + 2]!; + } + const h = STEP_SECONDS; + const position = this.position; + const previous = this.previous; + const damping = this.landedSeconds < 0 ? AIR_DAMPING : AIR_DAMPING + (SETTLE_DAMPING - AIR_DAMPING) * Math.min(1, this.landedSeconds / SETTLE_RAMP_SECONDS); + this.before.set(position); + for (let i = 0; i < PARTICLES; i += 1) { + for (let k = 0; k < 3; k += 1) { + const index = i * 3 + k; + const current = position[index]!; + const velocity = (current - previous[index]!) * damping; + previous[index] = current; + position[index] = current + velocity - (k === 1 ? GRAVITY * h * h : 0); + } + } + const pull = this.pull; + if (pull !== null) { + for (let i = 0; i < PARTICLES; i += 1) { + const dx = (pull[i * 3]! - position[i * 3]!) * this.pullShare; + const dy = (pull[i * 3 + 1]! - position[i * 3 + 1]!) * this.pullShare; + const dz = (pull[i * 3 + 2]! - position[i * 3 + 2]!) * this.pullShare; + // No part is drawn faster than PULL_STEP a step, unless it is drawn all the way. + const length = Math.sqrt(dx * dx + dy * dy + dz * dz); + const keep = this.pullShare < 1 && length > PULL_STEP ? PULL_STEP / length : 1; + for (let k = 0; k < 3; k += 1) { + const move = (k === 0 ? dx : k === 1 ? dy : dz) * keep; + position[i * 3 + k] = position[i * 3 + k]! + move; + previous[i * 3 + k] = previous[i * 3 + k]! + move; + } + } + } + this.carryHinges(); + for (let iteration = 0; iteration < ITERATIONS; iteration += 1) { + this.solveRigid(); + // Drawn to a pose a skeleton has (the get-up's), the body keeps only its lengths and the canvas: the joints' + // limits are the physics' own idea of a body and would hold it back from the pose. + if (pull === null) { + this.solveRanges(); + this.solveKnees(); + this.solveTwist(); + this.solveElbows(); + this.solveNeck(); + this.solveFeet(); + this.solveArmsAgainstTorso(); + } + this.solveEnvironment(); + } + if (this.landedSeconds >= 0 && pull === null) this.toppleLegs(); + this.applyFriction(); + this.limitSpeed(); + this.steps += 1; + let fastest = 0; + let torsoFastest = 0; + for (let i = 0; i < PARTICLES; i += 1) { + const dx = position[i * 3]! - previous[i * 3]!; + const dy = position[i * 3 + 1]! - previous[i * 3 + 1]!; + const dz = position[i * 3 + 2]! - previous[i * 3 + 2]!; + const speed = Math.sqrt(dx * dx + dy * dy + dz * dz) / h; + fastest = Math.max(fastest, speed); + if (TORSO_SET.has(i)) torsoFastest = Math.max(torsoFastest, speed); + } + this.stillSeconds = fastest < SLEEP_SPEED ? this.stillSeconds + h : 0; + this.fallSeconds += h; + if (this.landedSeconds >= 0) this.landedSeconds += h; + else { + this.torsoStillSeconds = torsoFastest < LANDED_SPEED ? this.torsoStillSeconds + h : 0; + if (this.torsoStillSeconds >= LANDED_SECONDS || this.fallSeconds >= LANDED_BY_SECONDS) this.landedSeconds = 0; + } + if (pull === null && (this.stillSeconds >= SLEEP_SECONDS || this.landedSeconds >= SETTLE_SECONDS)) this.asleep = true; + } + + private solveRigid(): void { + for (let index = 0; index < this.rigidLength.length; index += 1) { + this.satisfy(RIGID_PAIRS[index * 2]!, RIGID_PAIRS[index * 2 + 1]!, this.rigidLength[index]!, this.rigidLength[index]!); + } + } + + private solveRanges(): void { + const hold = this.holding ? STIFFNESS[this.style].spine : 1; + for (let index = 0; index < this.rangeMin.length; index += 1) { + let low = this.rangeMin[index]!; + let high = this.rangeMax[index]!; + if (hold < 1 && index < SPINE_RANGES) { + const start = this.rangeStart[index]!; + low = start - (start - low) * hold; + high = start + (high - start) * hold; + } + this.satisfy(RANGE_PAIRS[index * 2]!, RANGE_PAIRS[index * 2 + 1]!, low, high); + } + for (let index = 0; index < MINIMUM_DISTANCES.length; index += 1) { + this.satisfy(MINIMUM_PAIRS[index * 2]!, MINIMUM_PAIRS[index * 2 + 1]!, MINIMUM_DISTANCES[index]!, Infinity); + } + for (let index = 0; index < ANGLE_PAIRS.length / 2; index += 1) { + this.satisfy(ANGLE_PAIRS[index * 2]!, ANGLE_PAIRS[index * 2 + 1]!, this.angleRanges[index * 2]!, this.angleRanges[index * 2 + 1]!); + } + } + + /** Moves two particles, by their masses, until their distance is within [low, high]. */ + private satisfy(i: number, j: number, low: number, high: number): void { + const p = this.position; + const dx = p[j * 3]! - p[i * 3]!; + const dy = p[j * 3 + 1]! - p[i * 3 + 1]!; + const dz = p[j * 3 + 2]! - p[i * 3 + 2]!; + const length = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (length < 1e-9) return; + const wanted = length < low ? low : length > high ? high : length; + if (wanted === length) return; + const wi = this.invMass[i]!; + const wj = this.invMass[j]!; + const total = wi + wj; + if (total <= 0) return; + const scale = (length - wanted) / (length * total); + p[i * 3] = p[i * 3]! + dx * scale * wi; + p[i * 3 + 1] = p[i * 3 + 1]! + dy * scale * wi; + p[i * 3 + 2] = p[i * 3 + 2]! + dz * scale * wi; + p[j * 3] = p[j * 3]! - dx * scale * wj; + p[j * 3 + 1] = p[j * 3 + 1]! - dy * scale * wj; + p[j * 3 + 2] = p[j * 3 + 2]! - dz * scale * wj; + } + + /** Knees bend backward only, about an axis that swings with the thigh. */ + private solveKnees(): void { + this.pelvisFrame(this.position); + for (let side = 0; side < 2; side += 1) { + const hip = side === 0 ? P.hipL : P.hipR; + const knee = side === 0 ? P.kneeL : P.kneeR; + const thigh = this.dir(hip, knee, this.a); + const axis = this.hingeAxis(side, thigh, this.fx, this.fy, this.fz, this.b); + const high = this.holding ? Math.min(KNEE_MAX_FLEX, this.kneeStart[side]! + STIFFNESS[this.style].knee) : KNEE_MAX_FLEX; + this.hinge(knee, side === 0 ? P.ankleL : P.ankleR, thigh, axis, -HYPEREXTENSION, high); + } + } + + /** Elbows bend one way only, about an axis that swings with the upper arm. */ + private solveElbows(): void { + this.upperFrame(this.position); + for (let side = 0; side < 2; side += 1) { + const shoulder = side === 0 ? P.shoulderL : P.shoulderR; + const elbow = side === 0 ? P.elbowL : P.elbowR; + const upper = this.dir(shoulder, elbow, this.a); + const axis = this.hingeAxis(2 + side, upper, this.gx, this.gy, this.gz, this.b); + this.hinge(elbow, side === 0 ? P.wristL : P.wristR, upper, axis, -HYPEREXTENSION, ELBOW_MAX_FLEX); + } + } + + /** + * Nothing holds up a limp leg: a thigh or a shin left pointing up once the body is down goes over the + * way it leans, where the heavy damping of a body at rest would otherwise keep a foot in the air. + */ + private toppleLegs(): void { + const p = this.position; + for (let side = 0; side < 2; side += 1) { + const hip = side === 0 ? P.hipL : P.hipR; + const knee = side === 0 ? P.kneeL : P.kneeR; + const ankle = side === 0 ? P.ankleL : P.ankleR; + const thigh = this.dir(hip, knee, this.a); + if (thigh.y > TOPPLE_FROM) { + const across = this.dir(P.hipR, P.hipL, this.b); + // Which way about the hips' axis takes the knee down. + const down = this.c.crossVectors(across, this.d.set(p[knee * 3]! - p[hip * 3]!, p[knee * 3 + 1]! - p[hip * 3 + 1]!, p[knee * 3 + 2]! - p[hip * 3 + 2]!)).y > 0 ? -1 : 1; + this.rotateAbout(THIGH_PARTS[side]!, hip, across, down * TOPPLE_RATE * (thigh.y - TOPPLE_FROM)); + } + const shin = this.dir(knee, ankle, this.a); + if (shin.y > TOPPLE_FROM) { + const axis = this.kneeAxis(side === 0 ? 0 : 1, p, this.b); + const down = this.c.crossVectors(axis, this.d.set(p[ankle * 3]! - p[knee * 3]!, p[ankle * 3 + 1]! - p[knee * 3 + 1]!, p[ankle * 3 + 2]! - p[knee * 3 + 2]!)).y > 0 ? -1 : 1; + this.rotateAbout(SHIN_PARTS[side]!, knee, axis, down * TOPPLE_RATE * (shin.y - TOPPLE_FROM)); + } + } + } + + /** The shoulders turn on the hips only as far as a spine does, however the body landed. */ + private solveTwist(): void { + const spine = this.dir(P.pelvis, P.upper, this.e); + const hips = this.dir(P.hipR, P.hipL, this.a); + hips.addScaledVector(spine, -hips.dot(spine)); + const shoulders = this.dir(P.shoulderR, P.shoulderL, this.c); + shoulders.addScaledVector(spine, -shoulders.dot(spine)); + if (hips.lengthSq() < 1e-8 || shoulders.lengthSq() < 1e-8) return; + const twist = Math.atan2(this.d.crossVectors(hips, shoulders).dot(spine), hips.dot(shoulders)); + const excess = Math.abs(twist) - SPINE_TWIST; + if (excess <= 0) return; + // The shoulders and the hips (the legs with them) share the turn back by how hard each is to turn about the + // spine: legs folded out to the side are, so they no longer whip the feet round with half the correction. + const upper = this.spinInertia(UPPER_BODY, spine); + const lower = this.spinInertia(LOWER_BODY, spine); + const turn = Math.sign(twist) * excess * CONE_STIFFNESS; + this.rotateAbout(UPPER_BODY, P.chest, spine, -turn * (lower / (upper + lower))); + this.rotateAbout(LOWER_BODY, P.chest, spine, turn * (upper / (upper + lower))); + } + + /** The particles' mass times the square of their distance from the line through the chest along `axis`. */ + private spinInertia(parts: readonly number[], axis: THREE.Vector3): number { + const p = this.position; + let inertia = 1e-6; + for (let k = 0; k < parts.length; k += 1) { + const i = parts[k]!; + const dx = p[i * 3]! - p[P.chest * 3]!; + const dy = p[i * 3 + 1]! - p[P.chest * 3 + 1]!; + const dz = p[i * 3 + 2]! - p[P.chest * 3 + 2]!; + const along = dx * axis.x + dy * axis.y + dz * axis.z; + inertia += (dx * dx + dy * dy + dz * dz - along * along) / this.invMass[i]!; + } + return inertia; + } + + /** The neck and the skull lean from the line of the spine only so far, whatever the blow. */ + private solveNeck(): void { + this.upperFrame(this.position); + this.cone(this.gy, P.neck, P.head, NECK_CONE, P.neck, HEAD_PARTS); + const neck = this.dir(P.neck, P.head, this.e); + this.cone(neck, P.head, P.crown, SKULL_CONE, P.head, SKULL_PARTS); + } + + /** + * A foot rolls onto its edge only as far as an ankle lets it, and turns in or out on the shin only a little from + * the plane its knee bends in, on the canvas as well as off it. Each pass turns a foot at most FOOT_TURN, so a + * foot held at its range is never flicked back to it in one step. + */ + private solveFeet(): void { + this.pelvisFrame(this.position); + const p = this.position; + for (let side = 0; side < 2; side += 1) { + const toe = side === 0 ? P.toeL : P.toeR; + const heel = side === 0 ? P.heelL : P.heelR; + const hip = side === 0 ? P.hipL : P.hipR; + const knee = side === 0 ? P.kneeL : P.kneeR; + const ankle = side === 0 ? P.ankleL : P.ankleR; + const grounded = p[toe * 3 + 1]! <= RADIUS[toe]! + 0.01 || p[heel * 3 + 1]! <= RADIUS[heel]! + 0.01; + const stiffness = grounded ? CONE_STIFFNESS * FOOT_GROUNDED : CONE_STIFFNESS; + const shin = this.dir(knee, ankle, this.c); + // Onto its edge: the foot's side axis (to the fighter's left on both feet) leans along the shin as the foot rolls, + // the left foot's onto its outer edge, the right foot's the other way. The heel swings round the ankle-toe line. + const foot = this.footNormal(side === 0 ? 0 : 1, this.d).multiplyScalar(this.footSign[side]!); + const lean = foot.dot(shin); + const outward = Math.sin(FOOT_INVERSION); + const inward = Math.sin(FOOT_EVERSION); + const leanTo = side === 0 ? Math.min(outward, Math.max(-inward, lean)) : Math.min(inward, Math.max(-outward, lean)); + if (leanTo !== lean) { + // Turned by `a` about the ankle-toe line, the side axis's lean is lean cos a + ((line x side) . shin) sin a. + const line = this.dir(ankle, toe, this.a); + const swing = this.e.crossVectors(line, foot).dot(shin); + const reach = Math.hypot(lean, swing); + if (reach > Math.abs(leanTo) + 1e-6) { + const phase = Math.atan2(swing, lean); + const spread = Math.acos(leanTo / reach); + const first = wrapAngle(phase - spread); + const second = wrapAngle(phase + spread); + const turn = (Math.abs(first) <= Math.abs(second) ? first : second) * stiffness; + this.rotateAbout(HEEL_PARTS[side]!, ankle, line, Math.min(FOOT_ROLL_TURN, Math.max(-FOOT_ROLL_TURN, turn))); + } + } + // In or out on the shin: the knee's hinge and the foot's side axis, both seen across the shin. A side axis + // nearly along the shin has no direction across it, so the foot is left as it is rather than spun. + const thigh = this.dir(hip, knee, this.a); + const hinge = this.hingeAxis(side, thigh, this.fx, this.fy, this.fz, this.b); + const across = this.footNormal(side === 0 ? 0 : 1, this.d).multiplyScalar(this.footSign[side]!); + hinge.addScaledVector(shin, -hinge.dot(shin)); + across.addScaledVector(shin, -across.dot(shin)); + if (hinge.lengthSq() < 0.09 || across.lengthSq() < 0.09) continue; + hinge.normalize(); + across.normalize(); + const roll = Math.atan2(this.e.crossVectors(hinge, across).dot(shin), hinge.dot(across)); + const bounded = Math.min(FOOT_ROLL, Math.max(-FOOT_ROLL, roll)); + if (bounded !== roll) this.rotateAbout(FOOT_PARTS[side]!, ankle, shin, Math.min(FOOT_TURN, Math.max(-FOOT_TURN, (bounded - roll) * stiffness))); + } + } + + /** The side-to-side axis of a foot, from its heel, toe and ankle. */ + private footNormal(side: 0 | 1, out: THREE.Vector3): THREE.Vector3 { + const p = this.position; + const ankle = side === 0 ? P.ankleL : P.ankleR; + const toe = side === 0 ? P.toeL : P.toeR; + const heel = side === 0 ? P.heelL : P.heelR; + out.set(p[toe * 3]! - p[heel * 3]!, p[toe * 3 + 1]! - p[heel * 3 + 1]!, p[toe * 3 + 2]! - p[heel * 3 + 2]!); + this.turnPoint.set(p[ankle * 3]! - p[heel * 3]!, p[ankle * 3 + 1]! - p[heel * 3 + 1]!, p[ankle * 3 + 2]! - p[heel * 3 + 2]!); + return out.cross(this.turnPoint).normalize(); + } + + /** + * Keeps the direction from `from` to `to` within `limit` of `axis`, turning the `moved` particles about `pivot` back + * toward it a share of the excess each pass, never more than NECK_TURN. Turned all the way back in one go, a skull + * lying on the canvas was driven into it, lifted straight back out by the canvas and skidded across it, flipping + * the head 40-140 degrees in a step; into the canvas it now turns only a little a pass, so it rolls round on it. + */ + private cone(axis: THREE.Vector3, from: number, to: number, limit: number, pivot: number, moved: readonly number[]): void { + const direction = this.dir(from, to, this.turnPoint); + const angle = Math.acos(Math.min(1, Math.max(-1, direction.dot(axis)))); + if (angle <= limit) return; + const turn = this.turnAxis.crossVectors(direction, axis); + if (turn.lengthSq() < 1e-10) return; + turn.normalize(); + const most = this.pressed(moved, pivot, turn) ? NECK_TURN * NECK_PRESSED : NECK_TURN; + this.rotateAbout(moved, pivot, turn, Math.min(most, (angle - limit) * CONE_STIFFNESS)); + } + + /** Whether turning `parts` about `pivot` on `axis` would press one lying on the canvas into it. */ + private pressed(parts: readonly number[], pivot: number, axis: THREE.Vector3): boolean { + const p = this.position; + for (let k = 0; k < parts.length; k += 1) { + const i = parts[k]!; + if (p[i * 3 + 1]! > RADIUS[i]! + 0.005) continue; + // The part's way at the start of the turn is axis x (part - pivot); only its height matters. + const down = axis.z * (p[i * 3]! - p[pivot * 3]!) - axis.x * (p[i * 3 + 2]! - p[pivot * 3 + 2]!); + if (down < -1e-9) return true; + } + return false; + } + + /** Turns particles rigidly by `angle` about `axis` through the particle `pivot`. */ + private rotateAbout(moved: readonly number[], pivot: number, axis: THREE.Vector3, angle: number): void { + const p = this.position; + this.turnQuaternion.setFromAxisAngle(axis, angle); + const px = p[pivot * 3]!; + const py = p[pivot * 3 + 1]!; + const pz = p[pivot * 3 + 2]!; + for (let k = 0; k < moved.length; k += 1) { + const i = moved[k]!; + this.turnPoint.set(p[i * 3]! - px, p[i * 3 + 1]! - py, p[i * 3 + 2]! - pz).applyQuaternion(this.turnQuaternion); + p[i * 3] = px + this.turnPoint.x; + p[i * 3 + 1] = py + this.turnPoint.y; + p[i * 3 + 2] = pz + this.turnPoint.z; + } + } + + /** The current elbow hinge axis (0 left, 1 right) in world space, for mapping the arm's twist. */ + elbowAxis(side: 0 | 1, positions: Float64Array, out: THREE.Vector3): THREE.Vector3 { + this.upperFrame(positions); + const shoulder = side === 0 ? P.shoulderL : P.shoulderR; + const elbow = side === 0 ? P.elbowL : P.elbowR; + const upper = this.e.set(positions[elbow * 3]! - positions[shoulder * 3]!, positions[elbow * 3 + 1]! - positions[shoulder * 3 + 1]!, positions[elbow * 3 + 2]! - positions[shoulder * 3 + 2]!).normalize(); + return this.hingeAxis(2 + side, upper, this.gx, this.gy, this.gz, out); + } + + /** The current knee hinge axis in world space, for mapping the leg's twist. */ + kneeAxis(side: 0 | 1, positions: Float64Array, out: THREE.Vector3): THREE.Vector3 { + this.pelvisFrame(positions); + const hip = side === 0 ? P.hipL : P.hipR; + const knee = side === 0 ? P.kneeL : P.kneeR; + const thigh = this.e.set(positions[knee * 3]! - positions[hip * 3]!, positions[knee * 3 + 1]! - positions[hip * 3 + 1]!, positions[knee * 3 + 2]! - positions[hip * 3 + 2]!).normalize(); + return this.hingeAxis(side, thigh, this.fx, this.fy, this.fz, out); + } + + /** + * Keeps the bend of `child` about `axis` at `joint` (measured from the parent direction `parent` + * in the hinge plane) between `low` and `high` radians. Only the in-plane angle is corrected, by + * half per pass, so a limb twisted out of the plane is never yanked back into it. + */ + private hinge(joint: number, child: number, parent: THREE.Vector3, axis: THREE.Vector3, low: number, high: number): void { + const p = this.position; + const segment = this.e.set(p[child * 3]! - p[joint * 3]!, p[child * 3 + 1]! - p[joint * 3 + 1]!, p[child * 3 + 2]! - p[joint * 3 + 2]!); + const length = segment.length(); + if (length < 1e-9) return; + segment.divideScalar(length); + const out = segment.dot(axis); + segment.addScaledVector(axis, -out); + const inPlane = segment.length(); + if (inPlane < 0.2) return; + segment.divideScalar(inPlane); + const flex = Math.atan2(this.pivot.crossVectors(parent, segment).dot(axis), parent.dot(segment)); + const bounded = Math.min(high, Math.max(low, flex)); + if (bounded === flex) return; + const side = this.pivot.crossVectors(axis, parent); + segment.copy(parent).multiplyScalar(Math.cos(bounded)).addScaledVector(side, Math.sin(bounded)).multiplyScalar(inPlane).addScaledVector(axis, out); + const tx = p[joint * 3]! + segment.x * length; + const ty = p[joint * 3 + 1]! + segment.y * length; + const tz = p[joint * 3 + 2]! + segment.z * length; + const wj = this.invMass[joint]!; + const wc = this.invMass[child]!; + const share = wc / (wj + wc); + const dx = (tx - p[child * 3]!) * HINGE_STIFFNESS; + const dy = (ty - p[child * 3 + 1]!) * HINGE_STIFFNESS; + const dz = (tz - p[child * 3 + 2]!) * HINGE_STIFFNESS; + p[child * 3] = p[child * 3]! + dx * share; + p[child * 3 + 1] = p[child * 3 + 1]! + dy * share; + p[child * 3 + 2] = p[child * 3 + 2]! + dz * share; + p[joint * 3] = p[joint * 3]! - dx * (1 - share); + p[joint * 3 + 1] = p[joint * 3 + 1]! - dy * (1 - share); + p[joint * 3 + 2] = p[joint * 3 + 2]! - dz * (1 - share); + } + + /** Arms stay outside the torso. */ + private solveArmsAgainstTorso(): void { + const p = this.position; + for (let part = 0; part < ARM_PARTS.length; part += 1) { + const i = ARM_PARTS[part]!; + for (let capsule = 0; capsule < CAPSULE_RADII.length; capsule += 1) { + const radius = CAPSULE_RADII[capsule]!; + const a = v(p, CAPSULES[capsule * 2]!, this.a); + const b = v(p, CAPSULES[capsule * 2 + 1]!, this.b); + const point = v(p, i, this.c); + const closest = this.closestOnSegment(a, b, point, this.d); + const offset = this.e.subVectors(point, closest); + const distance = offset.length(); + const wanted = radius + RADIUS[i]!; + if (distance >= wanted || distance < 1e-6) continue; + point.addScaledVector(offset, (wanted - distance) / distance); + setV(p, i, point); + } + } + } + + private closestOnSegment(a: THREE.Vector3, b: THREE.Vector3, point: THREE.Vector3, out: THREE.Vector3): THREE.Vector3 { + const ab = this.segmentA.subVectors(b, a); + const lengthSq = ab.lengthSq(); + const t = lengthSq < 1e-12 ? 0 : Math.min(1, Math.max(0, this.segmentB.subVectors(point, a).dot(ab) / lengthSq)); + return out.copy(a).addScaledVector(ab, t); + } + + /** Canvas, ropes, posts, the edge of the apron, and the other fighter. */ + private solveEnvironment(): void { + const p = this.position; + for (let i = 0; i < PARTICLES; i += 1) { + const r = RADIUS[i]!; + let x = p[i * 3]!; + let y = p[i * 3 + 1]!; + let z = p[i * 3 + 2]!; + if (y < r) y = r; + const torso = TORSO_SET.has(i); + for (let side = 0; side < 4; side += 1) { + const alongX = side < 2; + const sign = side % 2 === 0 ? 1 : -1; + const line = this.ropeLines[side]!; + const out = sign * (alongX ? x : z); + if (out < line - r - ROPE_RADIUS - 0.25) continue; + for (let rope = 0; rope < ROPE_HEIGHTS.length; rope += 1) { + const height = ROPE_HEIGHTS[rope]!; + const dOut = out - line; + const dUp = y - height; + const distance = Math.sqrt(dOut * dOut + dUp * dUp); + const wanted = r + ROPE_RADIUS; + if (distance >= wanted || distance < 1e-9) continue; + // A part already past the rope is pushed out the far side; otherwise back into the ring. + const push = ((wanted - distance) / distance) * ROPE_SOFTNESS; + const pushOut = dOut * push; + const pushUp = dUp * push; + if (alongX) x += sign * pushOut; + else z += sign * pushOut; + y += pushUp; + } + const limit = torso ? line - r * 0.6 : APRON_LIMIT; + const now = sign * (alongX ? x : z); + if (now > limit) { + if (alongX) x = sign * limit; + else z = sign * limit; + } + } + for (let corner = 0; corner < 4; corner += 1) { + const cornerX = corner < 2 ? -ROPE_LINE : ROPE_LINE; + const cornerZ = corner % 2 === 0 ? -ROPE_LINE : ROPE_LINE; + const dx = x - cornerX; + const dz = z - cornerZ; + const distance = Math.sqrt(dx * dx + dz * dz); + const wanted = POST_RADIUS_WITH_PAD + r; + if (distance >= wanted || distance < 1e-9 || y > 1.45) continue; + x = cornerX + (dx / distance) * wanted; + z = cornerZ + (dz / distance) * wanted; + } + if (this.obstacleActive && y < OBSTACLE_HEIGHT + r) { + const dx = x - this.obstacleX; + const dz = z - this.obstacleZ; + const distance = Math.sqrt(dx * dx + dz * dz); + const wanted = OBSTACLE_RADIUS + r; + if (distance < wanted && distance > 1e-9) { + x = this.obstacleX + (dx / distance) * wanted; + z = this.obstacleZ + (dz / distance) * wanted; + } + } + p[i * 3] = x; + p[i * 3 + 1] = y; + p[i * 3 + 2] = z; + } + } + + /** + * A safety net: no part moves faster than a falling body can, whatever the constraints did this + * step. Parts barely moving lose a little more speed, so a body at rest does not creep across the canvas. + */ + private limitSpeed(): void { + const p = this.position; + const q = this.previous; + const limit = MAX_SPEED * STEP_SECONDS; + const creep = CREEP_SPEED * STEP_SECONDS; + for (let i = 0; i < PARTICLES; i += 1) { + const dx = p[i * 3]! - q[i * 3]!; + const dy = p[i * 3 + 1]! - q[i * 3 + 1]!; + const dz = p[i * 3 + 2]! - q[i * 3 + 2]!; + const travel = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (travel > limit || travel < creep) { + const keep = travel > limit ? limit / travel : CREEP_DAMPING; + q[i * 3] = p[i * 3]! - dx * keep; + q[i * 3 + 1] = p[i * 3 + 1]! - dy * keep; + q[i * 3 + 2] = p[i * 3 + 2]! - dz * keep; + } + } + } + + /** Parts on the canvas lose most of their sliding speed each step. */ + private applyFriction(): void { + const p = this.position; + const q = this.previous; + for (let i = 0; i < PARTICLES; i += 1) { + if (p[i * 3 + 1]! > RADIUS[i]! + 0.004) continue; + q[i * 3] = q[i * 3]! + (p[i * 3]! - q[i * 3]!) * GROUND_FRICTION; + q[i * 3 + 2] = q[i * 3 + 2]! + (p[i * 3 + 2]! - q[i * 3 + 2]!) * GROUND_FRICTION; + if (q[i * 3 + 1]! > p[i * 3 + 1]!) q[i * 3 + 1] = p[i * 3 + 1]!; + } + } + + /** Runs until the body is at rest, or for at most `seconds`; used to show where a replayed fall ends. */ + settle(seconds = 4, replay?: readonly ImpulseRecord[]): void { + const limit = this.steps + Math.ceil(seconds / STEP_SECONDS); + while (!this.asleep && this.steps < limit) this.step(replay); + } + + /** Copies where the body is now into `out`, as it comes to rest. */ + keepRest(out: FallRest): void { + out.position.set(this.position); + out.previous.set(this.previous); + out.steps = this.steps; + } + + /** Puts the body where a fall came to rest, asleep. */ + restore(rest: FallRest): void { + this.position.set(rest.position); + this.previous.set(rest.previous); + this.before.set(rest.position); + this.steps = rest.steps; + this.asleep = true; + } + + /** Writes positions drawn `alpha` of the way from the previous step to the latest. */ + interpolate(alpha: number, out: Float64Array): void { + const before = this.before; + const now = this.position; + for (let i = 0; i < PARTICLES * 3; i += 1) out[i] = before[i]! + (now[i]! - before[i]!) * alpha; + } + + private distance(positions: Float64Array, i: number, j: number): number { + const dx = positions[j * 3]! - positions[i * 3]!; + const dy = positions[j * 3 + 1]! - positions[i * 3 + 1]!; + const dz = positions[j * 3 + 2]! - positions[i * 3 + 2]!; + return Math.sqrt(dx * dx + dy * dy + dz * dz); + } + + private dir(from: number, to: number, out: THREE.Vector3): THREE.Vector3 { + const p = this.position; + out.set(p[to * 3]! - p[from * 3]!, p[to * 3 + 1]! - p[from * 3 + 1]!, p[to * 3 + 2]! - p[from * 3 + 2]!); + const length = out.length(); + return length > 1e-9 ? out.divideScalar(length) : out.set(0, 1, 0); + } + + /** Pelvis axes into fx (the fighter's left), fy (up the spine) and fz (forward). */ + pelvisFrame(positions: Float64Array): void { + this.fx.set(positions[P.hipL * 3]! - positions[P.hipR * 3]!, positions[P.hipL * 3 + 1]! - positions[P.hipR * 3 + 1]!, positions[P.hipL * 3 + 2]! - positions[P.hipR * 3 + 2]!).normalize(); + this.fz.set( + positions[P.belly * 3]! - (positions[P.hipL * 3]! + positions[P.hipR * 3]!) / 2, + positions[P.belly * 3 + 1]! - (positions[P.hipL * 3 + 1]! + positions[P.hipR * 3 + 1]!) / 2, + positions[P.belly * 3 + 2]! - (positions[P.hipL * 3 + 2]! + positions[P.hipR * 3 + 2]!) / 2, + ); + this.fz.addScaledVector(this.fx, -this.fz.dot(this.fx)).normalize(); + this.fy.crossVectors(this.fz, this.fx).normalize(); + } + + /** Upper-torso axes into gx (left), gy (up the neck) and gz (forward). */ + upperFrame(positions: Float64Array): void { + this.gx.set(positions[P.shoulderL * 3]! - positions[P.shoulderR * 3]!, positions[P.shoulderL * 3 + 1]! - positions[P.shoulderR * 3 + 1]!, positions[P.shoulderL * 3 + 2]! - positions[P.shoulderR * 3 + 2]!).normalize(); + this.gy.set(positions[P.neck * 3]! - positions[P.upper * 3]!, positions[P.neck * 3 + 1]! - positions[P.upper * 3 + 1]!, positions[P.neck * 3 + 2]! - positions[P.upper * 3 + 2]!); + this.gy.addScaledVector(this.gx, -this.gy.dot(this.gx)).normalize(); + this.gz.crossVectors(this.gx, this.gy).normalize(); + } + + /** The pelvis's forward direction, for telling a face-down landing from one on the back. */ + pelvisForward(out: THREE.Vector3): THREE.Vector3 { + this.pelvisFrame(this.position); + return out.copy(this.fz); + } +} + +/** A severed head or hand carries no weight; `lost` has a bit each for the head and the left and right hands. */ +function weigh(body: RagdollBody, lost: number): void { + body.setLimp(LOST_HEAD, (lost & 1) !== 0); + body.setLimp(LOST_LEFT_HAND, (lost & 2) !== 0); + body.setLimp(LOST_RIGHT_HAND, (lost & 4) !== 0); +} + +/** Bones the fall drives, parents first. Clavicles, toes and fingers keep their last pose. */ +const DRIVEN: readonly CanonicalBone[] = [ + "hips", "spine", "chest", "upperChest", "neck", "head", + "shoulderL", "elbowL", "gloveL", "shoulderR", "elbowR", "gloveR", + "hipL", "kneeL", "ankleL", "hipR", "kneeR", "ankleR", +]; +/** The particles each limb bone runs between, by its place in DRIVEN. */ +const SEGMENTS: readonly (readonly [number, number] | null)[] = DRIVEN.map((name) => ({ + shoulderL: [P.shoulderL, P.elbowL], elbowL: [P.elbowL, P.wristL], gloveL: [P.wristL, P.fistL], + shoulderR: [P.shoulderR, P.elbowR], elbowR: [P.elbowR, P.wristR], gloveR: [P.wristR, P.fistR], + hipL: [P.hipL, P.kneeL], kneeL: [P.kneeL, P.ankleL], hipR: [P.hipR, P.kneeR], kneeR: [P.kneeR, P.ankleR], +} as Partial>)[name] ?? null); +/** The particle each driven bone is put on, by its place in DRIVEN (-1: it is placed by its parent). */ +const PINNED: readonly number[] = DRIVEN.map((name) => ({ chest: P.chest, shoulderL: P.shoulderL, shoulderR: P.shoulderR, hipL: P.hipL, hipR: P.hipR } as Partial>)[name] ?? -1); +const BLEND_IN_SECONDS = 0.1; +/** A blow presented this soon after the fall began still decides how the fighter goes down. */ +const LATE_BLOW_SECONDS = 0.35; +/** A blow presented this soon before the fall is the one that caused it. */ +const EARLY_BLOW_SECONDS = 0.6; +const MAX_STEPS_PER_UPDATE = 10; +/** A root this far from where the pose was last sampled was drawn somewhere else: the samples are not his motion. */ +const STALE_SHIFT = 0.35; +/** Steps a frame the fall a replay cut short is run on ahead of it, so it has come to rest by the time the replay ends. */ +const AHEAD_STEPS = 6; +/** + * The share of the way to the get-up pose a body being handed over is drawn each step, no part faster than PULL_STEP a + * step (6 m/s), and the seconds over which the bones' own turns go over to the get-up's; what is left when the physics + * lets go, the get-up blends out (see blendRise). + */ +const FOLLOW_PULL = 0.25; +const PULL_STEP = 0.05; +const FOLLOW_TURN_SECONDS = 0.3; +/** How long a fall is run on at most, from where it is, to show where it ends. */ +const RUN_ON_SECONDS = 6; + +export interface Blow { + readonly target: Target; + readonly punchClass: PunchClass | null; + readonly hand: Hand | null; + /** +1 when the blow drives the head toward the fighter's own left. */ + readonly lateral: number; + readonly amount: number; +} + +export interface BlowImpulse { + readonly x: number; + readonly y: number; + readonly z: number; + readonly driveX: number; + readonly driveY: number; + readonly driveZ: number; + readonly twist: number; +} + +/** + * Velocity changes for a blow, in the struck fighter's own frame (+x his left, +y up, +z toward the + * puncher): the snap of the struck part, the carry of the whole body, and how much the head turns. + * Straights and uppercuts drive him back onto his back; a hook spins him over his front foot; a body + * shot folds him at the waist. + */ +export function blowImpulse(blow: Blow): BlowImpulse { + const lateral = blow.lateral; + if (blow.target === "body") { + const m = Math.min(2.6, Math.max(1, 0.9 + blow.amount / 90)); + return { x: 0.3 * lateral * m, y: -0.05 * m, z: -m, driveX: 0.1 * lateral * m, driveY: -0.2 * m, driveZ: 0.45 * m, twist: 0 }; + } + const m = Math.min(3.2, Math.max(1.4, 1.2 + blow.amount / 70)); + const carry = m / 3.2; + switch (blow.punchClass) { + case "hook": return { x: lateral * m, y: 0.05 * m, z: -0.25 * m, driveX: 0.55 * lateral * carry, driveY: -0.1 * carry, driveZ: 0.85 * carry, twist: 0.6 }; + case "uppercut": return { x: 0, y: 0.8 * m, z: -0.65 * m, driveX: 0, driveY: 0.25 * carry, driveZ: -1.1 * carry, twist: 0.4 }; + case "straight": return { x: 0.12 * lateral * m, y: 0.1 * m, z: -m, driveX: 0.1 * lateral * carry, driveY: 0, driveZ: -1.25 * carry, twist: 0.3 }; + default: return { x: 0.12 * lateral * m, y: 0.1 * m, z: -0.85 * m, driveX: 0.08 * lateral * carry, driveY: 0, driveZ: -1 * carry, twist: 0.2 }; + } +} + +/** + * Knockout physics on a fighter's rig: samples the animated skeleton while he stands, starts the + * fall from that pose and its velocities, takes the blow, drives the bones while he is down, hands + * him back to the authored get-up, and keeps each fall so the knockout replay can run it again. + */ +export class KnockoutRagdoll { + readonly body = new RagdollBody(); + /** The same fall run on ahead of a replay that cut the live one short, to find where it comes to rest. */ + private readonly ahead = new RagdollBody(); + private aheadOf: FallRecord | null = null; + private aheadBlows = 0; + private ragdolling = false; + private age = 0; + private accumulator = 0; + private readonly sampleNow = new Float64Array(PARTICLES * 3); + private readonly sampleBefore = new Float64Array(PARTICLES * 3); + /** Where the root was when the pose was last sampled. */ + private readonly sampleRoot = new THREE.Vector3(); + private sampleSeconds = 0; + private samples = 0; + private readonly drawn = new Float64Array(PARTICLES * 3); + private readonly velocities = new Float64Array(PARTICLES * 3); + private readonly offsets = DRIVEN.map(() => new THREE.Quaternion()); + private readonly fromLocal = DRIVEN.map(() => new THREE.Quaternion()); + private readonly fromPositions = DRIVEN.map(() => new THREE.Vector3()); + private readonly fromHips = new THREE.Vector3(); + private readonly riseLocal = DRIVEN.map(() => new THREE.Quaternion()); + private readonly riseHips = new THREE.Vector3(); + /** Each driven bone's turn in the body's frame (under the root) as the fall left it, and as the get-up has it. */ + private readonly riseTurns = DRIVEN.map(() => new THREE.Quaternion()); + private readonly solvedTurns = DRIVEN.map(() => new THREE.Quaternion()); + private readonly rootTurn = new THREE.Quaternion(); + private readonly turn = new THREE.Quaternion(); + private riseCaptured = false; + private lastFall: FallRecord | null = null; + /** The buffers the latest fall is recorded in, taken over by the next one: a knockdown builds nothing. */ + private readonly recordPositions = new Float64Array(PARTICLES * 3); + private readonly recordVelocities = new Float64Array(PARTICLES * 3); + private readonly recordOffsets = DRIVEN.map(() => new THREE.Quaternion()); + private readonly recordImpulses: ImpulseRecord[] = []; + private readonly recordObstacles: ObstacleTrack = { data: new Float64Array(TRACKED_STEPS * 3), steps: 0 }; + private readonly recordLosses: number[] = []; + private readonly recordRest: FallRest = { position: new Float64Array(PARTICLES * 3), previous: new Float64Array(PARTICLES * 3), steps: -1 }; + private replaying: FallRecord | null = null; + /** The get-up pose's particles and the offsets that would put the bones on them, while the body is handed over. */ + private readonly followTargets = new Float64Array(PARTICLES * 3); + private readonly followOffsets = DRIVEN.map(() => new THREE.Quaternion()); + private readonly blendedOffsets = DRIVEN.map(() => new THREE.Quaternion()); + private following = false; + private followAge = 0; + private primed = false; + private pending: (ImpulseRecord & { readonly at: number }) | null = null; + private lost = 0; + private clock = 0; + private readonly bones: readonly THREE.Bone[]; + private readonly frame = new THREE.Quaternion(); + private readonly world = new THREE.Quaternion(); + private readonly axisX = new THREE.Vector3(); + private readonly axisY = new THREE.Vector3(); + private readonly axisZ = new THREE.Vector3(); + private readonly hint = new THREE.Vector3(); + private readonly point = new THREE.Vector3(); + private readonly other = new THREE.Vector3(); + private readonly pelvisX = new THREE.Vector3(); + private readonly pelvisY = new THREE.Vector3(); + private readonly upperX = new THREE.Vector3(); + private readonly headX = new THREE.Vector3(); + private readonly kneeAxes = [new THREE.Vector3(1, 0, 0), new THREE.Vector3(1, 0, 0)] as const; + private readonly elbowAxes = [new THREE.Vector3(), new THREE.Vector3()] as const; + /** From the spine's joint to the chest's at rest, in world units. */ + private readonly spineLength: number; + private readonly tiltJoint = new THREE.Vector3(); + private readonly tiltChest = new THREE.Vector3(); + private readonly tiltAcross = new THREE.Vector3(); + private readonly tiltTurn = new THREE.Quaternion(); + + constructor(private readonly rig: SolvedRig, private readonly root: THREE.Object3D) { + this.bones = DRIVEN.map((name) => rig.bones[name]); + const locals = new Map(); + root.traverse((object) => { + if (object instanceof THREE.Bone) locals.set(object, [object.quaternion.clone(), object.position.clone()]); + }); + rig.resetToRest(); + root.updateMatrixWorld(true); + const rest = new Float64Array(PARTICLES * 3); + this.read(rest); + this.body.calibrate(rest); + this.ahead.calibrate(rest); + this.spineLength = this.tiltJoint.setFromMatrixPosition(rig.bones.spine.matrixWorld).distanceTo(this.tiltChest.setFromMatrixPosition(rig.bones.chest.matrixWorld)); + for (const [bone, [quaternion, position]] of locals) { + bone.quaternion.copy(quaternion); + bone.position.copy(position); + } + root.updateMatrixWorld(true); + } + + /** True while physics drives the skeleton. */ + get active(): boolean { + return this.ragdolling; + } + + /** The record of the latest fall, for the replay. */ + get record(): FallRecord | null { + return this.lastFall; + } + + /** Remembers the animated pose after each solve so a fall starts with the body's own momentum. */ + sample(dt: number): void { + if (this.ragdolling || dt <= 0) return; + this.sampleBefore.set(this.sampleNow); + this.read(this.sampleNow); + this.sampleRoot.setFromMatrixPosition(this.root.matrixWorld); + this.sampleSeconds = dt; + this.samples = Math.min(2, this.samples + 1); + } + + /** Advances the clock that ages a blow waiting for its fall. */ + tick(dt: number): void { + this.clock += dt; + } + + /** A blow on this fighter, already turned into the world frame by the caller. */ + takeBlow(snap: THREE.Vector3, drive: THREE.Vector3, target: Target, twist: number, style: FallStyle): void { + const impulse: ImpulseRecord = { step: 0, x: snap.x, y: snap.y, z: snap.z, driveX: drive.x, driveY: drive.y, driveZ: drive.z, target, twist, style }; + if (this.ragdolling) { + if (this.age > LATE_BLOW_SECONDS) return; + if (this.replaying !== null) { + // A replayed fall plays its recorded blows; one that never reached the live fall is added and plays at the next step + // (the fall run on ahead of the replay starts over with it). + if (this.replaying.impulses.length === 0) this.replaying.impulses.push({ ...impulse, step: this.body.steps }); + return; + } + this.lastFall?.impulses.push(this.body.impulse(impulse)); + return; + } + this.pending = { ...impulse, at: this.clock }; + } + + /** The next fall runs the recorded one again (the knockout replay). */ + prime(): void { + this.primed = this.lastFall !== null; + } + + /** Drops the recorded fall: the latest fall was played by the animation instead, so there is nothing to run again. */ + forget(): void { + this.lastFall = null; + this.primed = false; + } + + /** Starts the fall from the current pose. Returns false when there is nothing to start from. */ + start(defaultStyle: FallStyle): boolean { + const record = this.primed ? this.lastFall : null; + this.primed = false; + this.root.updateMatrixWorld(true); + this.captureLocals(this.fromLocal, this.fromHips); + if (record !== null) { + this.replaying = record; + this.body.start(record.positions, record.velocities, record.style); + this.body.trackObstacle(record.obstacles, true); + this.replayLosses(this.body, record, 0); + for (const [index, offset] of record.offsets.entries()) this.offsets[index]!.copy(offset); + // A live fall the replay cut short has not come to rest: run it on ahead, a few steps a frame, to find where it does. + this.aheadOf = null; + if (record.rest.steps < 0) this.runAhead(record, 0); + } else { + this.replaying = null; + // The samples are the pose the last frames drew. If the root has been put somewhere else since (a gap in drawing, + // or the fighter put back at his place), they are not his motion: the fall starts from the pose as it is now, at + // rest. Otherwise they move along with the root's last small step, which the bones have already taken. + const moved = this.point.setFromMatrixPosition(this.root.matrixWorld).sub(this.sampleRoot); + const fresh = this.samples > 0 && moved.lengthSq() <= STALE_SHIFT * STALE_SHIFT; + if (!fresh) this.read(this.sampleNow); + else { + for (let i = 0; i < PARTICLES; i += 1) { + for (const samples of [this.sampleNow, this.sampleBefore]) { + samples[i * 3] = samples[i * 3]! + moved.x; + samples[i * 3 + 1] = samples[i * 3 + 1]! + moved.y; + samples[i * 3 + 2] = samples[i * 3 + 2]! + moved.z; + } + } + } + const seconds = fresh && this.samples >= 2 && this.sampleSeconds > 0 ? this.sampleSeconds : 0; + this.samples = 0; + for (let i = 0; i < PARTICLES * 3; i += 1) { + const speed = seconds > 0 ? (this.sampleNow[i]! - this.sampleBefore[i]!) / seconds : 0; + this.velocities[i] = Math.max(-MAX_START_SPEED, Math.min(MAX_START_SPEED, speed)); + } + // The poser moves the head off the neck for slips and blows, but the body on the canvas is drawn with the + // head on its neck (see drive): the head, crown and face start where the neck will carry them. + const neckBone = this.rig.bones.neck; + const headBone = this.rig.bones.head; + this.point.copy(this.rig.restLocalPosition(headBone)).applyMatrix4(neckBone.matrixWorld).sub(this.other.setFromMatrixPosition(headBone.matrixWorld)); + for (const i of HEAD_PARTS) { + this.sampleNow[i * 3] = this.sampleNow[i * 3]! + this.point.x; + this.sampleNow[i * 3 + 1] = this.sampleNow[i * 3 + 1]! + this.point.y; + this.sampleNow[i * 3 + 2] = this.sampleNow[i * 3 + 2]! + this.point.z; + } + const blow = this.pending !== null && this.clock - this.pending.at <= EARLY_BLOW_SECONDS ? this.pending : null; + this.body.start(this.sampleNow, this.velocities, blow?.style ?? defaultStyle); + this.computeOffsets(); + const impulses = this.recordImpulses; + impulses.length = 0; + if (blow !== null) impulses.push(this.body.impulse({ step: 0, x: blow.x, y: blow.y, z: blow.z, driveX: blow.driveX, driveY: blow.driveY, driveZ: blow.driveZ, target: blow.target, twist: blow.twist })); + const obstacles = this.recordObstacles; + obstacles.steps = 0; + this.body.trackObstacle(obstacles, false); + this.recordPositions.set(this.sampleNow); + this.recordVelocities.set(this.velocities); + for (let index = 0; index < this.offsets.length; index += 1) this.recordOffsets[index]!.copy(this.offsets[index]!); + this.recordLosses.length = 0; + this.recordLosses.push(0, this.lost); + this.recordRest.steps = -1; + this.aheadOf = null; + this.lastFall = { + positions: this.recordPositions, + velocities: this.recordVelocities, + style: this.body.fallStyle, + impulses, + offsets: this.recordOffsets, + obstacles, + losses: this.recordLosses, + rest: this.recordRest, + }; + } + this.pending = null; + this.ragdolling = true; + this.riseCaptured = false; + this.age = 0; + this.accumulator = 0; + this.following = false; + this.body.pullToward(null, 0); + return true; + } + + /** + * Starts handing the body to the get-up: from now until letGo() the physics follows the get-up pose solved onto the + * skeleton each frame (see followPose). + */ + handOver(): void { + if (!this.ragdolling) return; + this.replaying = null; + this.following = true; + this.followAge = 0; + this.accumulator = 0; + } + + /** Whether the physics is following the get-up pose on its way out. */ + get handingOver(): boolean { + return this.ragdolling && this.following; + } + + /** How long the physics has been following the get-up pose. */ + get handOverSeconds(): number { + return this.handingOver ? this.followAge : 0; + } + + /** Ends the hand-over: the physics stops where it has the body, and the get-up blends on from there (see blendRise). */ + letGo(): void { + if (!this.handingOver) return; + this.following = false; + this.body.pullToward(null, 0); + this.stop(); + } + + /** + * One frame of the hand-over, with this frame's get-up pose solved onto the skeleton. The particles are drawn toward + * that pose's while the canvas, the ropes and the bones' lengths keep them a body, and the bones go on them with + * offsets turning from the fall's toward the get-up's. What is left between the two when it stops, the get-up blends + * out (see blendRise). Turning the bones straight from where the fall left them to the get-up pose swung limbs + * 30-50 cm through the canvas. + */ + followPose(dt: number, obstacle: { readonly x: number; readonly z: number } | null): void { + if (!this.handingOver) return; + this.root.updateMatrixWorld(true); + this.read(this.followTargets); + this.prepareFrames(this.followTargets); + for (let index = 0; index < this.bones.length; index += 1) { + this.frameFor(index, this.followTargets, this.frame); + this.bones[index]!.matrixWorld.decompose(this.point, this.world, this.other); + this.followOffsets[index]!.copy(this.frame.invert()).multiply(this.world); + } + this.followAge += Math.max(0, dt); + const t = Math.min(1, this.followAge / FOLLOW_TURN_SECONDS); + const w = t * t * (3 - 2 * t); + this.body.setObstacle(obstacle?.x ?? 0, obstacle?.z ?? 0, obstacle !== null); + this.body.pullToward(this.followTargets, FOLLOW_PULL); + this.accumulator += Math.max(0, dt); + let steps = 0; + while (this.accumulator >= STEP_SECONDS && steps < MAX_STEPS_PER_UPDATE) { + this.body.step(); + this.accumulator -= STEP_SECONDS; + steps += 1; + } + if (steps === MAX_STEPS_PER_UPDATE) this.accumulator = 0; + this.body.interpolate(this.accumulator / STEP_SECONDS, this.drawn); + for (let index = 0; index < this.bones.length; index += 1) this.blendedOffsets[index]!.slerpQuaternions(this.offsets[index]!, this.followOffsets[index]!, w); + this.drive(this.drawn, this.blendedOffsets); + } + + /** Steps the physics by `dt` and writes the pose onto the skeleton. */ + update(dt: number, obstacle: { readonly x: number; readonly z: number } | null): void { + if (!this.ragdolling) return; + this.body.setObstacle(obstacle?.x ?? 0, obstacle?.z ?? 0, obstacle !== null); + this.accumulator += Math.max(0, dt); + let steps = 0; + const replaying = this.replaying; + while (this.accumulator >= STEP_SECONDS && steps < MAX_STEPS_PER_UPDATE) { + if (replaying !== null) this.replayLosses(this.body, replaying, this.body.steps); + this.body.step(replaying?.impulses); + this.accumulator -= STEP_SECONDS; + steps += 1; + } + if (steps === MAX_STEPS_PER_UPDATE) this.accumulator = 0; + // The live fall keeps where it came to rest, which the replay's end goes back to. + const record = this.lastFall; + if (replaying === null && record !== null && record.rest.steps < 0 && this.body.asleep) this.body.keepRest(record.rest); + if (replaying !== null && this.aheadOf === replaying) this.runAhead(replaying, AHEAD_STEPS); + this.age += dt; + this.body.interpolate(this.body.asleep ? 1 : this.accumulator / STEP_SECONDS, this.drawn); + this.drive(this.drawn); + if (this.age < BLEND_IN_SECONDS) this.blend(this.fromLocal, this.fromHips, this.age / BLEND_IN_SECONDS); + } + + /** + * Puts the fall at its end (after the replay cuts back to live): where the live fall came to rest, which is what the + * count showed, or failing that where the fall run on ahead of the replay did; only a fall with neither is run on here. + */ + settle(): void { + if (!this.ragdolling) return; + const replaying = this.replaying; + const rest = replaying?.rest; + if (rest !== undefined && rest.steps >= 0) this.body.restore(rest); + else { + const limit = this.body.steps + Math.ceil(RUN_ON_SECONDS / STEP_SECONDS); + while (!this.body.asleep && this.body.steps < limit) { + if (replaying !== null) this.replayLosses(this.body, replaying, this.body.steps); + this.body.step(replaying?.impulses); + } + } + this.accumulator = 0; + this.age = Math.max(this.age, BLEND_IN_SECONDS); + this.body.interpolate(1, this.drawn); + this.drive(this.drawn); + } + + /** Starts the recorded fall over and runs it to its end; false when there is none. */ + restoreSettled(): boolean { + if (this.lastFall === null) return false; + this.primed = true; + this.start("crumple"); + this.settle(); + return true; + } + + /** Stops driving the skeleton; the pose it left is kept for the get-up blend. */ + stop(): void { + this.following = false; + this.body.pullToward(null, 0); + if (this.ragdolling) { + this.root.updateMatrixWorld(true); + this.captureLocals(this.riseLocal, this.riseHips); + this.bodyTurns(this.riseTurns); + this.riseCaptured = true; + } + this.ragdolling = false; + this.replaying = null; + } + + /** Moves the pose the fall left by `world` (a world-space offset): its root moved the other way. */ + shiftRise(world: THREE.Vector3): void { + const parent = this.rig.bones.hips.parent; + if (parent === null) { + this.riseHips.add(world); + return; + } + parent.updateWorldMatrix(true, false); + const from = parent.worldToLocal(this.point.set(0, 0, 0)); + this.riseHips.add(parent.worldToLocal(this.other.copy(world)).sub(from)); + } + + /** Forgets the pose left by the last fall, so nothing blends from it. */ + clearRise(): void { + this.riseCaptured = false; + } + + /** Whether the fall left a pose for the get-up to start from. */ + get hasRisePose(): boolean { + return this.riseCaptured; + } + + /** Blends the solved get-up pose from where the fall left the body; `weight` 1 is the solved pose. */ + blendRise(weight: number): void { + if (!this.riseCaptured || weight >= 1) return; + // Each bone turns in the body's frame from where the fall left it to where the get-up has it, rather than + // joint by joint: blended against a parent that is itself still turning, a limb would swing through the canvas. + const w = Math.min(1, Math.max(0, weight)); + this.bodyTurns(this.solvedTurns); + this.rig.bones.hips.position.lerpVectors(this.riseHips, this.rig.bones.hips.position, w); + for (let index = 0; index < this.bones.length; index += 1) { + const bone = this.bones[index]!; + if (bone !== this.rig.bones.hips) bone.position.lerpVectors(this.fromPositions[index]!, bone.position, w); + bone.updateWorldMatrix(false, false); + this.rig.setWorldRotation(bone, this.turn.slerpQuaternions(this.riseTurns[index]!, this.solvedTurns[index]!, w).premultiply(this.rootTurn)); + if (DRIVEN[index] === "upperChest") { + this.rig.bones.clavicleL.updateWorldMatrix(false, false); + this.rig.bones.clavicleR.updateWorldMatrix(false, false); + } + } + this.root.updateMatrixWorld(true); + } + + /** Every driven bone's world turn under the root's, into `out`; leaves the root's turn in `rootTurn`. */ + private bodyTurns(out: readonly THREE.Quaternion[]): void { + this.root.updateMatrixWorld(true); + this.root.getWorldQuaternion(this.rootTurn); + this.turn.copy(this.rootTurn).invert(); + for (let index = 0; index < this.bones.length; index += 1) this.bones[index]!.getWorldQuaternion(out[index]!).premultiply(this.turn); + } + + /** Where the pelvis came to rest, in world space. */ + pelvis(out: THREE.Vector3): THREE.Vector3 { + return v(this.body.position, P.pelvis, out); + } + + /** The middle of the body (between pelvis and chest), in world space. */ + centre(out: THREE.Vector3): THREE.Vector3 { + v(this.body.position, P.pelvis, out); + return out.add(v(this.body.position, P.upper, this.other)).multiplyScalar(0.5); + } + + /** Points along the body on the canvas that others keep clear of: head, chest, pelvis and knees. */ + bodyPoint(index: 0 | 1 | 2 | 3 | 4, out: THREE.Vector3): THREE.Vector3 { + const particle = [P.head, P.upper, P.pelvis, P.kneeL, P.kneeR][index]!; + return v(this.body.position, particle, out); + } + + /** Which way the face points, in world space. */ + faceDirection(out: THREE.Vector3): THREE.Vector3 { + return out.copy(v(this.body.position, P.face, out)).sub(v(this.body.position, P.head, this.other)).normalize(); + } + + /** Whether the body landed face down (belly to the canvas). */ + faceDown(): boolean { + return this.body.pelvisForward(this.point).y < -0.25; + } + + /** + * Lost parts carry no weight. A live fall records the step each loss came at; a replay weighs the body as the live + * fall did, whatever has been lost since (a knockout by the count loses the head only in its replay). + */ + setLost(head: boolean, leftHand: boolean, rightHand: boolean): void { + const lost = (head ? 1 : 0) | (leftHand ? 2 : 0) | (rightHand ? 4 : 0); + if (this.replaying !== null || lost === this.lost) return; + this.lost = lost; + weigh(this.body, lost); + if (this.ragdolling && this.lastFall !== null) this.lastFall.losses.push(this.body.steps, lost); + } + + /** Weighs `body` as the record's losses have it from `step`. */ + private replayLosses(body: RagdollBody, record: FallRecord, step: number): void { + const losses = record.losses; + for (let k = 0; k < losses.length; k += 2) { + if (losses[k] !== step) continue; + if (body === this.body) this.lost = losses[k + 1]!; + weigh(body, losses[k + 1]!); + } + } + + /** + * Runs the replayed fall on ahead by up to `steps` (from its start with 0), as the replay runs it, and keeps where + * it comes to rest; it starts over if the replay gives the fall a blow the record lacked. + */ + private runAhead(record: FallRecord, steps: number): void { + const ahead = this.ahead; + if (steps === 0 || this.aheadBlows !== record.impulses.length) { + ahead.start(record.positions, record.velocities, record.style); + ahead.trackObstacle(record.obstacles, true); + weigh(ahead, 0); + this.replayLosses(ahead, record, 0); + this.aheadOf = record; + this.aheadBlows = record.impulses.length; + if (steps === 0) return; + } + for (let k = 0; k < steps && !ahead.asleep; k += 1) { + this.replayLosses(ahead, record, ahead.steps); + ahead.step(record.impulses); + } + if (!ahead.asleep) return; + ahead.keepRest(record.rest); + this.aheadOf = null; + } + + private captureLocals(out: readonly THREE.Quaternion[], hips: THREE.Vector3): void { + for (let index = 0; index < this.bones.length; index += 1) { + out[index]!.copy(this.bones[index]!.quaternion); + this.fromPositions[index]!.copy(this.bones[index]!.position); + } + hips.copy(this.rig.bones.hips.position); + } + + /** Slerps every driven bone from a stored pose toward its current pose by `weight`. */ + private blend(from: readonly THREE.Quaternion[], hips: THREE.Vector3, weight: number): void { + const w = Math.min(1, Math.max(0, weight)); + for (let index = 0; index < this.bones.length; index += 1) { + const bone = this.bones[index]!; + // From the current pose back toward the stored one: slerpQuaternions(from, bone.quaternion, w) would copy + // `from` over the bone's own quaternion first and never leave the stored pose. + bone.quaternion.slerp(from[index]!, 1 - w); + if (bone !== this.rig.bones.hips) bone.position.lerpVectors(this.fromPositions[index]!, bone.position, w); + } + this.rig.bones.hips.position.lerpVectors(hips, this.rig.bones.hips.position, w); + this.root.updateMatrixWorld(true); + } + + /** Reads the particles off the posed skeleton. */ + private read(out: Float64Array): void { + const b = this.rig.bones; + const put = (i: number, value: THREE.Vector3): void => setV(out, i, value); + const position = (bone: THREE.Bone): THREE.Vector3 => this.point.setFromMatrixPosition(bone.matrixWorld); + const axis = (bone: THREE.Bone, column: 0 | 1 | 2, target: THREE.Vector3): THREE.Vector3 => target.setFromMatrixColumn(bone.matrixWorld, column).normalize(); + put(P.pelvis, position(b.hips)); + put(P.hipL, position(b.hipL)); + put(P.hipR, position(b.hipR)); + put(P.belly, position(b.hips).addScaledVector(axis(b.hips, 2, this.axisZ), BELLY_FORWARD)); + put(P.chest, position(b.chest)); + put(P.upper, position(b.upperChest)); + put(P.neck, position(b.neck)); + put(P.shoulderL, position(b.shoulderL)); + put(P.shoulderR, position(b.shoulderR)); + put(P.head, position(b.head)); + put(P.crown, position(b.head).addScaledVector(axis(b.head, 1, this.axisY), CROWN_UP)); + put(P.face, position(b.head).addScaledVector(axis(b.head, 2, this.axisZ), FACE_FORWARD).addScaledVector(axis(b.head, 1, this.axisY), FACE_UP)); + put(P.elbowL, position(b.elbowL)); + put(P.wristL, position(b.gloveL)); + put(P.fistL, position(b.gloveL).addScaledVector(axis(b.gloveL, 1, this.axisY), FIST_REACH)); + put(P.elbowR, position(b.elbowR)); + put(P.wristR, position(b.gloveR)); + put(P.fistR, position(b.gloveR).addScaledVector(axis(b.gloveR, 1, this.axisY), FIST_REACH)); + put(P.kneeL, position(b.kneeL)); + put(P.ankleL, position(b.ankleL)); + put(P.toeL, position(b.toeL)); + put(P.heelL, position(b.ankleL).addScaledVector(axis(b.toeL, 1, this.axisY), -HEEL_BACK).addScaledVector(axis(b.toeL, 2, this.axisZ), -HEEL_DOWN)); + put(P.kneeR, position(b.kneeR)); + put(P.ankleR, position(b.ankleR)); + put(P.toeR, position(b.toeR)); + put(P.heelR, position(b.ankleR).addScaledVector(axis(b.toeR, 1, this.axisY), -HEEL_BACK).addScaledVector(axis(b.toeR, 2, this.axisZ), -HEEL_DOWN)); + } + + /** Each driven bone keeps its offset from the frame its particles make, measured at the start. */ + private computeOffsets(): void { + this.prepareFrames(this.body.position); + for (let index = 0; index < this.bones.length; index += 1) { + const bone = this.bones[index]!; + this.frameFor(index, this.body.position, this.frame); + bone.matrixWorld.decompose(this.point, this.world, this.other); + this.offsets[index]!.copy(this.frame.invert()).multiply(this.world); + } + } + + private drive(positions: Float64Array, offsets: readonly THREE.Quaternion[] = this.offsets): void { + // The poser moves the head off the neck for slips and blows; the body on the canvas has its own neck. + for (let index = 0; index < this.bones.length; index += 1) { + const bone = this.bones[index]!; + if (bone !== this.rig.bones.hips) bone.position.copy(this.rig.restLocalPosition(bone)); + } + this.root.updateMatrixWorld(true); + const hips = this.rig.bones.hips; + this.rig.setWorldPosition(hips, v(positions, P.pelvis, this.point)); + this.prepareFrames(positions); + for (let index = 0; index < this.bones.length; index += 1) { + const bone = this.bones[index]!; + this.frameFor(index, positions, this.frame); + this.world.copy(this.frame).multiply(offsets[index]!); + this.rig.setWorldRotation(bone, this.world); + if (bone === hips) this.tiltHips(positions); + // The spine bone between the hips and the chest has no particle of its own, so the chest is put on its + // particle; everything above it then lies on its particles too (the upper body's lengths are rigid). The + // limbs' roots go on theirs as well: the tilted hips carry the hip joints a little off the hip line, and + // the shoulders hang from clavicles the physics does not move. + const pinned = PINNED[index]!; + if (pinned >= 0) this.rig.setWorldPosition(bone, v(positions, pinned, this.point)); + if (DRIVEN[index] === "upperChest") { + this.rig.bones.clavicleL.updateWorldMatrix(false, false); + this.rig.bones.clavicleR.updateWorldMatrix(false, false); + } + } + this.root.updateMatrixWorld(true); + } + + /** + * The particles bend the trunk at the pelvis, the skeleton at the spine's joint a hand's width above it: the + * hips pitch about the hip line until that joint is the spine's own length from the chest particle, so the + * belly neither stretches nor squashes however far he folds (the hip joints lie on that line and stay put). + */ + private tiltHips(positions: Float64Array): void { + const hips = this.rig.bones.hips; + const origin = this.point.setFromMatrixPosition(hips.matrixWorld); + const joint = this.tiltJoint.copy(this.rig.restLocalPosition(this.rig.bones.spine)).applyMatrix4(hips.matrixWorld).sub(origin); + const chest = v(positions, P.chest, this.tiltChest).sub(origin); + const axis = this.pelvisX; + // The joint turns about the hip line: joint(a) = along + across cos a + (axis x across) sin a. Its distance + // from the chest is the spine's length where joint(a).chest = wanted. + const along = axis.dot(joint); + const across = this.tiltAcross.copy(joint).addScaledVector(axis, -along); + const wanted = (joint.lengthSq() + chest.lengthSq() - this.spineLength * this.spineLength) / 2 - along * axis.dot(chest); + const b = across.dot(chest); + const c = this.other.crossVectors(axis, across).dot(chest); + const reach = Math.hypot(b, c); + if (reach < 1e-9) return; + const phase = Math.atan2(c, b); + const spread = Math.acos(Math.min(1, Math.max(-1, wanted / reach))); + // Of the two pitches that fit, the smaller. + const first = wrapAngle(phase - spread); + const second = wrapAngle(phase + spread); + const pitch = Math.abs(first) <= Math.abs(second) ? first : second; + if (Math.abs(pitch) < 1e-5) return; + hips.getWorldQuaternion(this.world).premultiply(this.tiltTurn.setFromAxisAngle(axis, pitch)); + this.rig.setWorldRotation(hips, this.world); + } + + /** Body axes shared by every bone's frame: the pelvis, the shoulder line and the head. Call before `frameFor`. */ + private prepareFrames(p: Float64Array): void { + // Pelvis: x from the right hip to the left, y up the spine, z out through the belly. + this.segment(p, P.hipR, P.hipL, this.pelvisX); + this.axisZ.set( + p[P.belly * 3]! - (p[P.hipL * 3]! + p[P.hipR * 3]!) / 2, + p[P.belly * 3 + 1]! - (p[P.hipL * 3 + 1]! + p[P.hipR * 3 + 1]!) / 2, + p[P.belly * 3 + 2]! - (p[P.hipL * 3 + 2]! + p[P.hipR * 3 + 2]!) / 2, + ); + this.axisZ.addScaledVector(this.pelvisX, -this.axisZ.dot(this.pelvisX)).normalize(); + this.pelvisY.crossVectors(this.axisZ, this.pelvisX).normalize(); + this.segment(p, P.shoulderR, P.shoulderL, this.upperX); + this.segment(p, P.head, P.crown, this.axisX); + this.segment(p, P.head, P.face, this.other); + this.headX.crossVectors(this.axisX, this.other).normalize(); + } + + private segment(p: Float64Array, from: number, to: number, out: THREE.Vector3): THREE.Vector3 { + out.set(p[to * 3]! - p[from * 3]!, p[to * 3 + 1]! - p[from * 3 + 1]!, p[to * 3 + 2]! - p[from * 3 + 2]!); + return out.lengthSq() > 1e-12 ? out.normalize() : out.set(0, 1, 0); + } + + /** The world frame a driven bone's particles make (after `prepareFrames` for the same positions). */ + private frameFor(index: number, p: Float64Array, out: THREE.Quaternion): THREE.Quaternion { + const name = DRIVEN[index]!; + switch (name) { + case "hips": + return frameFromAxes(this.pelvisY, this.pelvisX, "x", out); + case "spine": { + // Aimed from its own joint, where the hips carry it, at the chest particle: aimed along the pelvis's line + // instead, it left the chest up to 16 cm off its particle once the body folded at the waist. + const spine = this.bones[index]!; + spine.updateWorldMatrix(false, false); + this.axisY.setFromMatrixPosition(spine.matrixWorld); + this.axisY.set(p[P.chest * 3]! - this.axisY.x, p[P.chest * 3 + 1]! - this.axisY.y, p[P.chest * 3 + 2]! - this.axisY.z); + if (this.axisY.lengthSq() > 1e-12) this.axisY.normalize(); + else this.segment(p, P.pelvis, P.chest, this.axisY); + return frameFromAxes(this.axisY, this.hint.copy(this.pelvisX).multiplyScalar(0.75).addScaledVector(this.upperX, 0.25), "x", out); + } + case "chest": + this.segment(p, P.chest, P.upper, this.axisY); + return frameFromAxes(this.axisY, this.hint.copy(this.pelvisX).multiplyScalar(0.4).addScaledVector(this.upperX, 0.6), "x", out); + case "upperChest": + this.segment(p, P.upper, P.neck, this.axisY); + return frameFromAxes(this.axisY, this.upperX, "x", out); + case "neck": + this.segment(p, P.neck, P.head, this.axisY); + return frameFromAxes(this.axisY, this.hint.copy(this.upperX).add(this.headX), "x", out); + case "head": + this.segment(p, P.head, P.crown, this.axisY); + return frameFromAxes(this.axisY, this.segment(p, P.head, P.face, this.hint), "z", out); + case "shoulderL": case "elbowL": case "gloveL": + case "shoulderR": case "elbowR": case "gloveR": { + const side = name.endsWith("L") ? 0 : 1; + const segment = SEGMENTS[index]!; + const axis = this.body.elbowAxis(side, p, this.elbowAxes[side]); + this.segment(p, segment[0], segment[1], this.axisY); + return frameFromAxes(this.axisY, axis, "x", out); + } + case "hipL": case "kneeL": case "hipR": case "kneeR": { + const side = name.endsWith("L") ? 0 : 1; + const segment = SEGMENTS[index]!; + const axis = this.body.kneeAxis(side, p, this.kneeAxes[side]); + this.segment(p, segment[0], segment[1], this.axisY); + return frameFromAxes(this.axisY, axis, "x", out); + } + default: { + const left = name === "ankleL"; + const ankle = left ? P.ankleL : P.ankleR; + const toe = left ? P.toeL : P.toeR; + const heel = left ? P.heelL : P.heelR; + this.segment(p, ankle, toe, this.axisY); + this.segment(p, heel, toe, this.hint); + this.other.set(p[ankle * 3]! - p[heel * 3]!, p[ankle * 3 + 1]! - p[heel * 3 + 1]!, p[ankle * 3 + 2]! - p[heel * 3 + 2]!); + this.hint.cross(this.other); + return frameFromAxes(this.axisY, this.hint, "x", out); + } + } + } +} diff --git a/web/hands/src/render/render.test.ts b/web/hands/src/render/render.test.ts index c19f3062..e3de4edb 100644 --- a/web/hands/src/render/render.test.ts +++ b/web/hands/src/render/render.test.ts @@ -1,13 +1,18 @@ +import { PROTOCOL_VERSION } from "../types"; import * as THREE from "three"; -import { punchTiming, totalTicks } from "../manifest"; +import { FIGHTER_RADIUS, punchTiming, REST_CORNER_OFFSET, RING_HALF_WIDTH, totalTicks } from "../manifest"; import { buildArena } from "./arena"; -import { CameraDirector, cornerFrame, cornerPoint, cornerShot, cornerShotProgress, CUTMAN_WORK_DEGREES, CUTMAN_WORK_DISTANCE } from "./camera"; -import { bloodPatternFor, Effects3D } from "./effects"; -import { decisionLabel, drawHud, FINAL_REVEAL_DELAY_SECONDS, finalRevealDelay, fitFontSize, HUD_MAX_GUARD, HUD_MAX_POISE, RoundClock, RoundStatsTracker, scoreTotal, topPanelOffset } from "./hud"; -import { buildRing, ropePress } from "./ring"; +import { CameraDirector, ceremonyShot, cornerFrame, cornerPoint, cornerShot, cornerShotProgress, CUTMAN_WORK_DEGREES, CUTMAN_WORK_DISTANCE } from "./camera"; +import { captionSlot, drawCaption } from "./caption"; +import { bloodPatternFor, Effects3D, NECK_FOUNTAIN_SECONDS } from "./effects"; +import { CLOCK_PORTRAIT_RADIUS, cardRows, decisionLabel, drawHud, finalRevealDelay as revealDelay, hudScale, lagWarning, plateDetails, REFEREE_REVEAL_DELAY_SECONDS, FINAL_REVEAL_DELAY_SECONDS, finalRevealDelay, fitFontSize, HUD_MAX_GUARD, HUD_MAX_POISE, PLATE_PORTRAIT_RADIUS, RESULT_CARD_FOOTER, resultCard, resultCardLayout, RoundClock, RoundStatsTracker, scoreTotal, topPanelOffset } from "./hud"; +import { FightRenderer } from "./renderer"; +import { buildRing, disposeRing, nearRopeOpacityFor, ROPE_FLEX_GLSL, ROPE_GIVE_GLSL, ropePress, ROPE_BACK, ROPE_MAX_GIVE, ropeExcess } from "./ring"; +import { GloveTrail } from "./trails"; import { resizeHighDpi } from "./viewport"; -import { PALETTES, worldMapping } from "./world"; -import { fighter, publicPlayers, snapshot } from "../test/fixtures"; +import { CORNER_COLORS, PALETTES, ROPE_HEIGHTS, ROPE_LINE, worldMapping } from "./world"; +import { fighter, mockHudContext, publicPlayers, snapshot, type DrawnPicture } from "../test/fixtures"; +import type { EngineSnapshot } from "../types"; const mapping = worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }); @@ -56,6 +61,32 @@ describe("scene construction", () => { expect(ring.geometries.some((geometry) => geometry.type === "PlaneGeometry")).toBe(true); }); + it("pads the corner each fighter rests in with his colour, and the other two white", () => { + const ring = buildRing(); + ring.group.updateMatrixWorld(true); + const padsIn = (color: number): THREE.Vector3[] => { + const found: THREE.Vector3[] = []; + ring.group.traverse((object) => { + if (!(object instanceof THREE.Mesh) || !(object.geometry instanceof THREE.BoxGeometry) || object.geometry.parameters.height !== 0.52) return; + if ((object.material as THREE.MeshStandardMaterial).color.equals(new THREE.Color(color))) found.push(object.getWorldPosition(new THREE.Vector3())); + }); + return found; + }; + // The engine walks seat one (blue) to (-offset, -offset) for the rest and seat two (red) to (offset, offset). + for (const [color, sign] of [[CORNER_COLORS.blue, -1], [CORNER_COLORS.red, 1]] as const) { + const pads = padsIn(color); + expect(pads).toHaveLength(2); + for (const pad of pads) { + expect(Math.sign(pad.x)).toBe(Math.sign(mapping.x(sign * REST_CORNER_OFFSET))); + expect(Math.sign(pad.z)).toBe(Math.sign(mapping.z(sign * REST_CORNER_OFFSET))); + } + } + const neutral = padsIn(CORNER_COLORS.neutral); + expect(neutral).toHaveLength(4); + for (const pad of neutral) expect(Math.sign(pad.x)).toBe(Math.sign(pad.z)); + disposeRing(ring); + }); + it("builds and animates the arena crowd deterministically", () => { const arena = buildArena(); arena.update(1.5, 1 / 60, false); @@ -66,6 +97,22 @@ describe("scene construction", () => { }); describe("camera direction", () => { + it("keeps a fighter on the canvas up in the close shot over a count, clear of the plates along the bottom", () => { + const director = new CameraDirector(); + const standing = { x: -0.2, z: -1.3 }; + const down = { x: 0.4, z: 1.1 }; + let frame = director.update(1 / 60, 0, standing, down, 2.5, true, 0, true, true, down); + for (let i = 1; i < 300; i += 1) frame = director.update(1 / 60, i / 60, standing, down, 2.5, true, 0, true, true, down); + const camera = new THREE.PerspectiveCamera(36, 16 / 9, 0.1, 80); + camera.position.copy(frame.position); + camera.lookAt(frame.lookAt); + camera.updateMatrixWorld(true); + const onScreen = new THREE.Vector3(down.x, 0.15, down.z).project(camera); + // The plates take the bottom sixth of the screen. + expect(onScreen.y).toBeGreaterThan(-0.55); + expect(Math.abs(onScreen.x)).toBeLessThan(0.6); + }); + it("follows the fighters midpoint and pushes in on knockdowns", () => { const director = new CameraDirector(); for (let i = 0; i < 300; i += 1) director.update(1 / 60, i / 60, { x: -1.5, z: 0 }, { x: 1.5, z: 0 }, 3, false, 0, true); @@ -128,10 +175,10 @@ describe("effects", () => { const full = new Effects3D(new THREE.Scene()); full.addEvent(severeHit(1), origin, false); - expect(full.liveBloodParticles).toBe(110); - expect(full.liveMist).toBe(10); - expect(full.liveGibs).toBe(11); - expect(full.visibleDecals).toBe(12); + expect(full.liveBloodParticles).toBe(140); + expect(full.liveMist).toBe(18); + expect(full.liveGibs).toBe(8); + expect(full.canvasStains).toBe(8); full.dispose(); const reduced = new Effects3D(new THREE.Scene()); @@ -140,7 +187,7 @@ describe("effects", () => { expect(reduced.liveBloodParticles).toBe(24); expect(reduced.liveMist).toBe(2); expect(reduced.liveGibs).toBe(0); - expect(reduced.visibleDecals).toBe(3); + expect(reduced.canvasStains).toBe(2); reduced.decapitate(0, new THREE.Vector3(0, 1.5, 0), new THREE.Quaternion(), 1, 2); expect(reduced.activeHeads).toBe(0); expect(reduced.activeStumps).toBe(0); @@ -154,7 +201,7 @@ describe("effects", () => { expect(off.liveBloodParticles).toBe(0); expect(off.liveMist).toBe(0); expect(off.liveGibs).toBe(0); - expect(off.visibleDecals).toBe(0); + expect(off.canvasStains).toBe(0); off.dispose(); }); @@ -205,10 +252,10 @@ describe("effects", () => { effects.decapitate(0, origin, new THREE.Quaternion(), 1, 10); const sweat = effects.liveParticles - effects.liveBloodParticles; expect(sweat).toBeGreaterThan(0); - expect(effects.liveGibs).toBe(35); + expect(effects.liveGibs).toBe(32); expect(effects.activeHeads).toBe(1); expect(effects.activeStumps).toBe(1); - expect(effects.visibleDecals).toBeGreaterThan(0); + expect(effects.canvasStains).toBeGreaterThan(0); effects.setBloodLevel("off"); expect(effects.liveParticles).toBe(sweat); @@ -217,12 +264,12 @@ describe("effects", () => { expect(effects.liveGibs).toBe(0); expect(effects.activeHeads).toBe(0); expect(effects.activeStumps).toBe(0); - expect(effects.visibleDecals).toBe(0); + expect(effects.canvasStains).toBe(0); effects.addEvent(severeHit(11), origin, false); expect(effects.liveParticles).toBeGreaterThan(sweat); expect(effects.liveBloodParticles).toBe(0); - expect(effects.visibleDecals).toBe(0); + expect(effects.canvasStains).toBe(0); effects.dispose(); }); @@ -269,7 +316,7 @@ describe("effects", () => { expect(effects.liveGibs).toBe(0); expect(effects.liveBloodParticles).toBe(0); expect(effects.liveMist).toBe(0); - expect(effects.visibleDecals).toBe(0); + expect(effects.canvasStains).toBe(0); effects.decapitate(0, position, quaternion, 1, 42); expect(effects.activeHeads).toBe(0); @@ -315,21 +362,26 @@ describe("effects", () => { it("keeps ballistic updates finite, bounded to the ring, and stains the floor", () => { const scene = new THREE.Scene(); const effects = new Effects3D(scene); + const children = scene.children.length; effects.decapitate(0, new THREE.Vector3(2.9, 0.2, 2.9), new THREE.Quaternion(), 1, 70); const initialBlood = effects.liveBloodParticles; - const initialDecals = effects.visibleDecals; + const initialDecals = effects.canvasStains; effects.anchorStump(0, new THREE.Vector3(0.4, 1.4, -0.2), new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), 0.4)); for (let i = 0; i < 10; i += 1) effects.update(1 / 60); expect(effects.liveBloodParticles).toBeGreaterThan(initialBlood); + // The severed neck pumps for NECK_FOUNTAIN_SECONDS; what follows is once it has bled out. + for (let i = 0; i < NECK_FOUNTAIN_SECONDS * 60; i += 1) effects.update(1 / 60); for (let i = 0; i < 360; i += 1) effects.update(1 / 60); - expect(effects.liveGibs).toBe(0); + // The flesh lies where it fell for the rest of the bout; the blood in the air has all come down. + expect(effects.liveGibs).toBe(24); expect(effects.liveBloodParticles).toBe(0); expect(effects.liveMist).toBe(0); expect(effects.activeHeads).toBe(1); expect(effects.activeStumps).toBe(1); - expect(effects.visibleDecals).toBeGreaterThan(initialDecals); - expect(effects.visibleDecals).toBeLessThanOrEqual(48); + expect(effects.canvasStains).toBeGreaterThan(initialDecals); + // Stains are painted into the one canvas, so however many there are the scene holds no more objects. + expect(scene.children.length).toBe(children); effects.decapitate( 1, @@ -380,7 +432,7 @@ describe("effects", () => { head: head.position.toArray(), gib: gibPosition.toArray(), particles: Array.from(effects.dropletBuffers.position.array), - decals: effects.visibleDecals, + decals: effects.canvasStains, }; effects.dispose(); return result; @@ -490,6 +542,7 @@ describe("effects", () => { it("never exceeds any fixed pool under repeated production-valid events", () => { const scene = new THREE.Scene(); const effects = new Effects3D(scene); + const children = scene.children.length; const position = new THREE.Vector3(0, 1.5, 0); const quaternion = new THREE.Quaternion(); for (let eventId = 0; eventId < 100; eventId += 1) { @@ -503,12 +556,12 @@ describe("effects", () => { expect(effects.liveParticles).toBeLessThanOrEqual(900); expect(effects.liveBloodParticles).toBeLessThanOrEqual(900); expect(effects.liveMist).toBeLessThanOrEqual(90); - expect(effects.visibleDecals).toBeLessThanOrEqual(48); - expect(effects.liveGibs).toBe(48); + expect(scene.children.length).toBe(children); + expect(effects.liveGibs).toBe(96); expect(effects.activeHeads).toBeLessThanOrEqual(2); expect(effects.activeStumps).toBeLessThanOrEqual(2); - const stains = effects.visibleDecals; + const stains = effects.canvasStains; effects.clearDynamic(); expect(effects.liveParticles).toBe(0); expect(effects.liveBloodParticles).toBe(0); @@ -516,7 +569,7 @@ describe("effects", () => { expect(effects.liveGibs).toBe(0); expect(effects.activeHeads).toBe(0); expect(effects.activeStumps).toBe(0); - expect(effects.visibleDecals).toBe(stains); + expect(effects.canvasStains).toBe(stains); effects.dispose(); expect(scene.children).toHaveLength(0); }); @@ -566,6 +619,16 @@ describe("viewport and broadcast HUD", () => { expect(HUD_MAX_POISE).toBe(600); }); + it("tags the computer's plate instead of showing a rating for it", () => { + const texts: string[] = []; + const ctx = mockHudContext(texts); + const players = Object.fromEntries(publicPlayers.map((p) => [p.id, p.id === "two" ? { ...p, cpu: true } : p])); + drawHud(ctx, 1280, 720, snapshot(), players, "one", null, 0, 30); + expect(texts).toContain("ELO 1500"); + expect(texts).toContain("CPU"); + expect(texts.filter((text) => text.startsWith("ELO "))).toHaveLength(1); + }); + it("uses the bootstrap tick rate for the authoritative HUD clock", () => { const texts: string[] = []; const ctx = mockHudContext(texts); @@ -575,14 +638,178 @@ describe("viewport and broadcast HUD", () => { }); }); -describe("rope flex", () => { - it("presses into the nearest ropes only when a fighter is within reach of them", () => { +describe("near ropes", () => { + it("are solid while the fighters are across the ring and mostly clear when they are against them", () => { + expect(nearRopeOpacityFor(-2)).toBe(1); + expect(nearRopeOpacityFor(0)).toBeGreaterThan(0.75); + expect(nearRopeOpacityFor(1.8)).toBeCloseTo(0.26, 6); + expect(nearRopeOpacityFor(1)).toBeLessThan(nearRopeOpacityFor(0)); + }); + + it("fade only on the side that faces the broadcast camera", () => { + const ring = buildRing(); + ring.setNearRopeOpacity(0.3); + expect(ring.nearRopeOpacity()).toBeCloseTo(0.3, 6); + const faded = ring.materials.filter((material) => material.transparent && material.opacity < 1); + expect(faded).toHaveLength(3 + 1); + ring.group.updateMatrixWorld(true); + const box = new THREE.Box3(); + ring.group.traverse((object) => { + if (!(object instanceof THREE.Mesh) || !faded.includes(object.material as THREE.Material)) return; + box.setFromObject(object); + expect(box.min.z).toBeGreaterThan(2.3); + }); + ring.setNearRopeOpacity(4); + expect(ring.nearRopeOpacity()).toBe(1); + expect(faded.every((material) => material.visible)).toBe(true); + ring.setNearRopeOpacity(0); + expect(faded.every((material) => !material.visible)).toBe(true); + expect(ring.materials.filter((material) => !material.visible)).toHaveLength(faded.length); + disposeRing(ring); + }); + + it("blend last and write no depth while faded, so the blob shadows and glove trails seen through them are not cut", () => { + const ring = buildRing(); + ring.setNearRopeOpacity(0.26); + const faded = ring.materials.filter((material) => material.transparent && material.opacity < 1); + expect(faded).toHaveLength(3 + 1); + for (const material of faded) expect(material.depthWrite).toBe(false); + const trail = new GloveTrail(new THREE.Color(0xffffff)); + const blobShadowOrder = 1; + let near = 0; + ring.group.traverse((object) => { + if (!(object instanceof THREE.Mesh)) return; + if (faded.includes(object.material as THREE.Material)) { + near += 1; + expect(object.renderOrder).toBeGreaterThan(Math.max(trail.mesh.renderOrder, blobShadowOrder)); + } else { + expect(object.renderOrder).toBe(0); + } + }); + // Three ropes and two straps on the side that faces the broadcast camera. + expect(near).toBe(5); + // Solid again for the shots from inside the ring, they write depth as before. + ring.setNearRopeOpacity(1); + for (const material of faded) expect(material.depthWrite).toBe(true); + disposeRing(ring); + }); +}); + +describe("rope give", () => { + it("leaves the ropes alone until a fighter's back reaches them, and gives no further than their furthest give", () => { expect(ropePress(0, 0)).toEqual({ pressX: 0, pressZ: 0 }); - expect(ropePress(2.82, 0).pressX).toBeGreaterThan(0.95); - expect(ropePress(2.82, 0).pressZ).toBe(0); - expect(ropePress(0, -2.82).pressZ).toBeGreaterThan(0.95); - expect(ropePress(2.6, 0).pressX).toBeGreaterThan(0.1); - expect(ropePress(2.6, 0).pressX).toBeLessThan(0.6); + expect(ropePress(ROPE_LINE - ROPE_BACK - 0.01, 0.4)).toEqual({ pressX: 0, pressZ: 0 }); + expect(ropePress(-(ROPE_LINE - 0.1), 0).pressX).toBeCloseTo(ROPE_BACK - 0.1, 9); + expect(ropePress(0, -2.82).pressZ).toBeCloseTo(ROPE_MAX_GIVE, 9); + expect(ropePress(0, -2.82).pressX).toBe(0); + }); + + /** The shader's own function, run as JavaScript: its scalar GLSL is valid once the types are dropped. */ + const ropeGive = new Function(` + const smoothstep = (a, b, x) => { const t = Math.min(1, Math.max(0, (x - a) / (b - a))); return t * t * (3 - 2 * t); }; + const abs = Math.abs; + ${ROPE_GIVE_GLSL.replace(/\bfloat (\w+)\((float \w+(?:, )?)+\)/u, (signature) => signature.replace(/^float /u, "function ").replaceAll("float ", "")).replace(/\bfloat (?=\w+\s*=)/gu, "let ")} + return ropeGive; + `)() as (along: number, position: number, height: number) => number; + + it("moves each part of a rope by the most any fighter pushes it there", () => { + expect(ROPE_FLEX_GLSL).toContain("ropeFlex = max(ropeFlex, press * sameSide * ropeGive(ropeAlong - along, ropeAlong, ropeWorld.y));"); + expect(ROPE_FLEX_GLSL).toContain("transformed += ropeOut * ropeFlex;"); + }); + + it("keeps the top ropes on the back of a fighter anywhere the engine lets one stand, drawn in where they can give no more", () => { + const limit = mapping.x(RING_HALF_WIDTH - FIGHTER_RADIUS); + // The engine lets his middle reach 0.36 m past the rope line. + expect(limit - ROPE_LINE).toBeGreaterThan(0.35); + for (const x of [2.1, 2.3, 2.5, 2.7, limit]) { + const drawn = x - ropeExcess(x, 0.3, { x: 0, z: 0 }).x; + expect(drawn + ROPE_BACK).toBeLessThanOrEqual(ROPE_LINE + ROPE_MAX_GIVE + 1e-9); + for (const along of [-1.6, 0, 1.2]) { + for (const height of [0.88, 1.26]) { + const rope = ROPE_LINE + ropePress(x, along).pressX * ropeGive(0, along, height); + expect(rope).toBeGreaterThanOrEqual(drawn + ROPE_BACK - 1e-9); + const beside = ROPE_LINE + ropePress(x, along).pressX * ropeGive(0.2, along + 0.2, height); + expect(beside).toBeGreaterThanOrEqual(drawn + ROPE_BACK - 1e-9); + } + } + } + }); + + it("never bows a rope as far as the next one, so the ropes do not wrap round a fighter on them", () => { + const limit = mapping.x(RING_HALF_WIDTH - FIGHTER_RADIUS); + let furthest = 0; + for (const height of ROPE_HEIGHTS) furthest = Math.max(furthest, ropePress(limit, limit).pressX * ropeGive(0, 0, height), ropePress(limit, limit).pressZ * ropeGive(0, 0, height)); + expect(furthest).toBeLessThanOrEqual(0.25); + expect(furthest).toBeLessThan(ROPE_HEIGHTS[1] - ROPE_HEIGHTS[0]); + expect(ropeExcess(limit, -limit, { x: 0, z: 0 })).toEqual({ x: expect.closeTo(limit + ROPE_BACK - ROPE_LINE - ROPE_MAX_GIVE, 9), z: expect.closeTo(-(limit + ROPE_BACK - ROPE_LINE - ROPE_MAX_GIVE), 9) }); + expect(ropeExcess(ROPE_LINE + ROPE_MAX_GIVE - ROPE_BACK - 1e-6, 0, { x: 1, z: 1 })).toEqual({ x: 0, z: 0 }); + }); + + it("gives a little less at the bottom rope, nothing at the posts and nothing away from the fighter", () => { + expect(ropeGive(0, 0, 0.5)).toBeCloseTo(0.9, 9); + expect(ropeGive(0, 0, 1.26)).toBe(1); + expect(ropeGive(0, ROPE_LINE, 1.26)).toBeCloseTo(0, 9); + expect(ropeGive(0, -ROPE_LINE, 1.26)).toBeCloseTo(0, 9); + expect(ropeGive(1.2, 0, 1.26)).toBe(0); + expect(ropeGive(0.6, 0, 1.26)).toBeGreaterThan(0.3); + expect(ropeGive(0.6, 0, 1.26)).toBeLessThan(0.9); + }); + + it("gives the straps that tie the ropes together the ropes' own give", () => { + const ring = buildRing(); + const ropes: THREE.Material[] = []; + const straps: THREE.Mesh[] = []; + ring.group.traverse((object) => { + if (!(object instanceof THREE.Mesh)) return; + if (object.geometry.type === "TubeGeometry") ropes.push(object.material as THREE.Material); + if (object.geometry.type === "BoxGeometry" && (object.geometry as THREE.BoxGeometry).parameters.width === 0.035) straps.push(object); + }); + const flex = (ropes[0] as THREE.MeshStandardMaterial).onBeforeCompile.toString(); + for (const strap of straps) expect((strap.material as THREE.MeshStandardMaterial).onBeforeCompile.toString()).toBe(flex); + expect(new Set(straps.map((strap) => strap.geometry)).size).toBe(1); + }); + + it("ties the ropes with one strap at each tie, from under the bottom rope to over the top one, that bends with them", () => { + const ring = buildRing(); + ring.group.updateMatrixWorld(true); + const straps: THREE.Mesh[] = []; + ring.group.traverse((object) => { + if (object instanceof THREE.Mesh && object.geometry.type === "BoxGeometry" && (object.geometry as THREE.BoxGeometry).parameters.width === 0.035) straps.push(object); + }); + // Two ties on each side; a strap per tie rather than three overlapping ones, which banded as the near side faded. + expect(straps).toHaveLength(8); + expect(new Set(straps.map((strap) => `${strap.position.x.toFixed(3)},${strap.position.z.toFixed(3)}`)).size).toBe(8); + const ropeRadius = 0.028; + const box = new THREE.Box3(); + for (const strap of straps) { + box.setFromObject(strap); + expect(box.min.y).toBeLessThan(ROPE_HEIGHTS[0] - 0.02 - ropeRadius); + expect(box.max.y).toBeGreaterThan(ROPE_HEIGHTS[2] - 0.02 + ropeRadius); + } + expect((straps[0]!.geometry as THREE.BoxGeometry).parameters.heightSegments).toBeGreaterThanOrEqual(4); + disposeRing(ring); + }); + + it("casts the ropes' shadows from where they have given, not from where they hang", () => { + const ring = buildRing(); + const casting: THREE.Mesh[] = []; + ring.group.traverse((object) => { + if (object instanceof THREE.Mesh && object.geometry.type === "TubeGeometry" && object.castShadow) casting.push(object); + }); + expect(casting).toHaveLength(12); + const depth = casting[0]!.customDepthMaterial; + expect(depth).toBeInstanceOf(THREE.MeshDepthMaterial); + for (const rope of casting) expect(rope.customDepthMaterial).toBe(depth); + // It is the ring's to free, with its other materials. + expect(ring.materials).toContain(depth); + const shader = { uniforms: {} as Record, vertexShader: "#include \nvoid main() {\n#include \n#include \n}", fragmentShader: "" }; + depth!.onBeforeCompile(shader as unknown as THREE.WebGLProgramParametersWithUniforms, null as unknown as THREE.WebGLRenderer); + expect(shader.vertexShader).toContain(ROPE_GIVE_GLSL); + expect(shader.vertexShader).toContain(ROPE_FLEX_GLSL); + expect(shader.vertexShader.indexOf(ROPE_FLEX_GLSL)).toBeLessThan(shader.vertexShader.indexOf("#include ")); + expect(shader.uniforms.uRopeContactA!.value).toBe(ring.ropeContacts[0]); + expect(shader.uniforms.uRopeContactB!.value).toBe(ring.ropeContacts[1]); + disposeRing(ring); }); it("writes fighter contacts into the rope shader uniforms and clears them", () => { @@ -590,7 +817,8 @@ describe("rope flex", () => { ring.setRopeContacts({ x: 2.82, z: 0.4 }, null); expect(ring.ropeContacts[0].x).toBeCloseTo(2.82); expect(ring.ropeContacts[0].y).toBeCloseTo(0.4); - expect(ring.ropeContacts[0].z).toBeGreaterThan(0.95); + expect(ring.ropeContacts[0].z).toBeCloseTo(ROPE_MAX_GIVE, 9); + expect(ring.ropeContacts[0].w).toBe(0); expect(ring.ropeContacts[1].toArray()).toEqual([0, 0, 0, 0]); ring.setRopeContacts(null, null); expect(ring.ropeContacts[0].toArray()).toEqual([0, 0, 0, 0]); @@ -608,13 +836,40 @@ describe("round stats", () => { tracker.record(event("hit", "one")); tracker.record(event("punch_start", "two")); tracker.record(event("counter_hit", "two")); - tracker.record(event("block", "two")); - expect(tracker.get("one")).toEqual({ thrown: 2, landed: 1 }); - expect(tracker.get("two")).toEqual({ thrown: 1, landed: 1 }); + // A punch the guard took comes as a block and a hit of the same punch, never a lone block. + const guarded = [{ ...event("block", "one"), target_id: "two", action_id: "two@9" }, { ...event("hit", "two", "jab:head"), target_id: "one", action_id: "two@9" }]; + for (const part of guarded) tracker.record(part, guarded); + expect(tracker.get("one")).toMatchObject({ thrown: 2, landed: 1 }); + expect(tracker.get("two")).toMatchObject({ thrown: 1, landed: 1 }); tracker.record(event("bell", "", "round_end")); - expect(tracker.get("one")).toEqual({ thrown: 2, landed: 1 }); + expect(tracker.get("one")).toMatchObject({ thrown: 2, landed: 1 }); tracker.record(event("bell", "", "round_start")); - expect(tracker.get("one")).toEqual({ thrown: 0, landed: 0 }); + expect(tracker.get("one")).toEqual({ thrown: 0, landed: 0, jabsThrown: 0, jabsLanded: 0 }); + }); + + it("does not count a punch the guard took as landed", () => { + const tracker = new RoundStatsTracker(); + const thrown = { ...event("punch_start", "one", "right:straight:head"), action_id: "p1" }; + const block = { ...event("block", "two"), target_id: "one", action_id: "p1" }; + const leaked = { ...event("hit", "one", "straight:head"), target_id: "two", action_id: "p1" }; + const clean = { ...event("hit", "one", "straight:head"), target_id: "two", action_id: "p2" }; + tracker.record(thrown, [thrown]); + tracker.record(block, [block, leaked]); + tracker.record(leaked, [block, leaked]); + expect(tracker.get("one")).toMatchObject({ thrown: 1, landed: 0 }); + tracker.record(clean, [clean]); + expect(tracker.get("one")).toMatchObject({ landed: 1 }); + }); + + it("counts jabs apart from power punches", () => { + const tracker = new RoundStatsTracker(); + tracker.record(event("punch_start", "one", "left:jab:head")); + tracker.record(event("hit", "one", "jab:head")); + tracker.record(event("punch_start", "one", "right:straight:head")); + tracker.record(event("punch_start", "one", "left:hook:body")); + tracker.record(event("counter_hit", "one", "hook:body")); + expect(tracker.get("one")).toEqual({ thrown: 3, landed: 2, jabsThrown: 1, jabsLanded: 1 }); + expect(tracker.total("one")).toEqual({ thrown: 3, landed: 2, jabsThrown: 1, jabsLanded: 1 }); }); it("keeps bout totals across rounds and prints them on the result panel", () => { @@ -624,14 +879,15 @@ describe("round stats", () => { tracker.record(event("bell", "", "round_start")); tracker.record(event("punch_start", "one")); tracker.record(event("punch_start", "two")); - expect(tracker.get("one")).toEqual({ thrown: 1, landed: 0 }); - expect(tracker.total("one")).toEqual({ thrown: 2, landed: 1 }); + expect(tracker.get("one")).toMatchObject({ thrown: 1, landed: 0 }); + expect(tracker.total("one")).toMatchObject({ thrown: 2, landed: 1 }); const texts: string[] = []; const ctx = mockHudContext(texts); const players = Object.fromEntries(publicPlayers.map((player) => [player.id, player])); - const final = { version: 3 as const, type: "final" as const, match_id: "m", winner_id: "one", method: "decision" as const, round: 3, scorecards: [], ratings: {} }; + const final = { version: PROTOCOL_VERSION, type: "final" as const, match_id: "m", winner_id: "one", method: "decision" as const, round: 3, scorecards: [], ratings: {} }; drawHud(ctx, 1280, 720, { ...snapshot(), phase: "complete" }, players, "one", final, 0, 30, tracker); - expect(texts.some((text) => text.includes("One 1/2 landed") && text.includes("Two 0/1 landed"))).toBe(true); + const row = texts.indexOf("TOTAL PUNCHES"); + expect(texts.slice(row + 1, row + 3)).toEqual(["1/2 (50%)", "0/1 (0%)"]); }); it("draws the round callout only while the renderer asks for it", () => { @@ -645,15 +901,83 @@ describe("round stats", () => { expect(texts.filter((text) => text === "ROUND 2").length).toBe(1); }); - it("shows the local fighter's input latency readout", () => { + it("puts each fighter's record on the plate, and a first bout as a debut", () => { + const texts: string[] = []; + const players = Object.fromEntries(publicPlayers.map((player, index) => [player.id, { ...player, record: index === 0 ? { wins: 12, losses: 3, draws: 1, knockouts: 8 } : { wins: 0, losses: 0, draws: 0, knockouts: 0 } }])); + drawHud(mockHudContext(texts), 1280, 720, snapshot(), players, "one", null, 0, 30); + expect(texts.some((text) => text.startsWith("12-3-1 · ELO"))).toBe(true); + expect(texts.some((text) => text.startsWith("DEBUT · ELO"))).toBe(true); + }); + + it("puts a fighter's style on the plate, and nothing for a balanced one", () => { + const texts: string[] = []; + const players = Object.fromEntries(publicPlayers.map((player) => [player.id, player])); + const styled = snapshot(); + drawHud(mockHudContext(texts), 1280, 720, { ...styled, fighters: [{ ...styled.fighters[0], style: "counter_puncher" }, styled.fighters[1]] }, players, "one", null, 0, 30); + expect(texts.some((text) => text.startsWith("COUNTER · ELO"))).toBe(true); + expect(texts.filter((text) => text.startsWith("ELO "))).toHaveLength(1); + expect(texts.some((text) => text.includes("BALANCED"))).toBe(false); + }); + + it("warns the local fighter of a slow connection and otherwise keeps latency off the broadcast", () => { const texts: string[] = []; const ctx = mockHudContext(texts); const players = Object.fromEntries(publicPlayers.map((player) => [player.id, player])); drawHud(ctx, 1280, 720, snapshot(), players, "one", null, 0, 30, null, null, 48.4); - expect(texts).toContain("INPUT 48 ms"); + expect(texts.some((text) => /INPUT|SLOW CONNECTION/u.test(text))).toBe(false); + drawHud(ctx, 1280, 720, snapshot(), players, "one", null, 0, 30, null, null, 180.2); + expect(texts).toContain("SLOW CONNECTION 180 ms"); texts.length = 0; - drawHud(ctx, 1280, 720, snapshot(), players, null, null, 0, 30, null, null, 48.4); - expect(texts.some((text) => text.startsWith("INPUT"))).toBe(false); + drawHud(ctx, 1280, 720, snapshot(), players, null, null, 0, 30, null, null, 180.2); + expect(texts.some((text) => text.startsWith("SLOW"))).toBe(false); + texts.length = 0; + drawHud(ctx, 390, 844, snapshot(), players, "one", null, 0, 30, null, null, 180.2); + expect(texts.some((text) => text.startsWith("SLOW"))).toBe(false); + }); + + it("puts the slow-connection warning where the touch pads never reach, and leaves the replay's tag its corner", () => { + const players = Object.fromEntries(publicPlayers.map((player) => [player.id, player])); + const warning = (width: number, height: number, touch: boolean, replay: string | null = null) => { + const lines: { text: string; x: number; y: number; align: CanvasTextAlign }[] = []; + let align: CanvasTextAlign = "left"; + const ctx = Object.assign(mockHudContext([]), { fillText: (text: string, x: number, y: number) => lines.push({ text, x, y, align }) }); + Object.defineProperty(ctx, "textAlign", { set: (value: CanvasTextAlign) => { align = value; } }); + drawHud(ctx, width, height, snapshot(), players, "one", null, 0, 30, null, replay, 180.2, null, null, null, null, touch); + return lines.find((line) => line.text.startsWith("SLOW CONNECTION")); + }; + // On a phone on its side the pads (style.css) come 202 px in from the right edge, 336 px at their foot, + // from 118 + 196 px up: at 640x360 to 844x390 that is over the top-right corner where the warning was. + for (const [width, height] of [[640, 360], [667, 375], [740, 360], [844, 390], [932, 430]] as const) { + const shown = warning(width, height, true)!; + expect(shown, `${width}x${height}`).toMatchObject({ x: 24, y: 76, align: "left" }); + expect(shown.x + shown.text.length * 7).toBeLessThan(width - 336); + } + expect(warning(1280, 720, false)).toMatchObject({ x: 1280 - 24, y: 76, align: "right" }); + expect(warning(844, 390, true, "KNOCKOUT REPLAY")).toBeUndefined(); + expect(warning(1280, 720, false, "KNOCKOUT REPLAY")).toBeDefined(); + }); + + it("raises the slow-connection warning at 120 ms and lowers it only under 100 ms, so it cannot flicker", () => { + let shown = false; + expect([119, 125, 110, 105, 99.5, 112, 121].map((latency) => (shown = lagWarning(latency, shown)))).toEqual([false, true, true, true, false, false, true]); + expect(lagWarning(null, true)).toBe(false); + // The renderer keeps the latency it shows on screen to the warning's own state. + const players = Object.fromEntries(publicPlayers.map((player) => [player.id, player])); + const texts: string[] = []; + const ctx = mockHudContext(texts); + const renderer = Object.assign(Object.create(FightRenderer.prototype) as Record, { + hudCanvas: { getBoundingClientRect: () => ({ width: 1280, height: 720 }), width: 0, height: 0, getContext: () => ctx }, + hudViewport: { width: 0, height: 0 }, viewerHitFlash: 0, settings: () => ({ reducedMotion: false }), players, viewerId: "one", final: null, + frameSeconds: 0, finalRevealAt: 0, reconnectMs: 0, simulation: { tick_rate: 30 }, roundStats: new RoundStatsTracker(), replay: null, + roundCalloutUntil: 0, roundCalloutRound: 0, eventCallout: null, roundClock: { ticks: () => null }, avatars: { get: () => null }, drawCaption: () => undefined, + }) as unknown as { setInputLatency(milliseconds: number | null): void; drawHudOverlay(snapshot: EngineSnapshot): void }; + const shownAt = (latency: number): string | undefined => { + texts.length = 0; + renderer.setInputLatency(latency); + renderer.drawHudOverlay(snapshot()); + return texts.find((text) => text.startsWith("SLOW CONNECTION")); + }; + expect([119, 125, 105, 95].map(shownAt)).toEqual([undefined, "SLOW CONNECTION 125 ms", "SLOW CONNECTION 105 ms", undefined]); }); it("shows the landed counts on the rest panel", () => { @@ -663,14 +987,150 @@ describe("round stats", () => { tracker.record(event("punch_start", "one")); tracker.record(event("hit", "one")); drawHud(ctx, 1280, 720, { ...snapshot(), phase: "rest" }, Object.fromEntries(publicPlayers.map((player) => [player.id, player])), "one", null, 0, 30, tracker); - expect(texts.some((text) => text.includes("One 1/1"))).toBe(true); + expect(texts.some((text) => text.includes("Blue 1 of 1"))).toBe(true); + }); +}); + +describe("the broadcast HUD after the QA pass", () => { + const players = Object.fromEntries(publicPlayers.map((player) => [player.id, player])); + const down = (count: number, viewerDown: boolean) => ({ ...snapshot(), phase: "knockdown" as const, fighters: [{ ...fighter("one", -100), is_downed: viewerDown, get_up_count: count, get_up_prompt: viewerDown ? ("get_up_left" as const) : null }, { ...fighter("two", 100), is_downed: !viewerDown, get_up_count: count }] as const }); + + it("writes the plate detail plainly, with warnings and points taken only when there are any", () => { + const record = { wins: 19, losses: 5, draws: 1, knockouts: 12 }; + const clean = { style: "balanced" as const, knockdowns: 0, warnings: 0, deductions: 0 }; + const fouled = { style: "counter_puncher" as const, knockdowns: 1, warnings: 2, deductions: 1 }; + expect(plateDetails({ ...publicPlayers[0]!, record }, clean, false)).toEqual(["19-5-1 · ELO 1500", "ELO 1500"]); + expect(plateDetails({ ...publicPlayers[0]!, cpu: true, record }, { ...fouled, style: "balanced" }, false)[0]).toBe("19-5-1 · CPU · 1 KD · 2 WARNINGS · −1 PT"); + // Too long for its plate, a line drops the record, then the rating, then shortens the counts, then drops the style. + expect(plateDetails({ ...publicPlayers[0]!, record }, fouled, false)).toEqual([ + "COUNTER · 19-5-1 · ELO 1500 · 1 KD · 2 WARNINGS · −1 PT", + "COUNTER · ELO 1500 · 1 KD · 2 WARNINGS · −1 PT", + "COUNTER · 1 KD · 2 WARNINGS · −1 PT", + "COUNTER · 1 KD · W2 · −1", + "1 KD · W2 · −1", + ]); + // A phone's plate is the style and the counts, which it keeps all of. + expect(plateDetails({ ...publicPlayers[0]!, record }, fouled, true)).toEqual(["COUNTER · 1 KD · W2 · −1", "1 KD · W2 · −1"]); + expect(plateDetails({ ...publicPlayers[0]!, record }, clean, true)).toEqual([]); + }); + + it("keeps the plate detail inside its plate on a phone", () => { + const texts: string[] = []; + const long = Object.fromEntries(publicPlayers.map((player) => [player.id, { ...player, record: { wins: 123, losses: 45, draws: 6, knockouts: 78 } }])); + const widths: number[] = []; + const ctx = mockHudContext(texts); + const measure = ctx.measureText.bind(ctx); + ctx.measureText = (text: string) => { const metrics = measure(text); widths.push(metrics.width); return metrics; }; + drawHud(ctx, 390, 844, { ...snapshot(), fighters: [{ ...fighter("one"), knockdowns: 2 }, { ...fighter("two"), knockdowns: 1 }] as const }, long, "one", null, 0, 30); + const details = texts.filter((text) => / KD$/u.test(text)); + expect(details).toEqual(["2 KD", "1 KD"]); + for (const detail of details) expect(ctx.measureText(detail).width).toBeLessThanOrEqual((390 - 56) / 2 - 24); + }); + + it("keeps the knockdown headline below the top bar and the round card, and the commentary caption off the count, on every screen", () => { + const line = { speaker: "play" as const, text: "Down goes Two! What a right hand from One, and the referee starts the count!", priority: 95, urgent: true, hold: 3, card: null }; + for (const [width, height, top] of [[844, 390, 42], [390, 844, 112], [1280, 720, 42], [320, 568, 112], [360, 640, 112], [375, 667, 112], [360, 740, 112], [375, 812, 112]] as const) { + const ctx = mockHudContext([]); + const ys: number[] = []; + let count = 0; + let font = ""; + Object.defineProperty(ctx, "font", { get: () => font, set: (value: string) => { font = value; } }); + const fill = ctx.fillText.bind(ctx); + ctx.fillText = (text: string, x: number, y: number) => { + if (text === "KNOCKDOWN" && font.includes("44px")) ys.push(y); + if (text.startsWith("COUNT") && font.includes("36px")) count = y; + fill(text, x, y); + }; + drawHud(ctx, width, height, down(3, false), players, "one", null, 0, 30); + expect(ys.length).toBeGreaterThan(0); + for (const y of ys) expect(y - 36).toBeGreaterThanOrEqual(top); + // The caption is drawn after the HUD, over it: on a phone it starts under the count's figures or not at all. + for (const touch of [false, true]) { + const slot = captionSlot({ width, height, phase: "knockdown", resultTop: null, touch, hint: false, viewerDown: false, replay: false }); + if (slot === null) continue; + const plates: number[] = []; + drawCaption(Object.assign(mockHudContext([]), { fillRect: (_x: number, y: number) => plates.push(y) }), { line, opacity: 1, entering: 0 }, slot, true); + expect(plates[0], `${width}x${height}${touch ? " with the pads" : ""}`).toBeGreaterThanOrEqual(count + 4); + } + } + }); + + it("names the fighter who beat the count through the rest of the eight, and never the viewer for want of one", () => { + const named = Object.fromEntries(publicPlayers.map((player) => [player.id, { ...player, name: player.id === "one" ? "Azure Vector" : "Crimson Geometry" }])); + // Up at the count of three, the referee counts on to eight with the phase still the knockdown's. + const standing = { ...snapshot(), phase: "knockdown" as const, fighters: [{ ...fighter("one", -100), get_up_count: 6 }, { ...fighter("two", 100), get_up_count: 6 }] as const }; + const countLines = (texts: string[]) => texts.filter((text) => /IS DOWN$|BEAT THE COUNT$|^STANDING EIGHT$/u.test(text)); + for (const viewer of ["one", "two", null]) { + const texts: string[] = []; + drawHud(mockHudContext(texts), 390, 844, standing, named, viewer, null, 0, 30, null, null, null, null, null, null, "two"); + expect(countLines(texts), `viewer ${viewer}`).toEqual(["CRIMSON GEOMETRY BEAT THE COUNT"]); + expect(texts).toContain("COUNT 6"); + // A spectator who arrived after the knockdown saw nobody go down, and is told no name. + const late: string[] = []; + drawHud(mockHudContext(late), 390, 844, standing, named, viewer, null, 0, 30); + expect(countLines(late), `viewer ${viewer}`).toEqual(["STANDING EIGHT"]); + } + // While he is on the canvas the snapshot says who it is. + const texts: string[] = []; + drawHud(mockHudContext(texts), 390, 844, { ...standing, fighters: [standing.fighters[0], { ...standing.fighters[1], is_downed: true }] as const }, named, "one", null, 0, 30, null, null, null, null, null, null, "one"); + expect(countLines(texts)).toEqual(["CRIMSON GEOMETRY IS DOWN"]); + }); + + it("leaves the knockdown banner off the knockout replay", () => { + const texts: string[] = []; + drawHud(mockHudContext(texts), 1280, 720, down(3, false), players, "one", null, 0, 30, null, "KNOCKOUT REPLAY"); + expect(texts).toContain("KNOCKOUT REPLAY"); + expect(texts.some((text) => text.endsWith("IS DOWN") || text.startsWith("COUNT"))).toBe(false); + }); + + it("starts the count at one, as a referee does", () => { + const watching: string[] = []; + drawHud(mockHudContext(watching), 1280, 720, down(0, false), players, "one", null, 0, 30); + expect(watching.some((text) => text.startsWith("COUNT"))).toBe(false); + const downed: string[] = []; + drawHud(mockHudContext(downed), 1280, 720, down(0, true), players, "one", null, 0, 30); + expect(downed).toContain("GET UP!"); + expect(downed.some((text) => text === "COUNT 0")).toBe(false); + const later: string[] = []; + drawHud(mockHudContext(later), 1280, 720, down(1, false), players, "one", null, 0, 30); + expect(later).toContain("COUNT 1"); + }); + + it("freezes the clock where a bout was stopped instead of running it to zero", () => { + const clock = new RoundClock(); + expect(clock.ticks({ ...snapshot(), phase: "fight", round_number: 2, phase_ticks_remaining: 2880 })).toBe(2880); + expect(clock.ticks({ ...snapshot(), phase: "complete", round_number: 2, phase_ticks_remaining: 0 })).toBe(2880); + const texts: string[] = []; + drawHud(mockHudContext(texts), 1280, 720, { ...snapshot(), phase: "complete", round_number: 2, phase_ticks_remaining: 0 }, players, "one", null, 0, 30, null, null, null, null, 2880); + expect(texts).toContain("1:36"); + expect(texts).toContain("FINAL"); + expect(texts).not.toContain("COMPLETE"); + }); + + it("gives the referee a moment before a disqualification or a doctor's stoppage is shown", () => { + const final = (method: string) => ({ version: PROTOCOL_VERSION, type: "final" as const, match_id: "m", winner_id: "one", method: method as "decision", round: 1, scorecards: [], ratings: {} }); + expect(revealDelay(final("disqualification"))).toBe(REFEREE_REVEAL_DELAY_SECONDS); + expect(revealDelay(final("doctor_stoppage"))).toBe(REFEREE_REVEAL_DELAY_SECONDS); + expect(revealDelay(final("decision"))).toBe(0); + }); + + it("draws the HUD larger on a big screen and the same on a 1280 x 720 one", () => { + expect(hudScale(1280, 720)).toBe(1); + expect(hudScale(800, 450)).toBe(1); + expect(hudScale(1920, 1080)).toBe(1.5); + expect(hudScale(3840, 2160)).toBe(1.6); + const scales: number[] = []; + const ctx = mockHudContext([]); + ctx.scale = (x: number) => { scales.push(x); }; + drawHud(ctx, 1920, 1080, snapshot(), players, "one", null, 0, 30); + expect(scales).toEqual([1.5]); }); }); describe("decision label", () => { const card = (one: number[], two: number[], judge = "J") => ({ judge, player_one: one, player_two: two }); const final = (method: "decision" | "draw" | "ko" | "tko", scorecards: ReturnType[]) => - ({ version: 3 as const, type: "final" as const, match_id: "m", winner_id: method === "draw" ? null : "one", method, round: 3, scorecards, ratings: {} }); + ({ version: PROTOCOL_VERSION, type: "final" as const, match_id: "m", winner_id: method === "draw" ? null : "one", method, round: 3, scorecards, ratings: {} }); it("names unanimous, split and majority decisions and draws from the scorecards", () => { expect(decisionLabel(final("decision", [card([10, 10, 10], [9, 9, 9]), card([10, 10, 9], [9, 9, 10]), card([10, 10, 10], [9, 9, 9])]))).toBe("UNANIMOUS DECISION"); @@ -685,7 +1145,7 @@ describe("decision label", () => { }); describe("final reveal", () => { - const final = { version: 3 as const, type: "final" as const, match_id: "m", winner_id: "one", method: "ko" as const, round: 1, scorecards: [], ratings: {} }; + const final = { version: PROTOCOL_VERSION, type: "final" as const, match_id: "m", winner_id: "one", method: "ko" as const, round: 1, scorecards: [], ratings: {} }; it("holds stoppage results back for the slow-motion fall and shows decisions at once", () => { expect(finalRevealDelay(final)).toBe(FINAL_REVEAL_DELAY_SECONDS); expect(finalRevealDelay({ ...final, method: "tko" })).toBe(FINAL_REVEAL_DELAY_SECONDS); @@ -695,34 +1155,6 @@ describe("final reveal", () => { }); }); -function mockHudContext(texts: string[]): CanvasRenderingContext2D { - const gradient = { addColorStop: () => {} }; - return { - save: () => {}, - restore: () => {}, - beginPath: () => {}, - closePath: () => {}, - moveTo: () => {}, - lineTo: () => {}, - fill: () => {}, - stroke: () => {}, - fillRect: () => {}, - strokeRect: () => {}, - clearRect: () => {}, - fillText: (text: string) => texts.push(text), - strokeText: () => undefined, - measureText: (text: string) => ({ width: text.length * 7 }), - createLinearGradient: () => gradient, - createRadialGradient: () => gradient, - set fillStyle(_value: unknown) {}, - set strokeStyle(_value: unknown) {}, - set font(_value: string) {}, - set lineWidth(_value: number) {}, - set textAlign(_value: CanvasTextAlign) {}, - set textBaseline(_value: CanvasTextBaseline) {}, - } as unknown as CanvasRenderingContext2D; -} - describe("fitFontSize", () => { it("returns the largest size that fits and the minimum when nothing does", () => { const measure = (size: number) => size * 10; @@ -768,6 +1200,34 @@ describe("corner shots", () => { }); }); +describe("announcement shot", () => { + const visible = (shot: { distance: number; height: number }, fov: number, fromTop: number): number => + shot.height + (0.5 - fromTop) * 2 * shot.distance * Math.tan(THREE.MathUtils.degToRad(fov) / 2); + + it("fits the three from the raised glove to the waist between the top bar and the card", () => { + for (const covered of [0.2, 0.35, 0.5]) { + const shot = ceremonyShot(16 / 9, 36, covered); + expect(visible(shot, 36, 0.09)).toBeCloseTo(2.42, 6); + expect(visible(shot, 36, 1 - covered)).toBeCloseTo(0.95, 6); + } + }); + + it("stands further back the more of the screen the card covers", () => { + expect(ceremonyShot(16 / 9, 36, 0.5).distance).toBeGreaterThan(ceremonyShot(16 / 9, 36, 0.3).distance); + }); + + it("stands back far enough on a tall screen to keep all three in the width", () => { + const aspect = 390 / 844; + const shot = ceremonyShot(aspect, 46.8, 0.45); + const span = 2 * shot.distance * Math.tan(THREE.MathUtils.degToRad(46.8) / 2); + expect(span * aspect).toBeCloseTo(2.4, 6); + expect(visible(shot, 46.8, 0.09)).toBeGreaterThan(2.42); + expect(visible(shot, 46.8, 0.55)).toBeLessThan(0.95); + const middle = visible(shot, 46.8, 0.09 + (1 - 0.45 - 0.09) / 2); + expect(middle).toBeCloseTo((2.42 + 0.95) / 2, 6); + }); +}); + describe("corner staging", () => { it("puts the cutman on the fighter's left and the camera on the right, both inside the ropes", () => { const cutman = cornerPoint(1, 2.56, CUTMAN_WORK_DISTANCE, CUTMAN_WORK_DEGREES, new THREE.Vector3()); @@ -810,6 +1270,92 @@ describe("compact scoreboard labels", () => { expect(texts.some((text) => text.startsWith("HP "))).toBe(true); expect(texts.some((text) => text.startsWith("STAMINA"))).toBe(false); }); + + it("keeps a phone's plates to the style and the bout's count so the two lines do not run together", () => { + const texts: string[] = []; + const players = Object.fromEntries(publicPlayers.map((p) => [p.id, { ...p, record: { wins: 12, losses: 3, draws: 1, knockouts: 8 } }])); + const styled = snapshot(); + drawHud(mockHudContext(texts), 390, 844, { ...styled, fighters: [{ ...styled.fighters[0], style: "counter_puncher" }, styled.fighters[1]] }, players, "one", null, 0, 30); + expect(texts).toContain("COUNTER"); + expect(texts.some((text) => text.includes("ELO") || text.includes("12-3-1") || text.includes("KD"))).toBe(false); + const down = snapshot(); + texts.length = 0; + drawHud(mockHudContext(texts), 390, 844, { ...down, fighters: [{ ...down.fighters[0], style: "counter_puncher", knockdowns: 2 }, down.fighters[1]] }, players, "one", null, 0, 30); + expect(texts).toContain("COUNTER · 2 KD"); + // 7 px a character in this context: every line stays inside its 167 px plate. + expect(texts.filter((text) => text.includes("KD")).every((text) => text.length * 7 <= 167 - 20 - 14)).toBe(true); + }); +}); + +describe("players' pictures", () => { + const players = { one: { id: "one", name: "alpha", avatar: null, rating: 1500, connected: true }, two: { id: "two", name: " ~bravo", avatar: null, rating: 1500, connected: true } }; + const picture = { of: "one" } as unknown as CanvasImageSource; + const onlyOne = (player: { id: string }): CanvasImageSource | null => (player.id === "one" ? picture : null); + + it("are drawn on the plates, with the first letter of the name where there is none", () => { + const texts: string[] = []; + const drawn: DrawnPicture[] = []; + const arcs: Array<{ x: number; y: number; radius: number }> = []; + drawHud(mockHudContext(texts, drawn, arcs), 1280, 720, snapshot(), players, "one", null, 0, 30, null, null, null, null, null, onlyOne); + expect(drawn).toEqual([{ image: picture, x: 35, y: 640, width: PLATE_PORTRAIT_RADIUS * 2, height: PLATE_PORTRAIT_RADIUS * 2 }]); + expect(texts).toContain("B"); + expect(texts).not.toContain("A"); + const rings = arcs.filter((arc) => arc.radius === PLATE_PORTRAIT_RADIUS); + expect(new Set(rings.map((arc) => arc.x))).toEqual(new Set([51, 1280 - 51])); + expect(rings.every((arc) => arc.y === 656)).toBe(true); + }); + + it("are both letters when no pictures are on offer", () => { + const texts: string[] = []; + const drawn: DrawnPicture[] = []; + drawHud(mockHudContext(texts, drawn), 1280, 720, snapshot(), players, "one", null, 0, 30); + expect(drawn).toHaveLength(0); + expect(texts).toContain("A"); + expect(texts).toContain("B"); + }); + + it("stand beside the clock where the plates are too narrow for them", () => { + const drawn: DrawnPicture[] = []; + const arcs: Array<{ x: number; y: number; radius: number }> = []; + drawHud(mockHudContext([], drawn, arcs), 390, 844, snapshot(), players, "one", null, 0, 30, null, null, null, null, null, onlyOne); + expect(arcs.some((arc) => arc.radius === PLATE_PORTRAIT_RADIUS)).toBe(false); + const rings = arcs.filter((arc) => arc.radius === CLOCK_PORTRAIT_RADIUS); + expect(new Set(rings.map((arc) => arc.x))).toEqual(new Set([195 - 112, 195 + 112])); + expect(rings.every((arc) => arc.y === 83)).toBe(true); + expect(drawn).toEqual([{ image: picture, x: 195 - 112 - 20, y: 63, width: 40, height: 40 }]); + for (const ring of rings) { + expect(ring.x - ring.radius).toBeGreaterThan(0); + expect(ring.x + ring.radius).toBeLessThan(390); + expect(Math.abs(ring.x - 195) - ring.radius).toBeGreaterThan(84); + } + }); + + it("stay off a narrow phone on its side while the touch pads stand where the red corner's would go", () => { + const clockRings = (width: number, height: number, touch: boolean): number => { + const arcs: Array<{ x: number; y: number; radius: number }> = []; + drawHud(mockHudContext([], [], arcs), width, height, snapshot(), players, "one", null, 0, 30, null, null, null, null, null, onlyOne, null, touch); + return new Set(arcs.filter((arc) => arc.radius === CLOCK_PORTRAIT_RADIUS).map((arc) => arc.x)).size; + }; + // At 568x320 the red corner's picture (x 376-416, y 63-103) would sit under the modifiers and punch pads (from x 368). + expect(clockRings(568, 320, true)).toBe(0); + expect(clockRings(568, 320, false)).toBe(2); + expect(clockRings(390, 844, true)).toBe(2); + }); + + it("head the columns of the result card", () => { + const texts: string[] = []; + const drawn: DrawnPicture[] = []; + const final = { version: PROTOCOL_VERSION, type: "final" as const, match_id: "m", winner_id: "one", method: "decision" as const, round: 3, scorecards: [], ratings: { one: { before: 1000, after: 1016 }, two: { before: 1000, after: 984 } } }; + drawHud(mockHudContext(texts, drawn), 1280, 720, { ...snapshot(), phase: "complete" }, players, "one", final, 0, 30, null, null, null, null, null, onlyOne); + expect(drawn).toHaveLength(1); + expect(drawn[0]!.image).toBe(picture); + const layout = resultCardLayout(1280, 720, resultCard(final, snapshot().fighters, players, [{ thrown: 0, landed: 0, jabsThrown: 0, jabsLanded: 0 }, { thrown: 0, landed: 0, jabsThrown: 0, jabsLanded: 0 }]), true); + expect(drawn[0]!.x).toBeGreaterThan(layout.x + layout.width * 0.42); + expect(drawn[0]!.x + drawn[0]!.width).toBeLessThan(layout.x + layout.width * 0.6); + expect(drawn[0]!.y).toBeGreaterThanOrEqual(layout.y + 14); + expect(drawn[0]!.y + drawn[0]!.height).toBeLessThanOrEqual(layout.y + 14 + layout.rowHeight); + expect(texts).toContain("B"); + }); }); describe("compact scoreboard mini bars", () => { @@ -865,16 +1411,228 @@ describe("compact scoreboard mini bars", () => { }); }); +describe("the guard bar", () => { + it("marks the line under which a guard stops nothing, and shows a guard worn under it as spent, on a desktop and a 320 px phone", async () => { + const { GUARD_BLOCK_MINIMUM } = await import("../manifest"); + const players = Object.fromEntries(publicPlayers.map((player) => [player.id, player])); + const drawn = (width: number, height: number, guards: readonly [number, number]) => { + const fills: Array<{ x: number; y: number; w: number; h: number; style: string }> = []; + const labels: Array<{ text: string; style: string }> = []; + const strokes: string[] = []; + let fill = ""; + let stroke = ""; + const ctx = Object.assign(mockHudContext([]), { + fillRect: (x: number, y: number, w: number, h: number) => fills.push({ x, y, w, h, style: fill }), + fillText: (text: string) => labels.push({ text, style: fill }), + stroke: () => strokes.push(stroke), + }); + Object.defineProperty(ctx, "fillStyle", { set: (value: unknown) => { fill = typeof value === "string" ? value : "gradient"; } }); + Object.defineProperty(ctx, "strokeStyle", { set: (value: unknown) => { stroke = typeof value === "string" ? value : "gradient"; } }); + const state = { ...snapshot(), fighters: [{ ...fighter("one", -100), guard: guards[0] }, { ...fighter("two", 100), guard: guards[1] }] as const }; + drawHud(ctx, width, height, state, players, "one", null, 0, 30); + return { fills, labels: labels.filter((label) => label.text === "GUARD"), strokes }; + }; + for (const [width, height] of [[1280, 720], [320, 568]] as const) { + // The guard bars ride above the plates: the blue corner's fills from the left, the red corner's from the right. + const plateWidth = width < 640 ? (width - 56) / 2 : Math.min(300, width * 0.38, width / 2 - 84 - 8 - 24); + const bar = Math.min(64, (plateWidth - 52) / 2); + const line = (bar * GUARD_BLOCK_MINIMUM) / HUD_MAX_GUARD; + const { fills, labels, strokes } = drawn(width, height, [40, 650]); + const marks = fills.filter((rect) => rect.w === 1.5 && rect.y === height - 96 - 2); + expect(marks.map((mark) => mark.x + 0.75), `${width}x${height}`).toEqual([44 + line, width - 44 - bar - line]); + // Worn to 40 the blue corner's guard is spent: its label, its mark and hatching across its bar in one red. + const red = labels[0]!.style; + expect(labels[1]!.style).not.toBe(red); + expect(marks[0]!.style).toBe(red); + expect(marks[1]!.style).not.toBe(red); + expect(strokes.filter((style) => style === red)).toHaveLength(1); + // At the line itself the guard still stops punches. + expect(drawn(width, height, [GUARD_BLOCK_MINIMUM, 650]).strokes.filter((style) => style === red)).toHaveLength(0); + } + }); +}); + +describe("the line under a fighter's name", () => { + /** Records where each text and filled rectangle lands, measuring text at 0.55 em a character, about what a phone's system font gives these lines. */ + function measuring(): { ctx: CanvasRenderingContext2D; texts: { text: string; left: number; right: number }[]; rects: { x: number; y: number; w: number; h: number }[] } { + const texts: { text: string; left: number; right: number }[] = []; + const rects: { x: number; y: number; w: number; h: number }[] = []; + let size = 10; + let align: CanvasTextAlign = "left"; + const width = (text: string): number => text.length * size * 0.55; + const ctx = Object.assign(mockHudContext([]), { + fillRect: (x: number, y: number, w: number, h: number) => rects.push({ x, y, w, h }), + fillText: (text: string, x: number) => { + const left = align === "right" ? x - width(text) : align === "center" ? x - width(text) / 2 : x; + texts.push({ text, left, right: left + width(text) }); + }, + measureText: (text: string) => ({ width: width(text) }), + }); + Object.defineProperty(ctx, "font", { set: (font: string) => { size = Number(/(\d+)px/.exec(font)?.[1] ?? 10); } }); + Object.defineProperty(ctx, "textAlign", { set: (value: CanvasTextAlign) => { align = value; } }); + return { ctx, texts, rects }; + } + const players = Object.fromEntries(publicPlayers.map((player) => [player.id, { ...player, name: player.id === "one" ? "Azure Vector" : "Crimson Geometry", record: { wins: 12, losses: 3, draws: 1, knockouts: 8 } }])); + const fouled = { ...snapshot(), fighters: [{ ...fighter("one", -100), style: "counter_puncher" as const, knockdowns: 2, warnings: 2, deductions: 1 }, { ...fighter("two", 100), style: "swarmer" as const, knockdowns: 1, warnings: 1, deductions: 0 }] as const }; + + it("keeps every count on a phone's plate, in short, and stays inside the plate", () => { + for (const [width, height, lines] of [[360, 640, ["COUNTER · 2 KD · W2 · −1", "SWARMER · 1 KD · W1"]], [320, 568, ["2 KD · W2 · −1", "SWARMER · 1 KD · W1"]]] as const) { + const { ctx, texts } = measuring(); + drawHud(ctx, width, height, fouled, players, "one", null, 0, 30); + const details = texts.filter((text) => text.text.includes("KD")); + // At 320 px the style is the part that gives way, never a count. + expect(details.map((text) => text.text)).toEqual(lines); + const plate = (width - 56) / 2; + expect(details[0]!.left).toBeGreaterThanOrEqual(24 + 20); + expect(details[0]!.right).toBeLessThanOrEqual(24 + plate - 14); + expect(details[1]!.left).toBeGreaterThanOrEqual(width - 24 - plate + 14); + expect(details[1]!.right).toBeLessThanOrEqual(width - 24 - 20); + } + }); + + it("keeps the counts on a landscape plate and the whole plate clear of the round card", () => { + for (const [width, height] of [[640, 360], [667, 375], [740, 360], [800, 600]] as const) { + const { ctx, texts, rects } = measuring(); + drawHud(ctx, width, height, fouled, players, "one", null, 0, 30); + const card = { left: width / 2 - 84, right: width / 2 + 84 }; + const [left, right] = texts.filter((text) => text.text.includes("KD")); + expect(left!.text, `${width}x${height}`).toMatch(/2 KD · (2 WARNINGS|W2) · −1/u); + expect(right!.text, `${width}x${height}`).toMatch(/1 KD · (1 WARNING|W1)$/u); + // Nothing on a plate runs under the card, which is drawn over them: not the line, the name or the bars. + const names = texts.filter((text) => text.text.startsWith("AZURE") || text.text.startsWith("CRIMSON")); + for (const text of [left!, ...names.slice(0, 1)]) expect(text.right, `${width}x${height} ${text.text}`).toBeLessThanOrEqual(card.left - 8); + for (const text of [right!, ...names.slice(1)]) expect(text.left, `${width}x${height} ${text.text}`).toBeGreaterThanOrEqual(card.right + 8); + for (const bar of rects.filter((rect) => rect.y === height - 84 + 47 && rect.h === 11)) expect(bar.x + bar.w <= card.left - 8 || bar.x >= card.right + 8, `${width}x${height} bar at ${bar.x}`).toBe(true); + } + }); +}); + describe("result panel text", () => { it("keeps a long winner name whole on a narrow screen instead of cutting the verdict", () => { const texts: string[] = []; const ctx = mockHudContext(texts); const longName = "Anastasia-the-Great5"; const players = { one: { id: "one", name: longName, avatar: null, rating: 1500, connected: true }, two: { id: "two", name: "Bo", avatar: null, rating: 1500, connected: true } }; - const final = { version: 3 as const, type: "final" as const, match_id: "m", winner_id: "one", method: "decision" as const, round: 2, scorecards: [{ judge: "Impact", player_one: [10, 10], player_two: [9, 10] }], ratings: { one: { before: 1000, after: 1016 }, two: { before: 1000, after: 984 } } }; + const final = { version: PROTOCOL_VERSION, type: "final" as const, match_id: "m", winner_id: "one", method: "decision" as const, round: 2, scorecards: [{ judge: "Impact", player_one: [10, 10], player_two: [9, 10] }], ratings: { one: { before: 1000, after: 1016 }, two: { before: 1000, after: 984 } } }; drawHud(ctx, 390, 844, { ...snapshot(), phase: "complete" }, players, "one", final, 0, 30); - expect(texts).toContain(`${longName} WINS`); - expect(texts.some((text) => text.startsWith(`${longName} 1000`))).toBe(true); + expect(texts).toContain(`${longName.toUpperCase()} WINS`); + expect(texts).toContain("1016 (+16)"); + expect(texts).toContain("984 (−16)"); expect(texts.some((text) => text.startsWith("INPUT"))).toBe(false); }); }); + +describe("result card", () => { + const players = { one: { id: "one", name: "Alpha", avatar: null, rating: 1500, connected: true }, two: { id: "two", name: "Bravo", avatar: null, rating: 1500, connected: true } }; + const fighters = (downOne = 0, downTwo = 0): [ReturnType, ReturnType] => [{ ...fighter("one"), knockdowns: downOne }, { ...fighter("two"), knockdowns: downTwo }]; + const result = (method: "decision" | "draw" | "ko" | "flash_ko" | "tko" | "forfeit", winner: string | null, scorecards: { judge: string; player_one: number[]; player_two: number[] }[] = [], ratings: Record = {}) => + ({ version: PROTOCOL_VERSION, type: "final" as const, match_id: "m", winner_id: winner, method, round: 2, scorecards, ratings }); + const none = { thrown: 0, landed: 0, jabsThrown: 0, jabsLanded: 0 }; + + it("lists each judge's total for a decision, with the fighter who took the card in the lead", () => { + const card = resultCard(result("decision", "two", [{ judge: "Impact", player_one: [10, 9], player_two: [9, 10] }, { judge: "Craft", player_one: [9, 9], player_two: [10, 10] }]), fighters(), players, [none, none]); + expect(card.headline).toBe("MAJORITY DECISION"); + expect(card.detail).toBe("ROUND 2"); + expect(card.verdict).toBe("BRAVO WINS"); + expect(card.winnerSeat).toBe(1); + expect(card.names).toEqual(["Alpha", "Bravo"]); + expect(card.judges).toEqual([ + { label: "IMPACT", values: ["19", "19"], lead: null }, + { label: "CRAFT", values: ["18", "20"], lead: 1 }, + ]); + expect(card.rows).toEqual([]); + }); + + it("leaves the unfinished scorecards off a stoppage and spells the method out", () => { + const empty = [{ judge: "Impact", player_one: [], player_two: [] }]; + expect(resultCard(result("ko", "one", empty), fighters(0, 3), players, [none, none])).toMatchObject({ headline: "KNOCKOUT", verdict: "ALPHA WINS", winnerSeat: 0, judges: [], rows: [{ label: "KNOCKDOWNS", values: ["3", "0"], lead: 0 }] }); + expect(resultCard(result("flash_ko", "one", empty), fighters(), players, [none, none]).headline).toBe("FLASH KNOCKOUT"); + expect(resultCard(result("tko", "one", empty), fighters(), players, [none, none]).headline).toBe("TECHNICAL KNOCKOUT"); + expect(resultCard(result("forfeit", "one", empty), fighters(), players, [none, none])).toMatchObject({ headline: "FORFEIT", rows: [] }); + }); + + it("credits a knockdown to the fighter who scored it", () => { + const card = resultCard(result("decision", "one"), fighters(1, 2), players, [none, none]); + expect(card.rows).toEqual([{ label: "KNOCKDOWNS", values: ["2", "1"], lead: 0 }]); + }); + + it("shows the punch stats the way CompuBox counts them, and the rating changes", () => { + const card = resultCard(result("decision", "one", [], { one: { before: 1000, after: 1016 }, two: { before: 1000, after: 984 } }), fighters(), players, [{ thrown: 9, landed: 6, jabsThrown: 4, jabsLanded: 1 }, none]); + expect(card.rows).toEqual([ + { label: "TOTAL PUNCHES", values: ["6/9 (67%)", "0/0"], lead: 0 }, + { label: "JABS", values: ["1/4 (25%)", "0/0"], lead: 0 }, + { label: "POWER PUNCHES", values: ["5/5 (100%)", "0/0"], lead: 0 }, + { label: "RATING", values: ["1016 (+16)", "984 (−16)"], lead: null, news: [true, false] }, + ]); + }); + + it("shows the engine's count for the whole bout when the result carries it, not what this client saw", () => { + const counted = { ...result("decision", "one"), punches: { one: { thrown: 120, landed: 41, jabs_thrown: 60, jabs_landed: 15 }, two: { thrown: 98, landed: 30, jabs_thrown: 50, jabs_landed: 12 } } }; + // A spectator who arrived for the last round saw only a few of those punches. + const card = resultCard(counted, fighters(), players, [{ thrown: 9, landed: 2, jabsThrown: 4, jabsLanded: 1 }, none]); + expect(card.rows.slice(0, 3)).toEqual([ + { label: "TOTAL PUNCHES", values: ["41/120 (34%)", "30/98 (31%)"], lead: 0 }, + { label: "JABS", values: ["15/60 (25%)", "12/50 (24%)"], lead: 0 }, + { label: "POWER PUNCHES", values: ["26/60 (43%)", "18/48 (38%)"], lead: 0 }, + ]); + }); + + it("never shows more punches landed than thrown", () => { + const card = resultCard(result("decision", "one"), fighters(), players, [{ thrown: 2, landed: 3, jabsThrown: 2, jabsLanded: 0 }, none]); + expect(card.rows.find((row) => row.label === "POWER PUNCHES")!.values[0]).toBe("0/0"); + expect(card.rows.find((row) => row.label === "TOTAL PUNCHES")!.values[0]).toBe("2/2 (100%)"); + }); + + it("calls a bout against the computer unrated", () => { + const computer = { ...players, two: { ...players.two, cpu: true } }; + const card = resultCard(result("decision", "one", [], { one: { before: 1000, after: 1000 }, two: { before: 1400, after: 1400 } }), fighters(), computer, [none, none]); + expect(card.rows).toEqual([{ label: "RATING", values: ["Unrated", "Unrated"], lead: null }]); + }); + + it("calls a draw a draw and names a winner who is not in either seat", () => { + expect(resultCard(result("draw", null, [{ judge: "Impact", player_one: [10], player_two: [10] }]), fighters(), players, [none, none])).toMatchObject({ headline: "UNANIMOUS DRAW", verdict: "DRAW", winnerSeat: null }); + expect(resultCard(result("forfeit", "three"), fighters(), { ...players, three: { id: "three", name: "Carol", avatar: null, rating: 1500, connected: true } }, [none, none])).toMatchObject({ verdict: "CAROL WINS", winnerSeat: null }); + }); + + const full = result("decision", "one", [{ judge: "Impact", player_one: [10, 10], player_two: [9, 9] }, { judge: "Craft", player_one: [10, 10], player_two: [9, 9] }, { judge: "Generalship", player_one: [10, 10], player_two: [9, 9] }], { one: { before: 1000, after: 1016 }, two: { before: 1000, after: 984 } }); + const fullCard = resultCard(full, fighters(0, 1), players, [{ thrown: 9, landed: 6, jabsThrown: 5, jabsLanded: 3 }, { thrown: 4, landed: 1, jabsThrown: 2, jabsLanded: 1 }]); + + it("sits along the bottom of the screen and leaves the ring in view above it", () => { + for (const [width, height, clear] of [[1280, 720, 0.55], [1920, 1080, 0.6], [390, 844, 0.45], [844, 390, 0.15], [640, 360, 0.1]] as const) { + const layout = resultCardLayout(width, height, fullCard, true); + expect(layout.x).toBeGreaterThanOrEqual(12); + expect(layout.x + layout.width).toBeLessThanOrEqual(width - 12); + expect(layout.y + layout.height).toBeCloseTo(height - 14 * hudScale(width, height), 6); + expect(layout.y).toBeGreaterThanOrEqual(height * clear); + expect(layout.rowHeight).toBeGreaterThanOrEqual(16); + } + }); + + it("keeps the rows that matter on a phone on its side, so the card leaves the ring in view", () => { + const layout = resultCardLayout(844, 390, fullCard, true); + expect(layout.dense).toBe(true); + expect(cardRows(fullCard, true).map((row) => row.label)).toEqual(["KNOCKDOWNS", "TOTAL PUNCHES", "RATING"]); + expect(layout.height / 390).toBeLessThan(0.5); + expect(cardRows(fullCard, false)).toHaveLength(fullCard.rows.length); + }); + + it("puts the verdict beside the table on a wide or a short screen and above it on a tall one", () => { + expect(resultCardLayout(1280, 720, fullCard, true).wide).toBe(true); + expect(resultCardLayout(844, 390, fullCard, true).wide).toBe(true); + expect(resultCardLayout(390, 844, fullCard, true).wide).toBe(false); + expect(resultCardLayout(800, 900, fullCard, true).wide).toBe(false); + }); + + it("leaves room for the rematch button on a fighter's card only", () => { + expect(resultCardLayout(1280, 720, fullCard, true).height - resultCardLayout(1280, 720, fullCard, false).height).toBe(RESULT_CARD_FOOTER - 14); + }); + + it("replaces the plates and the clock", () => { + const texts: string[] = []; + const tracker = new RoundStatsTracker(); + tracker.record({ event_id: 1, tick: 1, kind: "punch_start", actor_id: "one", target_id: "two", amount: 0, detail: "", blood: 0, direction: 1, action_id: null }); + drawHud(mockHudContext(texts), 1280, 720, { ...snapshot(), phase: "complete" }, players, "one", full, 0, 30, tracker); + expect(texts.some((text) => /STAMINA|HEALTH|GUARD|POISE|COMPLETE/.test(text))).toBe(false); + expect(texts).toEqual(expect.arrayContaining(["UNANIMOUS DECISION", "ROUND 2", "ALPHA WINS", "IMPACT", "20 – 18", "Alpha", "Bravo", "TOTAL PUNCHES", "0/1 (0%)", "RATING"])); + }); +}); diff --git a/web/hands/src/render/renderer-frame.test.ts b/web/hands/src/render/renderer-frame.test.ts new file mode 100644 index 00000000..5a030c37 --- /dev/null +++ b/web/hands/src/render/renderer-frame.test.ts @@ -0,0 +1,824 @@ +import * as THREE from "three"; +import { REST_CORNER_OFFSET, RING_CORNER_REACH } from "../manifest"; +import { fighter, snapshot } from "../test/fixtures"; +import { PROTOCOL_VERSION, type EngineSnapshot, type FighterSnapshot, type FinalMessage } from "../types"; +import { CameraDirector, ceremonyShot, cornerPoint, FIGHTER_CAM_FOV_SCALE, FighterCam } from "./camera"; +import { RoundStatsTracker } from "./hud"; +import { lookFor } from "./looks"; +import { FightRenderer, resultCardTop } from "./renderer"; +import { buildRing, disposeRing } from "./ring"; +import { RockedVision } from "./rocked"; +import { worldMapping } from "./world"; + +/** + * Runs the renderer's real frame (draw) against stand-ins for the GPU and the scene, so that what the + * frame decides and whether it calls each of its parts is tested, not only the parts on their own. + */ +const SIMULATION = { tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }; + +type Settings = { reducedMotion: boolean; blood: "full" | "reduced" | "off"; camera: "broadcast" | "close" | "fighter"; commentary: boolean; announcer: boolean; volume: number; haptics: boolean }; + +function graph() { + return { + boxer: { root: new THREE.Object3D(), bone: () => null, setLook: vi.fn(), metrics: { headRestY: 1.52 }, rig: { bones: { gloveL: new THREE.Object3D(), gloveR: new THREE.Object3D() } } }, + awaitVerdict: vi.fn(), + announce: vi.fn(), + setResting: vi.fn(), + setCountdown: vi.fn(), + setObstacle: vi.fn(), + stayDown: vi.fn(), + update: vi.fn(), + fallBody: null, + stoolVisible: false, + }; +} + +function frame(state: EngineSnapshot, overrides: Record = {}) { + const settings: Settings = { reducedMotion: false, blood: "full", camera: "broadcast", commentary: true, announcer: true, volume: 0, haptics: false }; + const latest = { current: state }; + const camera = new THREE.PerspectiveCamera(36, 16 / 9, 0.1, 100); + const renderer = Object.assign(Object.create(FightRenderer.prototype) as Record, { + destroyed: false, + previous: 0, + canvas: { clientWidth: 1280, clientHeight: 720 }, + renderer: { getSize: (out: THREE.Vector2) => out.set(1280, 720), setSize: vi.fn() }, + composer: { setSize: vi.fn(), render: vi.fn() }, + camera, + portraitPull: 1, + baseFov: 36, + sizeCheck: new THREE.Vector2(), + frameSeconds: 0, + finishPass: { uniforms: { uTime: { value: 0 } } }, + lastManualTime: 0, + settings: () => settings, + setBloodLevel: vi.fn(), + buffer: { latest: () => latest.current, sample: () => latest.current, renderTick: () => latest.current.tick, interpolationDelayTicks: 2 }, + history: [state], + updateRocked: vi.fn(), + finishSeen: false, + finishSlowMotion: 0, + ovationUntil: 0, + arena: { excite: vi.fn(), update: vi.fn(), makeRoomForCamera: vi.fn() }, + viewerHitFlash: 0, + localInput: null, + replay: null, + ceremony: null, + drawnFighters: [fighter("one"), fighter("two")], + drawnOffsets: [{ x: 0, y: 0 }, { x: 0, y: 0 }], + drawnTargets: [{ x: 0, y: 0 }, { x: 0, y: 0 }], + arcadeInjuries: [null, null], + observedInjuryDown: [false, false], + restoreInjury: vi.fn(), + graphs: [graph(), graph()], + lookIds: [null, null], + headCache: [new THREE.Vector3(), new THREE.Vector3()], + headCacheValid: [false, false], + liveFallTicks: [null, null], + fallingLive: [false, false], + simulation: SIMULATION, + lastPhase: state.phase, + roundCalloutUntil: 0, + roundCalloutRound: 0, + enterRest: vi.fn(), + anticipatePunches: vi.fn(), + mapping: worldMapping(SIMULATION), + bodyPoint: new THREE.Vector3(), + ring: { setRopeContacts: vi.fn(), setNearRopeOpacity: vi.fn(), nearRopeOpacity: () => 1 }, + tmpA: new THREE.Vector3(), + tmpB: new THREE.Vector3(), + tmpHead: new THREE.Vector3(), + tmpCamera: new THREE.Vector3(), + downedPoolAccumulators: [0, 0], + downedPoolCounts: [0, 0], + effects: { drip: vi.fn(), stopDrip: vi.fn(), pool: vi.fn(), update: vi.fn(), shakeAmount: 0, setViewDistance: vi.fn() }, + updateIdleFighters: vi.fn(), + updateTrails: vi.fn(), + updateReferee: vi.fn(), + updateCornermen: vi.fn(), + updateCutmen: vi.fn(), + updateBlobShadows: vi.fn(), + fireContacts: vi.fn(), + followSpot: null, + director: new CameraDirector(), + viewerId: "one", + fighterCam: new FighterCam(), + ownViewActive: false, + ownForward: { x: 0, z: -1 }, + cameraOverride: null, + finishCloseUpUntil: 0, + finishCloseUpIndex: -1, + stoppageWinner: -1, + stoppageRaiseAt: Number.POSITIVE_INFINITY, + stoppageSpot: null, + raiseCentre: new THREE.Vector3(), + raiseFramed: false, + restStartedAt: 0, + cutmen: null, + cutmanProgress: [0, 0], + cornerPosition: new THREE.Vector3(), + cornerLookAt: new THREE.Vector3(), + final: null, + players: {}, + roundStats: new RoundStatsTracker(), + hudViewport: { width: 1280, height: 720 }, + manualClock: true, + pixelRatioCap: 2, + basePixelRatio: 2, + pendingFinish: null, + ...overrides, + }); + const draw = (FightRenderer.prototype as unknown as { draw(this: unknown, time: number, manual: boolean, render: boolean): void }).draw; + let time = 1000; + const run = (frames = 1): void => { + for (let index = 0; index < frames; index += 1) { + time += 1000 / 60; + draw.call(renderer, time, true, false); + } + }; + return { renderer, settings, camera, latest, run, graphs: renderer.graphs as ReturnType[] }; +} + +const fighting = (one: Partial = {}, two: Partial = {}, extra: Partial = {}): EngineSnapshot => { + const base = snapshot(40); + return { ...base, fighters: [{ ...base.fighters[0], ...one }, { ...base.fighters[1], ...two }], ...extra }; +}; + +describe("a rendered frame", () => { + it("keeps the cameras on the fighters on the frame a heavy punch lands", () => { + const fireContacts = (FightRenderer.prototype as unknown as { fireContacts: unknown }).fireContacts; + const opponentHead = new THREE.Vector3(0.67, 1.6, 0); + const moved = (camera: Settings["camera"]): { quiet: number; contact: number } => { + const state = fighting({ x: -110 }, { x: 110 }); + const effects = { drip: vi.fn(), stopDrip: vi.fn(), pool: vi.fn(), update: vi.fn(), shakeAmount: 0, setViewDistance: vi.fn(), addEvent: vi.fn(), spawnTeeth: vi.fn(), ejectMouthpiece: vi.fn() }; + const harness = frame(state, { + fireContacts, + effects, + pendingContacts: [], + contactPoint: new THREE.Vector3(), + mouthPoint: new THREE.Vector3(), + presentFightEvent: vi.fn(), + applyArcadeInjury: vi.fn(), + headWorldPose: () => ({ position: opponentHead, quaternion: new THREE.Quaternion() }), + gearColor: () => 0xffffff, + flashKnockout: null, + onContact: null, + }); + harness.settings.camera = camera; + for (const graph of harness.graphs) Object.assign(graph, { react: vi.fn(), landedHit: vi.fn() }); + harness.run(240); + const before = harness.camera.position.clone(); + harness.run(1); + const quiet = harness.camera.position.distanceTo(before); + const settled = harness.camera.position.clone(); + const counter = { event_id: 7, tick: 40, kind: "counter_hit", actor_id: "one", target_id: "two", amount: 130, detail: "straight:head", blood: 40, direction: 1, action_id: "p1" }; + (harness.renderer.pendingContacts as unknown[]).push({ event: counter, presentationEvent: counter, presentImpact: true, reactAmount: counter.amount, contactTick: 0, recipientIndex: 1, puncherIndex: 0, injury: null }); + harness.run(1); + expect(effects.spawnTeeth).toHaveBeenCalled(); + expect(effects.ejectMouthpiece).toHaveBeenCalled(); + return { quiet, contact: harness.camera.position.distanceTo(settled) }; + }; + for (const camera of ["fighter", "broadcast"] as const) { + const { quiet, contact } = moved(camera); + expect(quiet).toBeLessThan(0.002); + expect(contact).toBeLessThan(0.002); + } + }); + + it("dresses each fighter in the look of the player in that seat", () => { + const { run, graphs } = frame(fighting()); + run(3); + expect(graphs[0]!.boxer.setLook).toHaveBeenCalledOnce(); + expect(graphs[0]!.boxer.setLook).toHaveBeenCalledWith(lookFor("one")); + expect(graphs[1]!.boxer.setLook).toHaveBeenCalledWith(lookFor("two")); + }); + + it("eases apart fighters standing in each other, but not two in a clinch or one who is down", () => { + const drawnGap = (one: Partial): number => { + const { run, graphs } = frame(fighting({ x: -30, ...one }, { x: 30 })); + run(); + const [a, b] = graphs[0]!.update.mock.calls[0]! as [FighterSnapshot, FighterSnapshot]; + return b.x - a.x; + }; + expect(drawnGap({})).toBeGreaterThanOrEqual(103); + expect(drawnGap({ clinch_ticks: 20 })).toBe(60); + expect(drawnGap({ is_downed: true })).toBe(60); + }); + + it("seats the fighters through the rest until the seconds are called out", () => { + const seated = (remaining: number): unknown => { + const { run, graphs } = frame(fighting({}, {}, { phase: "rest", phase_ticks_remaining: remaining })); + run(); + return graphs[0]!.setResting.mock.calls.at(-1)?.[0]; + }; + expect(seated(300)).toBe(true); + expect(seated(60)).toBe(false); + }); + + it("offers each fighter's next punch from the newest snapshot every frame", () => { + const { renderer, run, latest } = frame(fighting()); + run(2); + expect(renderer.anticipatePunches).toHaveBeenCalledTimes(2); + expect((renderer.anticipatePunches as ReturnType).mock.calls[0]![0]).toBe(latest.current); + }); + + it("keeps the crowd cheering through the ovation", () => { + const { renderer, run } = frame(fighting(), { ovationUntil: 999 }); + run(); + expect((renderer.arena as { excite: ReturnType }).excite).toHaveBeenCalled(); + }); + + it("tells the blood how far the camera is from what it looks at", () => { + const { renderer, run } = frame(fighting()); + run(); + const [distance] = (renderer.effects as { setViewDistance: ReturnType }).setViewDistance.mock.calls.at(-1)! as [number]; + expect(distance).toBeGreaterThan(3); + expect(distance).toBeLessThan(12); + }); + + it("hurts the viewer's own vision every frame it is due", () => { + const { renderer, run } = frame(fighting()); + run(2); + expect(renderer.updateRocked).toHaveBeenCalledTimes(2); + }); + + it("starts the hurt vision and the muffle with the punch that rocked him, not when its snapshot arrives", () => { + const updateRocked = (FightRenderer.prototype as unknown as { updateRocked: unknown }).updateRocked; + const calm = { ...fighting(), tick: 40 }; + const stunned = (tick: number): EngineSnapshot => ({ ...fighting({ stunned_ticks: 36 }), tick }); + const watch = (viewerId: string | null) => { + const heard: number[] = []; + const finishPass = { uniforms: { uTime: { value: 0 }, uRocked: { value: 0 } } }; + const harness = frame(calm, { viewerId, updateRocked, rocked: new RockedVision(), finishPass, onRocked: (level: number) => heard.push(level) }); + const arrive = (next: EngineSnapshot): void => { + harness.latest.current = next; + (harness.renderer.history as EngineSnapshot[]).push(next); + }; + harness.run(3); + // The snapshot carrying the stun arrives; the render clock is still a tick short of the punch. + arrive(stunned(41)); + harness.run(3); + const early = { vision: finishPass.uniforms.uRocked.value, sound: heard.at(-1) }; + // The clock reaches it. + arrive(stunned(42)); + harness.run(3); + return { early, vision: finishPass.uniforms.uRocked.value, sound: heard.at(-1), heard }; + }; + const own = watch("one"); + expect(own.early).toEqual({ vision: 0, sound: 0 }); + expect(own.vision).toBeGreaterThan(0.2); + expect(own.sound).toBe(1); + // A spectator's sound is never muffled. + expect(watch(null).heard).toEqual([]); + }); + + it("fades the near ropes for the broadcast camera as the fighters come toward it", () => { + const opacity = (y: number): number => { + const { renderer, run } = frame(fighting({ y }, { y })); + run(); + return (renderer.ring as { setNearRopeOpacity: ReturnType }).setNearRopeOpacity.mock.calls.at(-1)![0] as number; + }; + expect(opacity(-420)).toBeLessThan(0.99); + expect(opacity(420)).toBeCloseTo(1, 6); + }); + + it("uses the close broadcast camera when the player chose it", () => { + const director = new CameraDirector(); + const update = vi.spyOn(director, "update"); + const { settings, run } = frame(fighting(), { director }); + settings.camera = "close"; + run(); + expect(update.mock.calls.at(-1)![8]).toBe(true); + settings.camera = "broadcast"; + run(); + expect(update.mock.calls.at(-1)![8]).toBe(false); + }); + + it("puts the camera over the player's own shoulder when chosen, but never for a spectator", () => { + const own = frame(fighting()); + own.settings.camera = "fighter"; + own.run(2); + expect((own.renderer as unknown as FightRenderer).viewForward()).not.toBeNull(); + expect(own.camera.fov).toBeCloseTo(36 * FIGHTER_CAM_FOV_SCALE, 6); + const watching = frame(fighting(), { viewerId: null }); + watching.settings.camera = "fighter"; + watching.run(2); + expect((watching.renderer as unknown as FightRenderer).viewForward()).toBeNull(); + expect(watching.camera.fov).toBe(36); + }); + + it("keeps the player's own camera behind his back on a phone held upright", () => { + const scale = 3.05 / 500; + const portrait = { canvas: { clientWidth: 390, clientHeight: 844 }, renderer: { getSize: (out: THREE.Vector2) => out.set(1, 1), setSize: vi.fn() } }; + for (const z of [1, 1.5, 2, 2.4]) { + // Facing -z, the opponent a metre ahead: his back is toward +z. + const own = frame(fighting({ x: 0, y: -z / scale, facing_x: 0, facing_y: 1000 }, { x: 0, y: -(z - 1) / scale }), portrait); + own.settings.camera = "fighter"; + own.run(240); + const behind = own.camera.position.z - z; + const offHisBack = THREE.MathUtils.radToDeg(Math.atan2(Math.abs(own.camera.position.x), behind)); + // Was 1.2 / 0.7 / 0.2 / -0.2 m behind and 60 / 71 / 84 / 96 degrees round: a side view, clamped inside the ropes. + // Now 2.3 / 2.0 / 1.5 / 1.1 m behind and 19 degrees round throughout. + expect(behind).toBeGreaterThan(z < 2.4 ? 1.4 : 1); + expect(offHisBack).toBeLessThan(22); + // His own chest and the opponent's head are on the screen. + own.camera.updateMatrixWorld(true); + for (const point of [new THREE.Vector3(0, 1.2, z), new THREE.Vector3(0, 1.55, z - 1)]) { + const shown = point.project(own.camera); + expect(Math.abs(shown.x)).toBeLessThan(0.9); + expect(Math.abs(shown.y)).toBeLessThan(0.9); + } + } + }); + + it("shows the winner and the referee lifting his arm after a knockout, not the count's shot of the body", () => { + const scale = 3.05 / 500; + const final: FinalMessage = { version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "one", method: "ko", round: 2, scorecards: [], ratings: {} }; + const result = { match_id: "m", activity_instance_id: "i", guild_id: "g", player_one_id: "one", player_two_id: "two", winner_id: "one", finish_method: "ko" as const, round_number: 2, tick: 400, scorecards: [], player_one_knockdowns: 0, player_two_knockdowns: 1, player_one_damage: 1, player_two_damage: 1 }; + const lying = [new THREE.Vector3(1.8, 0, 0), new THREE.Vector3(1.4, 0, 0), new THREE.Vector3(1.05, 0, 0), new THREE.Vector3(0.7, 0, 0.12), new THREE.Vector3(0.7, 0, -0.12)]; + const screens = [{ width: 1280, height: 720, canvas: { clientWidth: 1280, clientHeight: 720 } }, { width: 390, height: 844, canvas: { clientWidth: 390, clientHeight: 844 } }]; + for (const screen of screens) { + for (const camera of ["broadcast", "close"] as const) { + // The winner at x = -0.5, the beaten fighter's spot at 0.3 and his body toward +x; the referee is to lift the arm. + const knockedOut = fighting({ x: -0.5 / scale }, { x: 0.3 / scale, is_downed: true }, { phase: "complete", result }); + const view = frame(knockedOut, { + final, stoppageWinner: 0, stoppageRaiseAt: 0, hudViewport: { width: screen.width, height: screen.height }, + canvas: screen.canvas, renderer: { getSize: (out: THREE.Vector2) => out.set(1, 1), setSize: vi.fn() }, + }); + (view.graphs[1] as { fallBody: unknown }).fallBody = { centre: (out: THREE.Vector3) => out.set(1.05, 0, 0), bodyPoint: (index: number, out: THREE.Vector3) => out.copy(lying[index]!) }; + view.settings.camera = camera; + view.run(400); + view.camera.updateMatrixWorld(true); + const top = resultCardTop(final, screen.width, screen.height, knockedOut.fighters, {}, new RoundStatsTracker(), "one"); + const cardTop = 1 - (2 * top) / screen.height; + // Before, the winner's head was at y 0.97 and his raised glove at 1.38 (16:9, close), or his head at + // x -1.21 (390x844, close): above or past the edge of the picture. + for (const height of [1.55, 1.95]) { + const shown = new THREE.Vector3(-0.5, height, 0).project(view.camera); + expect(Math.abs(shown.x)).toBeLessThan(0.85); + expect(shown.y).toBeLessThan(0.82); + expect(shown.y).toBeGreaterThan(cardTop); + } + } + } + }); + + it("spreads a pool of blood under a bleeding fighter who is down", () => { + const bleeding = { ...fighter("two").trauma, bleeding: 500, left_cut: 300 }; + const { renderer, run } = frame(fighting({}, { is_downed: true, trauma: bleeding }, { phase: "knockdown" })); + run(40); + expect((renderer.effects as { pool: ReturnType }).pool).toHaveBeenCalled(); + }); + + it("frames the decision above the result card, looking through the near ropes", () => { + const final: FinalMessage = { version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "one", method: "decision", round: 3, scorecards: [], ratings: {} }; + const state = fighting({}, {}, { phase: "complete" }); + const ceremony = { winnerSeat: 0, positions: [{ x: -102, y: -16 }, { x: 102, y: -16 }], marks: [0, 1], refereeArrived: true, arrivedAt: 0.5, announced: true }; + const { renderer, run, camera } = frame(state, { ceremony, final, settings: () => ({ reducedMotion: true, blood: "full", camera: "broadcast" }), referee: { raise: vi.fn() }, ceremonyWrists: [new THREE.Vector3(), new THREE.Vector3()], commentary: { verdict: vi.fn() } }); + run(); + const top = resultCardTop(final, 1280, 720, state.fighters, {}, new RoundStatsTracker(), "one"); + const shot = ceremonyShot(16 / 9, 36, (720 - top) / 720); + expect(camera.position.z).toBeCloseTo(shot.distance, 4); + expect(camera.position.y).toBeCloseTo(shot.height + 0.05 * shot.distance, 4); + expect((renderer.ring as { setNearRopeOpacity: ReturnType }).setNearRopeOpacity.mock.calls.at(-1)![0]).toBeLessThan(0.99); + }); +}); + +describe("around the fight", () => { + const combat = (kind: string, fields: Record = {}) => ({ event_id: 1, tick: 40, kind, actor_id: "one", target_id: "two", amount: 0, detail: "", blood: 0, direction: 1, action_id: null, ...fields }); + const prototypeOf = (fields: Record) => Object.assign(Object.create(FightRenderer.prototype) as Record, fields); + const method = (name: T) => (FightRenderer.prototype as unknown as Record unknown>)[name]; + + it("draws the commentary caption on top of the HUD", async () => { + const { mockHudContext } = await import("../test/fixtures"); + const drawCaption = vi.fn(); + const stub = prototypeOf({ + hudCanvas: { getBoundingClientRect: () => ({ width: 1280, height: 720 }), width: 0, height: 0, getContext: () => mockHudContext([]) }, + hudViewport: { width: 0, height: 0 }, viewerHitFlash: 0, settings: () => ({ reducedMotion: false }), players: {}, viewerId: "one", final: null, + frameSeconds: 0, finalRevealAt: 0, reconnectMs: 0, simulation: SIMULATION, roundStats: new RoundStatsTracker(), replay: null, inputLatencyMs: null, + roundCalloutUntil: 0, roundCalloutRound: 0, eventCallout: null, roundClock: { ticks: () => null }, avatars: { get: () => null }, drawCaption, + }); + method("drawHudOverlay").call(stub, snapshot()); + expect(drawCaption).toHaveBeenCalledOnce(); + }); + + it("does a knockout's finishing injury at once when there is no replay to show it in", () => { + const hit = combat("counter_hit", { event_id: 7, detail: "right:uppercut:head", amount: 140 }); + const applyArcadeInjury = vi.fn(() => true); + const presentFinish = vi.fn(); + const stub = prototypeOf({ + frameSeconds: 5, buffer: { latest: () => snapshot() }, commentary: { finish: vi.fn() }, endCeremony: vi.fn(), players: {}, history: [], simulation: SIMULATION, + graphs: null, settings: () => ({ reducedMotion: false, blood: "full" }), arcadeInjuries: [null, null], + lastKnockdown: { knockdown: combat("knockdown", { event_id: 8, amount: 1 }), hit, finisher: "decapitation" }, applyArcadeInjury, presentFinish, pendingContacts: [], + }); + method("setFinal").call(stub, { version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "one", method: "ko", round: 2, scorecards: [], ratings: {} }); + expect(applyArcadeInjury).toHaveBeenCalledWith(1, "decapitation", hit); + expect(presentFinish).toHaveBeenCalledOnce(); + }); + + it("leaves a finishing injury to its punch while the clock has not shown that punch yet", () => { + const hit = combat("counter_hit", { event_id: 7, detail: "right:uppercut:head", amount: 140 }); + const applyArcadeInjury = vi.fn(() => true); + const waiting = { event: hit, presentationEvent: hit, presentImpact: true, reactAmount: hit.amount, contactTick: 160, recipientIndex: 1, puncherIndex: 0, injury: null as string | null }; + const stub = prototypeOf({ + frameSeconds: 5, buffer: { latest: () => snapshot() }, commentary: { finish: vi.fn() }, endCeremony: vi.fn(), players: {}, history: [], simulation: SIMULATION, + graphs: null, settings: () => ({ reducedMotion: false, blood: "full" }), arcadeInjuries: [null, null], + lastKnockdown: { knockdown: combat("knockdown", { event_id: 8, amount: 3 }), hit, finisher: "decapitation" }, applyArcadeInjury, presentFinish: vi.fn(), pendingContacts: [waiting], + }); + method("setFinal").call(stub, { version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "one", method: "tko", round: 2, scorecards: [], ratings: {} }); + expect(applyArcadeInjury).not.toHaveBeenCalled(); + expect(waiting.injury).toBe("decapitation"); + }); + + it("knocks the gum shield out with a big counter to the head", () => { + const knockOutMouthpiece = vi.fn(); + const event = combat("counter_hit", { event_id: 9, detail: "hook:head", amount: 120 }); + const stub = prototypeOf({ + pendingContacts: [{ event, presentationEvent: event, presentImpact: true, reactAmount: event.amount, contactTick: 40, recipientIndex: 1, puncherIndex: 0, injury: null }], + buffer: { latest: () => snapshot() }, presentFightEvent: vi.fn(), mapping: worldMapping(SIMULATION), contactPoint: new THREE.Vector3(), mouthPoint: new THREE.Vector3(), tmpA: new THREE.Vector3(), tmpB: new THREE.Vector3(), + effects: { addEvent: vi.fn(), spawnTeeth: vi.fn() }, settings: () => ({ reducedMotion: false, blood: "full" }), arena: { excite: vi.fn() }, viewerId: null, + arcadeInjuries: [null, null], graphs: null, headWorldPose: () => null, knockOutMouthpiece, onContact: null, viewerHitFlash: 0, + }); + method("fireContacts").call(stub, 50); + // It flies the way the punch travelled, from fighter one toward fighter two. + expect(knockOutMouthpiece).toHaveBeenCalledWith(1, { x: 1, z: 0 }, 9, false); + }); + + it("waves a stoppage off and lets the winner celebrate only once the punch that ended it is on screen", () => { + // A third knockdown on tick 30 ends the bout; with reduced motion there is no replay to wait for. + const result = { match_id: "m", activity_instance_id: "i", guild_id: "g", player_one_id: "one", player_two_id: "two", winner_id: "one", finish_method: "tko" as const, round_number: 1, tick: 30, scorecards: [], player_one_knockdowns: 0, player_two_knockdowns: 3, player_one_damage: 0, player_two_damage: 900 }; + const ended = { ...snapshot(30), phase: "complete" as const, fighters: [fighter("one", -60), { ...fighter("two", 60), is_downed: true, knockdowns: 3 }] as const, result }; + const finish = (frameSeconds: number) => { + const waveOff = vi.fn(); + const celebrate = vi.fn(); + const stub = prototypeOf({ + frameSeconds, buffer: { latest: () => ended }, commentary: { finish: vi.fn() }, endCeremony: vi.fn(), players: {}, history: [ended], simulation: SIMULATION, + graphs: [{ celebrate }, { celebrate: vi.fn() }], settings: () => ({ reducedMotion: true, blood: "full" }), arcadeInjuries: [null, null], pendingContacts: [], + lastKnockdown: null, referee: { waveOff }, headCacheValid: [false, false], stoppageWinner: -1, + }); + method("setFinal").call(stub, { version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "one", method: "tko", round: 1, scorecards: [], ratings: {} }); + return { stub, waveOff, celebrate }; + }; + const { stub, waveOff, celebrate } = finish(5); + // The final arrives while the screen is still two ticks short of the punch. + method("presentFinishWhenShown").call(stub, 28); + expect(waveOff).not.toHaveBeenCalled(); + expect(celebrate).not.toHaveBeenCalled(); + // The frame that shows the punch (contacts are presented first, in the same frame) calls the finish. + method("presentFinishWhenShown").call(stub, 30); + expect(waveOff).toHaveBeenCalledOnce(); + expect(celebrate).toHaveBeenCalledOnce(); + expect(stub.stoppageWinner).toBe(0); + // Should the clock never get there, it waits half a second at most. + const stuck = finish(5); + (stuck.stub as { frameSeconds: number }).frameSeconds = 5.6; + method("presentFinishWhenShown").call(stuck.stub, 25); + expect(stuck.waveOff).toHaveBeenCalledOnce(); + }); + + it("shows a result once, however many times it arrives", () => { + const standing = { ...snapshot(400), phase: "complete" as const }; + const stub = prototypeOf({ + frameSeconds: 5, buffer: { latest: () => standing }, commentary: { finish: vi.fn() }, endCeremony: vi.fn(), players: {}, history: [], simulation: SIMULATION, final: null, + }); + const decision = { version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "one", method: "decision", round: 3, scorecards: [], ratings: {} } as const; + method("setFinal").call(stub, decision); + const ceremony = stub.ceremony; + (stub as { frameSeconds: number }).frameSeconds = 8; + method("setFinal").call(stub, { ...decision }); + expect(stub.ceremony).toBe(ceremony); + expect(stub.ovationUntil).toBe(5 + 16); + expect((stub.commentary as { finish: ReturnType }).finish).toHaveBeenCalledOnce(); + }); + + it("has the winner celebrate when the bout is over", () => { + const celebrate = [vi.fn(), vi.fn()]; + const stub = prototypeOf({ + referee: { waveOff: vi.fn() }, buffer: { latest: () => snapshot() }, headCacheValid: [false, false], settings: () => ({ reducedMotion: false }), frameSeconds: 0, + graphs: [{ celebrate: celebrate[0] }, { celebrate: celebrate[1] }], + }); + method("presentFinish").call(stub, { version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "two", method: "tko", round: 2, scorecards: [], ratings: {} }); + expect(celebrate[1]).toHaveBeenCalledOnce(); + expect(celebrate[0]).not.toHaveBeenCalled(); + // He celebrates for as long as the result is up, rather than going back into his guard. + expect(celebrate[1]!.mock.calls[0]![0]).toBeGreaterThanOrEqual(60); + }); + + it("keeps both fighters in the rest's wide shot on a phone held upright, steady as they cross and clear of the corner", () => { + const mapping = worldMapping(SIMULATION); + const corner = mapping.x(REST_CORNER_OFFSET); + const pull = THREE.MathUtils.clamp(1.2 / (390 / 844), 1, 2.2); + const shotFor = (one: number, two: number, viewerId: string | null = "one", portraitPull = pull) => { + const stub = prototypeOf({ + portraitPull, viewerId, mapping, cornerPosition: new THREE.Vector3(), cornerLookAt: new THREE.Vector3(), tmpCamera: new THREE.Vector3(), + tmpA: new THREE.Vector3(mapping.x(one), 0, 0), tmpB: new THREE.Vector3(mapping.x(two), 0, 0), + }); + const shot = method("restWideFrame").call(stub, { ...snapshot(), phase: "rest", fighters: [fighter("one", one), fighter("two", two)] }) as { position: THREE.Vector3; lookAt: THREE.Vector3 } | null; + return shot === null ? null : { position: shot.position.clone(), lookAt: shot.lookAt.clone() }; + }; + // They walk past each other to their corners: the shot holds still (it used to swing 173 degrees round). + const passing = [-200, -60, -10, 10, 60, 200].map((x) => shotFor(x, -x)!); + for (const shot of passing) { + expect(shot.position.distanceTo(passing[0]!.position)).toBe(0); + expect(shot.lookAt.distanceTo(passing[0]!.lookAt)).toBe(0); + } + const { position, lookAt } = passing[0]!; + const camera = new THREE.PerspectiveCamera(36 * Math.min(1.3, Math.sqrt(pull)), 390 / 844, 0.1, 80); + camera.position.copy(position); + camera.lookAt(lookAt); + camera.updateMatrixWorld(true); + // Both in their corners, seated or standing, are on the screen. + const stools = [cornerPoint(0, corner, 0, 0, new THREE.Vector3()), cornerPoint(1, corner, 0, 0, new THREE.Vector3())]; + for (const stool of stools) { + for (const height of [0.5, 1.3, 1.65]) { + const shown = new THREE.Vector3(stool.x, height, stool.z).project(camera); + expect(Math.abs(shown.x)).toBeLessThan(0.85); + expect(Math.abs(shown.y)).toBeLessThan(0.85); + } + } + // Neither the near post and its pads nor the cornerman leaning over the ropes stands between the camera + // and the near fighter's head and chest: before, every line to him ran through the post and both pads. + const ring = buildRing(); + ring.group.updateMatrixWorld(true); + const corners: THREE.Object3D[] = []; + ring.group.traverse((object) => { + if (object instanceof THREE.Mesh && (object.geometry.type === "CylinderGeometry" || (object.geometry.type === "BoxGeometry" && (object.geometry as THREE.BoxGeometry).parameters.height === 0.52))) corners.push(object); + }); + const raycaster = new THREE.Raycaster(); + const cornerman = new THREE.Vector3(-2.95, 0, 2.95); + for (const height of [1.3, 1.0]) { + const target = new THREE.Vector3(stools[0]!.x, height, stools[0]!.z); + raycaster.set(position, target.clone().sub(position).normalize()); + raycaster.far = position.distanceTo(target) - 0.15; + expect(raycaster.intersectObjects(corners, false)).toEqual([]); + for (const at of [1.2, 1.6]) { + const leaning = new THREE.Vector3(cornerman.x, at, cornerman.z); + expect(new THREE.Line3(position, target).closestPointToPoint(leaning, true, new THREE.Vector3()).distanceTo(leaning)).toBeGreaterThan(0.5); + } + } + disposeRing(ring); + // The red corner's fighter gets the same shot from behind his own corner. + const red = shotFor(-60, 60, "two")!; + expect(red.position.x).toBeCloseTo(-position.x, 6); + expect(red.position.z).toBeCloseTo(-position.z, 6); + // A landscape screen keeps the broadcast's own wide shot. + expect(shotFor(-60, 60, "one", 1)).toBeNull(); + }); + + it("has the referee lift the winner's arm once he has waved the fight off", () => { + const raise = vi.fn(); + const glove = (x: number): THREE.Object3D => { + const bone = new THREE.Object3D(); + bone.position.set(x, 1.9, 0.3); + bone.updateMatrixWorld(true); + return bone; + }; + const stub = prototypeOf({ + referee: { waveOff: vi.fn(), raise, setRefereeCount: vi.fn(), aimBreak: vi.fn(), update: vi.fn(), boxer: { root: new THREE.Object3D() } }, blobShadows: [], buffer: { latest: () => snapshot() }, headCacheValid: [false, false], + settings: () => ({ reducedMotion: false }), frameSeconds: 0, finishCloseUpIndex: -1, + graphs: [{ celebrate: vi.fn(), fallBody: null }, { celebrate: vi.fn(), fallBody: null, boxer: { rig: { bones: { gloveL: glove(1.2), gloveR: glove(0.8) } } } }], + mapping: worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }), + tmpA: new THREE.Vector3(-1.4, 0, -0.6), tmpB: new THREE.Vector3(1, 0, 0.3), refereePosition: new THREE.Vector3(-0.8, 0, -1.4), + refereeVelocity: new THREE.Vector3(), refereeAway: new THREE.Vector3(), refereeYaw: 0, effects: { severedHeadPosition: () => false }, + closeUpTarget: new THREE.Vector3(), replay: null, ceremony: null, stoppageWrist: new THREE.Vector3(), stoppageOtherWrist: new THREE.Vector3(), + }); + method("presentFinish").call(stub, { version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "two", method: "ko", round: 2, scorecards: [], ratings: {} }); + const step = method("updateReferee"); + for (let frame = 0; frame < 150; frame += 1) { + (stub as { frameSeconds: number }).frameSeconds = frame / 60; + step.call(stub, 1 / 60, frame / 60, null, frame / 2); + } + // While the fight is being waved off, nobody's arm goes up. + expect(raise.mock.calls.every(([left, right]) => left === null && right === null)).toBe(true); + for (let frame = 150; frame < 420; frame += 1) { + (stub as { frameSeconds: number }).frameSeconds = frame / 60; + step.call(stub, 1 / 60, frame / 60, null, frame / 2); + } + const referee = (stub as { refereePosition: THREE.Vector3 }).refereePosition; + expect(Math.hypot(referee.x - 0.4, referee.z - 0.3)).toBeLessThan(0.05); + // Beside the winner on his -x side, facing the camera: the referee's left hand holds the winner's nearer glove. + const [left, right] = raise.mock.lastCall!; + expect(right).toBeNull(); + expect((left as THREE.Vector3).distanceTo(new THREE.Vector3(0.8, 1.9, 0.3))).toBeLessThan(1e-6); + }); + + /** The two fighters as drawn, frame after frame, from where the engine has them. */ + const drawing = (graphs: unknown = null) => { + const stub = prototypeOf({ + graphs, mapping: worldMapping(SIMULATION), replay: null, bodyPoint: new THREE.Vector3(), + drawnFighters: [fighter("one"), fighter("two")], drawnOffsets: [{ x: 0, y: 0 }, { x: 0, y: 0 }], drawnTargets: [{ x: 0, y: 0 }, { x: 0, y: 0 }], + }); + return (one: Partial, two: Partial) => { + const [a, b] = method("standApart").call(stub, [{ ...fighter("one"), ...one }, { ...fighter("two"), ...two }], 1 / 60) as [FighterSnapshot, FighterSnapshot]; + return [{ x: a.x, y: a.y }, { x: b.x, y: b.y }] as const; + }; + }; + /** The most either fighter's drawn offset from his engine place changes in one frame, in metres, through a sequence of engine places. */ + const steadiest = (frames: readonly (readonly [Partial, Partial])[]) => { + const draw = drawing(); + const offsets = (one: Partial, two: Partial) => { + const drawn = draw(one, two); + return [{ x: drawn[0].x - (one.x ?? 0), y: drawn[0].y - (one.y ?? 0) }, { x: drawn[1].x - (two.x ?? 0), y: drawn[1].y - (two.y ?? 0) }] as const; + }; + let previous = offsets(...frames[0]!); + let worst = 0; + for (const [one, two] of frames.slice(1)) { + const next = offsets(one, two); + for (const seat of [0, 1] as const) worst = Math.max(worst, Math.hypot(next[seat].x - previous[seat].x, next[seat].y - previous[seat].y) * (3.05 / 500)); + previous = next; + } + return worst; + }; + const hold = (count: number, frame: T): T[] => Array.from({ length: count }, () => frame); + + it("eases fighters apart and together rather than jumping them at a clinch, on the ropes or in a corner", () => { + // Closing from 110 to the engine's 76 units, then holding on: the clinch drops the drawn gap. + const clinch = [...hold(20, [{ x: -55 }, { x: 55 }] as const), ...hold(20, [{ x: -38 }, { x: 38 }] as const), ...hold(30, [{ x: -30, clinch_ticks: 9 }, { x: 30, clinch_ticks: 9 }] as const)]; + // Before: 8.5 cm in one frame for each fighter when the clinch began. + expect(steadiest(clinch)).toBeLessThan(0.02); + // Backed onto the ropes a unit at a time: before, both drawn fighters jumped 6.7 cm when the one on the + // ropes ran out of room to step back. + const ropes = Array.from({ length: 30 }, (_, step) => [{ x: 432 + step - 76 }, { x: 432 + step }] as const); + expect(steadiest(ropes)).toBeLessThan(0.02); + // Backed into a corner, drawn inside the corner pad's cut as the engine keeps him (before: 9 cm into it). + const draw = drawing(); + let cornered = draw({ x: 382, y: 255 }, { x: 440, y: 293 }); + for (let frame = 0; frame < 60; frame += 1) cornered = draw({ x: 382, y: 255 }, { x: 440, y: 293 }); + expect(Math.abs(cornered[1].x) + Math.abs(cornered[1].y)).toBeLessThanOrEqual(RING_CORNER_REACH + 1e-6); + expect(Math.hypot(cornered[1].x - cornered[0].x, cornered[1].y - cornered[0].y)).toBeGreaterThan(103); + }); + + it("starts the decision's walk to the marks from where the fighters were drawn at the bell", () => { + // Engine places 60 units apart, drawn 104 apart. + const { renderer, run, graphs } = frame(fighting({ x: -30 }, { x: 30 })); + run(30); + // The drawn fighters are reused objects: keep the number, not the fighter. + const drawnApart = (graphs[0]!.update.mock.calls.at(-1)![0] as FighterSnapshot).x; + expect(drawnApart).toBeCloseTo(-52, 0); + (renderer as { ceremony: unknown }).ceremony = { winnerSeat: 0, positions: null, refereeArrived: false, arrivedAt: null, announced: false }; + run(1); + // Before, the walk started from the engine's place (x = -30), 13 cm from where he stood a frame before. + const walking = (graphs[0]!.update.mock.calls.at(-1)![0] as FighterSnapshot).x; + expect(Math.abs(walking - drawnApart)).toBeLessThan(200 / 60 + 0.5); + }); + + it("keeps a fighter pushed off a body on the canvas inside the ropes, sliding along them", () => { + const limit = 3.05 * (462 / 500); + let seed = 2026; + const random = (): number => { + seed = Math.imul(seed ^ (seed >>> 15), 1 | seed); + seed ^= seed + Math.imul(seed ^ (seed >>> 7), 61 | seed); + return ((seed ^ (seed >>> 14)) >>> 0) / 4294967296; + }; + let outside = 0; + let inside = 0; + for (let trial = 0; trial < 3000; trial += 1) { + const centre = { x: (random() * 2 - 1) * 2.6, z: (random() * 2 - 1) * 2.6 }; + const angle = random() * Math.PI * 2; + const body = [0, 1, 2, 3, 4].map((point) => new THREE.Vector3(centre.x + Math.cos(angle) * (point - 2) * 0.4, 0, centre.z + Math.sin(angle) * (point - 2) * 0.4)); + const standing = { x: (random() * 2 - 1) * 2.6, z: (random() * 2 - 1) * 2.6 }; + const draw = drawing([{ fallBody: null }, { fallBody: { bodyPoint: (point: number, out: THREE.Vector3) => out.copy(body[point]!) } }]); + const [drawn] = draw({ x: standing.x / (3.05 / 500), y: -standing.z / (3.05 / 500) }, { x: 300, y: 300, is_downed: true }); + const x = drawn.x * (3.05 / 500); + const z = -drawn.y * (3.05 / 500); + if (Math.abs(x) > limit + 1e-3 || Math.abs(z) > limit + 1e-3) outside += 1; + if (body.some((point) => Math.hypot(x - point.x, z - point.z) < 0.5 - 1e-3)) inside += 1; + } + // Before, 33 in 20,000 such pushes left him outside the ropes' limit, by up to 0.37 m. + expect(outside).toBe(0); + expect(inside).toBe(0); + }); + + /** A close-up on fighter two, beaten, with the winner and the referee out of the way. */ + const closeUp = (fields: Record) => prototypeOf({ + finishCloseUpUntil: 101.7, finishCloseUpAt: 100, finishCloseUpIndex: 1, finishCloseUpBearing: null, finishCloseUpHanging: false, headCacheValid: [true, true], + headCache: [new THREE.Vector3(), new THREE.Vector3(0.4, 0.3, 0.1)], arcadeInjuries: [null, null], graphs: null, + closeUpTarget: new THREE.Vector3(), closeUpFacing: new THREE.Vector3(), closeUpPosition: new THREE.Vector3(), replayLookAt: new THREE.Vector3(), bodyPoint: new THREE.Vector3(), + tmpA: new THREE.Vector3(-1.6, 0, -1.2), tmpB: new THREE.Vector3(0.4, 0, 0.1), refereePosition: new THREE.Vector3(-0.6, 0, -1.9), + effects: { severedHeadPosition: () => false, eyePosition: () => false }, + ...fields, + }); + const shoot = (stub: Record, seconds: number) => method("closeUpFrame").call(stub, seconds) as { position: THREE.Vector3; lookAt: THREE.Vector3 } | null; + + it("frames an eye out of its socket one way for the whole close-up, however it swings", () => { + const skull = new THREE.Vector3(0.5, 0.3, 0.2); + const eye = new THREE.Vector3(0.62, 0.3, 0.2); + const stub = closeUp({ + arcadeInjuries: [null, "eye_right"], + effects: { severedHeadPosition: () => false, eyePosition: (_index: number, out: THREE.Vector3) => { out.copy(eye); return true; } }, + headWorldPose: () => ({ position: skull, quaternion: new THREE.Quaternion() }), + }); + const beside = shoot(stub, 100.2)!; + const before = { position: beside.position.clone(), lookAt: beside.lookAt.clone() }; + // The eye swings under the skull, past the line where it would no longer be in sight. + const swung = eye.distanceTo(new THREE.Vector3(0.5, 0.16, 0.2)); + eye.set(0.5, 0.16, 0.2); + const under = shoot(stub, 100.2 + 1 / 60)!; + // Before, the shot switched to the head's own framing there and the camera jumped 55 cm in one frame; + // now it only goes with the eye. + expect(under.position.distanceTo(before.position)).toBeLessThan(swung + 0.01); + expect(under.position.y - under.lookAt.y).toBeCloseTo(before.position.y - before.lookAt.y, 6); + }); + + it("waits for a falling body to come to rest before it picks the side to shoot his face from", () => { + const face = new THREE.Vector3(1, 0, 0); + const body = { body: { asleep: false }, faceDirection: (out: THREE.Vector3) => out.copy(face), bodyPoint: (_point: number, out: THREE.Vector3) => out.set(0.4, 0, 1.2) }; + const stub = closeUp({ graphs: [{ fallBody: null }, { fallBody: body }] }); + // Still falling, face to +x: no close-up yet, and no side picked from a face that is still turning. + let seconds = 100; + for (; seconds < 100.8; seconds += 1 / 60) expect(shoot(stub, seconds)).toBeNull(); + expect(stub.finishCloseUpBearing).toBeNull(); + // He lands face to -z and comes to rest: the shot comes from there, for its full length. + face.set(0, 0, -1); + body.body.asleep = true; + const shot = shoot(stub, seconds)!; + expect(stub.finishCloseUpBearing).toBeCloseTo(Math.PI, 6); + expect(shot.position.z).toBeLessThan(shot.lookAt.z); + expect(stub.finishCloseUpUntil).toBeCloseTo(seconds + 1.7, 1); + // A body that never comes to rest is not waited for past 1.2 s. + const restless = closeUp({ graphs: [{ fallBody: null }, { fallBody: { ...body, body: { asleep: false } } }] }); + expect(shoot(restless, 101.25)).not.toBeNull(); + }); + + /** The referee's own stand-ins, with the winner (seat one) and the beaten fighter where they are drawn. */ + const officiating = (fields: Record) => { + const glove = (x: number): THREE.Object3D => { + const bone = new THREE.Object3D(); + bone.position.set(x, 1.9, 0); + bone.updateMatrixWorld(true); + return bone; + }; + const referee = { raise: vi.fn(), setRefereeCount: vi.fn(), aimBreak: vi.fn(), update: vi.fn(), breaking: false, boxer: { root: new THREE.Object3D() } }; + const stub = prototypeOf({ + referee, blobShadows: [], replay: null, ceremony: null, mapping: worldMapping(SIMULATION), frameSeconds: 0, stoppageWinner: -1, stoppageRaiseAt: Number.POSITIVE_INFINITY, stoppageSpot: null, + graphs: [{ fallBody: null, boxer: { rig: { bones: { gloveL: glove(-0.35), gloveR: glove(-0.65) } } } }, { fallBody: null }], + tmpA: new THREE.Vector3(), tmpB: new THREE.Vector3(), refereePosition: new THREE.Vector3(0.4, 0, -2.1), refereeVelocity: new THREE.Vector3(), refereeAway: new THREE.Vector3(), + refereeYaw: 0, effects: { severedHeadPosition: () => false }, closeUpTarget: new THREE.Vector3(), bodyPoint: new THREE.Vector3(), + stoppageWrist: new THREE.Vector3(), stoppageOtherWrist: new THREE.Vector3(), lastKnockdown: null, + ...fields, + }); + const step = (snapshot: EngineSnapshot | null, frames: number): void => { + for (let frame = 0; frame < frames; frame += 1) { + (stub as { frameSeconds: number }).frameSeconds += 1 / 60; + method("updateReferee").call(stub, 1 / 60, (stub as { frameSeconds: number }).frameSeconds, snapshot, 0); + } + }; + return { stub, referee, step }; + }; + + it("lifts the winner's arm when the beaten fighter or his body stands on the side he would walk to", () => { + // The winner at x = -0.5, so the spot toward the middle of the ring is at x = +0.1: right where the loser stands. + const lying = [new THREE.Vector3(1.4, 0, 0), new THREE.Vector3(1.05, 0, 0), new THREE.Vector3(0.7, 0, 0), new THREE.Vector3(0.35, 0, 0.12), new THREE.Vector3(0.35, 0, -0.12)]; + for (const body of [null, { bodyPoint: (index: number, out: THREE.Vector3) => out.copy(lying[index]!) }]) { + const { stub, referee, step } = officiating({ stoppageWinner: 0, stoppageRaiseAt: 0 }); + (stub.tmpA as THREE.Vector3).set(-0.5, 0, 0); + (stub.tmpB as THREE.Vector3).set(0.3, 0, 0); + (stub.graphs as Array<{ fallBody: unknown }>)[1]!.fallBody = body; + step(null, 300); + // Before, he was pushed off the spot every frame and no arm ever went up. + expect(referee.raise.mock.calls.some(([left, right]) => left !== null || right !== null)).toBe(true); + const at = stub.refereePosition as THREE.Vector3; + // Beside him, 0.6 m away. + expect(Math.hypot(at.x + 0.5, at.z)).toBeCloseTo(0.6, 1); + expect(Math.hypot(at.x - 0.3, at.z)).toBeGreaterThan(0.55); + for (const point of body === null ? [] : lying) expect(Math.hypot(at.x - point.x, at.z - point.z)).toBeGreaterThan(0.55); + } + }); + + it("goes on counting over a fighter who has beaten the count, to the mandatory eight", () => { + const { stub, referee, step } = officiating({ lastKnockdown: { knockdown: { event_id: 4, tick: 30, kind: "knockdown", actor_id: "one", target_id: "two", amount: 1, detail: "", blood: 0, direction: 1, action_id: null }, hit: null, finisher: null } }); + (stub.tmpA as THREE.Vector3).set(-2.2, 0, 2.2); + (stub.tmpB as THREE.Vector3).set(0.6, 0, 0); + // Up at five: the engine no longer has him down, but the phase is still the knockdown's. + const standingEight = { ...snapshot(60), phase: "knockdown" as const, fighters: [fighter("one", -360), { ...fighter("two", 100), get_up_count: 5 }] as const }; + step(standingEight, 120); + expect(referee.setRefereeCount).toHaveBeenLastCalledWith(true, 5); + const at = stub.refereePosition as THREE.Vector3; + expect(Math.hypot(at.x - 0.61, at.z)).toBeLessThan(1.4); + // Once they box on, the count is over. + step({ ...standingEight, phase: "fight" }, 1); + expect(referee.setRefereeCount).toHaveBeenLastCalledWith(false, 0); + }); + + it("counts over the fighter where the fall took him, and where he walks back from once up", () => { + const knockdown = { knockdown: { event_id: 4, tick: 30, kind: "knockdown", actor_id: "one", target_id: "two", amount: 1, detail: "", blood: 0, direction: 1, action_id: null }, hit: null, finisher: null }; + // The physics carried him 1.2 m from his place in the engine (x = 0.61). + const lying = [new THREE.Vector3(2.2, 0, 0), new THREE.Vector3(1.95, 0, 0), new THREE.Vector3(1.7, 0, 0), new THREE.Vector3(1.4, 0, 0.12), new THREE.Vector3(1.4, 0, -0.12)]; + const body = { centre: (out: THREE.Vector3) => out.set(1.82, 0, 0), bodyPoint: (index: number, out: THREE.Vector3) => out.copy(lying[index]!) }; + const down = officiating({ lastKnockdown: knockdown }); + (down.stub.tmpA as THREE.Vector3).set(-2.2, 0, 2.2); + (down.stub.tmpB as THREE.Vector3).set(0.61, 0, 0); + (down.stub.graphs as Array<{ fallBody: unknown }>)[1]!.fallBody = body; + down.step({ ...snapshot(60), phase: "knockdown" as const, fighters: [fighter("one", -360), { ...fighter("two", 100), is_downed: true, get_up_count: 3 }] as const }, 180); + const over = down.stub.refereePosition as THREE.Vector3; + expect(Math.hypot(over.x - 1.82, over.z)).toBeLessThan(1.4); + for (const point of lying) expect(Math.hypot(over.x - point.x, over.z - point.z)).toBeGreaterThan(0.54); + // Up from where he lay and walking back: the referee looks him over there, not at his place in the engine. + const up = officiating({ lastKnockdown: knockdown }); + (up.stub.tmpA as THREE.Vector3).set(-2.2, 0, 2.2); + (up.stub.tmpB as THREE.Vector3).set(0.61, 0, 0); + (up.stub.graphs as Array>)[1]!.currentRoot = { x: 1.7, z: 0.4 }; + up.step({ ...snapshot(60), phase: "knockdown" as const, fighters: [fighter("one", -360), { ...fighter("two", 100), get_up_count: 6 }] as const }, 180); + const looking = up.stub.refereePosition as THREE.Vector3; + expect(Math.hypot(looking.x - 1.7, looking.z - 0.4)).toBeLessThan(1.4); + }); +}); diff --git a/web/hands/src/render/renderer-gpu.test.ts b/web/hands/src/render/renderer-gpu.test.ts index e78c3736..7c480a01 100644 --- a/web/hands/src/render/renderer-gpu.test.ts +++ b/web/hands/src/render/renderer-gpu.test.ts @@ -1,7 +1,7 @@ import * as THREE from "three"; import type { Settings } from "../settings"; import { fighter, snapshot } from "../test/fixtures"; -import type { CombatEvent, FighterSnapshot, FinalMessage, MatchResult } from "../types"; +import { PROTOCOL_VERSION, type CombatEvent, type FighterSnapshot, type FinalMessage, type MatchResult } from "../types"; import { attachFakeWebGl, type FakeWebGl } from "../test/webgl"; import type { Effects3D } from "./effects"; import type { BoxingGraph } from "./graph"; @@ -17,6 +17,8 @@ interface Internals { readonly effects: Effects3D; readonly graphs: readonly [BoxingGraph, BoxingGraph]; restoreAllInjuries(): void; + applyArcadeInjury(index: number, injury: string, event: CombatEvent): boolean; + knockOutMouthpiece(index: number, direction: number, eventId: number, again: boolean): void; } const punch = (eventId: number, tick: number, detail = "straight:head"): CombatEvent => ({ @@ -30,11 +32,17 @@ const exchange = (tick: number, target: "head" | "body", downed = false): readon ]; /** Pushes the snapshot carrying `events` and the next one, so the manual clock presents the contact. */ -function land(fight: FightRenderer, tick: number, events: readonly CombatEvent[], fighters: readonly [FighterSnapshot, FighterSnapshot]): void { - fight.push({ ...snapshot(tick), fighters, events }); - fight.push({ ...snapshot(tick + 1), fighters }); +function land(fight: FightRenderer, tick: number, events: readonly CombatEvent[], fighters: readonly [FighterSnapshot, FighterSnapshot], result: MatchResult | null = null): void { + fight.push({ ...snapshot(tick), fighters, events, result }); + fight.push({ ...snapshot(tick + 1), fighters, result }); } +/** Fighter one knocking fighter two out on `tick`: severed parts are finishers, done by the punch that ends the bout. */ +const knockout = (tick: number): MatchResult => ({ + match_id: "m", activity_instance_id: "a", guild_id: "g", player_one_id: "one", player_two_id: "two", winner_id: "one", finish_method: "ko", + round_number: 1, tick, scorecards: [], player_one_knockdowns: 0, player_two_knockdowns: 1, player_one_damage: 0, player_two_damage: 900, +}); + /** The graphics-card handle three made for a texture. */ const glTexture = (renderer: THREE.WebGLRenderer, texture: THREE.Texture | null | undefined): unknown => texture === null || texture === undefined ? undefined : (renderer.properties.get(texture) as { __webglTexture?: unknown }).__webglTexture; @@ -51,7 +59,7 @@ async function mount(settings: Partial = {}, manualClock = true): Prom const canvas = document.createElement("canvas"); document.body.append(canvas); const gl = attachFakeWebGl(canvas); - const current: Settings = { volume: 0, haptics: false, reducedMotion: false, blood: "full", ...settings }; + const current: Settings = { volume: 0, haptics: false, reducedMotion: false, blood: "full", commentary: false, announcer: false, camera: "broadcast", ...settings }; const fight = new FightRenderer(canvas, undefined, () => current, { manualClock }); mounted.push(fight); await fight.ready; @@ -102,28 +110,37 @@ describe("spray direction", () => { { ...fighter("one"), x: 0, y: -60, facing_x: 0, facing_y: 1000, action_key: "straight:right:head:normal", action_contact_tick: 20 }, { ...fighter("two"), x: 0, y: 60, facing_x: 0, facing_y: -1000, is_downed: true }, ] as const; - land(fight, 20, [punch(40, 20)], fighters); - for (let frame = 0; frame < 18; frame += 1) fight.labFrame((time += 1 / 60)); + land(fight, 20, [punch(40, 20)], fighters, knockout(20)); + // The blood and the head leave from where they burst, the head's bone and the mouth, which stand + // off the fighter's feet in his stance, so their flight is measured from there. + const blood = (): { x: number; z: number; count: number } => { + const positions = internals.effects.dropletBuffers.position; + const colors = internals.effects.dropletBuffers.color; + let x = 0; + let z = 0; + let count = 0; + for (let index = 0; index < positions.count; index += 1) { + if (positions.getY(index) < -10 || colors.getY(index) >= 0.2) continue; + x += positions.getX(index); + z += positions.getZ(index); + count += 1; + } + return { x: x / Math.max(1, count), z: z / Math.max(1, count), count }; + }; + fight.labFrame((time += 1 / 60)); + const start = blood(); + const headStart = new THREE.Vector3(); + expect(internals.effects.severedHeadPosition(1, headStart)).toBe(true); + for (let frame = 1; frame < 18; frame += 1) fight.labFrame((time += 1 / 60)); expect(internals.effects.activeHeads).toBe(1); - const target = { x: 0, z: -60 * (3.05 / 500) }; - const positions = internals.effects.dropletBuffers.position; - const colors = internals.effects.dropletBuffers.color; - let x = 0; - let z = 0; - let count = 0; - for (let index = 0; index < positions.count; index += 1) { - if (positions.getY(index) < -10 || colors.getY(index) >= 0.2) continue; - x += positions.getX(index) - target.x; - z += positions.getZ(index) - target.z; - count += 1; - } - expect(count).toBeGreaterThan(50); - expect(z / count).toBeLessThan(-0.15); - expect(Math.abs(x / count)).toBeLessThan(Math.abs(z / count) / 2); + const end = blood(); + expect(end.count).toBeGreaterThan(50); + expect(end.z - start.z).toBeLessThan(-0.15); + expect(Math.abs(end.x - start.x)).toBeLessThan(Math.abs(end.z - start.z) / 2); const head = new THREE.Vector3(); internals.effects.severedHeadPosition(1, head); - expect(head.z - target.z).toBeLessThan(-0.2); - expect(Math.abs(head.x - target.x)).toBeLessThan(Math.abs(head.z - target.z)); + expect(head.z - headStart.z).toBeLessThan(-0.2); + expect(Math.abs(head.x - headStart.x)).toBeLessThan(Math.abs(head.z - headStart.z)); }); }); @@ -143,7 +160,8 @@ describe("knockdown punch", () => { describe("stoppage without a replay", () => { it("cuts to the finish only once the punch that ended the bout is on screen", async () => { const now = vi.spyOn(performance, "now").mockReturnValue(0); - const { fight } = await mount({}, false); + // A stoppage is replayed in slow motion unless motion is reduced; then it cuts straight to the finish. + const { fight } = await mount({ reducedMotion: true }, false); const draw = (time: number): void => (fight as unknown as { draw(time: number): void }).draw(time); const presentFinish = vi.spyOn(fight as unknown as { presentFinish(final: FinalMessage): void }, "presentFinish"); const shown: number[] = []; @@ -162,7 +180,7 @@ describe("stoppage without a replay", () => { draw((tick * 1000) / 30); } // The final message lands a moment after the result, while the screen is still two ticks behind it. - fight.setFinal({ version: 3, type: "final", match_id: "m", winner_id: "one", method: "tko", round: 1, scorecards: [], ratings: {} }); + fight.setFinal({ version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "one", method: "tko", round: 1, scorecards: [], ratings: {} }); expect(presentFinish).not.toHaveBeenCalled(); for (let time = 1000; time < 1500 && presentFinish.mock.calls.length === 0; time += 1000 / 60) { now.mockReturnValue(time); @@ -228,20 +246,33 @@ describe("graphics memory across rematches on a shared context", () => { }); describe("shaders for effects that start hidden", () => { - it("are compiled before the first bloody hit, decapitation and severed hand", async () => { + it("are compiled before the first bloody hit, decapitation, severed hand, burst head, gouged eye and lost gum shield", async () => { const { gl, fight, internals } = await mount(); let time = settle(fight); const programs = gl.created.programs; - land(fight, 20, [punch(41, 20)], exchange(20, "head")); + // A straight on an open cut, the bleeding behind most of its blood, splashes the canvas as it lands. + land(fight, 20, [{ ...punch(41, 20), amount: 90, blood: 60 }], exchange(20, "head")); fight.labFrame((time += 0.1)); - expect(internals.effects.visibleDecals).toBeGreaterThan(0); - land(fight, 22, [punch(42, 22)], exchange(22, "head", true)); + expect(internals.effects.canvasStains).toBeGreaterThan(0); + land(fight, 22, [punch(42, 22)], exchange(22, "head", true), knockout(22)); fight.labFrame((time += 0.1)); expect(internals.effects.activeHeads).toBe(1); internals.restoreAllInjuries(); - land(fight, 24, [punch(44, 24, "straight:body")], exchange(24, "body", true)); + land(fight, 24, [punch(44, 24, "hook:body")], exchange(24, "body", true), knockout(24)); fight.labFrame((time += 0.1)); expect(internals.effects.activeHands).toBe(1); + // The other finishers, whatever punch earns them, and the gum shield a big punch knocks out. + internals.restoreAllInjuries(); + expect(internals.applyArcadeInjury(1, "head_burst", punch(46, 26))).toBe(true); + fight.labFrame((time += 0.1)); + expect(internals.effects.headBurst(1)).toBe(true); + internals.restoreAllInjuries(); + expect(internals.applyArcadeInjury(1, "eye_left", punch(47, 27))).toBe(true); + fight.labFrame((time += 0.1)); + expect(internals.effects.eyeOut(1)).toBe(true); + internals.knockOutMouthpiece(1, 1, 48, false); + fight.labFrame((time += 0.1)); + expect(internals.effects.mouthpieceOut(1)).toBe(true); fight.labFrame((time += 0.1)); expect(gl.created.programs).toBe(programs); }); diff --git a/web/hands/src/render/renderer-mechanics.test.ts b/web/hands/src/render/renderer-mechanics.test.ts new file mode 100644 index 00000000..739eaf8a --- /dev/null +++ b/web/hands/src/render/renderer-mechanics.test.ts @@ -0,0 +1,323 @@ +import * as THREE from "three"; +import { EventDeduplicator, SnapshotBuffer } from "../interpolation"; +import { EvasionPrediction, MovementPrediction } from "../prediction"; +import { fighter, snapshot } from "../test/fixtures"; +import { PROTOCOL_VERSION, type CombatEvent, type EngineSnapshot } from "../types"; +import { BoxingGraph, SkinnedBoxer, loadBoxerGlb } from "./graph"; +import { RoundStatsTracker } from "./hud"; +import { bodySideStruck, contactPresentationPlan, cutmanWork, FightRenderer, isDelayedBodyKnockdown } from "./renderer"; +import { worldMapping } from "./world"; + +const combat = (kind: string, overrides: Partial = {}): CombatEvent => ({ + event_id: 1, tick: 10, kind, actor_id: "one", target_id: "two", amount: 0, detail: "", blood: 0, direction: 1, action_id: null, ...overrides, +}); + +interface FakeGraph { stagger: ReturnType; windedFor: ReturnType; fallToKnee: ReturnType; resetTransient: ReturnType; primeReplayFall: ReturnType; acknowledge: ReturnType } +const fakeGraphs = (): [FakeGraph, FakeGraph] => [0, 1].map(() => ({ stagger: vi.fn(), windedFor: vi.fn(), fallToKnee: vi.fn(), resetTransient: vi.fn(), primeReplayFall: vi.fn(), acknowledge: vi.fn() })) as [FakeGraph, FakeGraph]; + +const methods = FightRenderer.prototype as unknown as { + push(this: unknown, snapshot: EngineSnapshot): void; + presentFightEvent(this: unknown, event: CombatEvent, recipientIndex: number, puncherIndex: number): void; + recordedHit(this: unknown, knockdown: CombatEvent): CombatEvent | null; +}; + +function pushStub(graphs: [FakeGraph, FakeGraph]): Record { + return { + buffer: new SnapshotBuffer(64, 30), + dedupe: new EventDeduplicator(), + history: [] as EngineSnapshot[], + roundStats: new RoundStatsTracker(), + referee: null, + graphs, + acknowledgeActions: vi.fn(), + mapping: worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }), + contactPoint: new THREE.Vector3(), + pendingContacts: [] as unknown[], + effects: { addEvent: vi.fn() }, + manualClock: true, + lastManualTime: 0, + lastKnockdown: null, + recordedHit: methods.recordedHit, + settings: () => ({ reducedMotion: false, blood: "full" }), + commentary: { observe: vi.fn() }, + movement: new MovementPrediction(), + simulation: { tick_rate: 30 }, + players: {}, + frameSeconds: 0, + liveFallTicks: [null, null], + }; +} + +describe("fight mechanics on screen", () => { + it("presents a parry, a body shot that is putting a fighter down and a swollen eye without an impact of their own", () => { + const frame = snapshot(10); + const plan = contactPresentationPlan([combat("parry"), combat("body_collapse", { amount: 10 }), combat("eye_shut", { detail: "right" })], frame); + expect(plan.map((entry) => entry.presentImpact)).toEqual([false, false, false]); + }); + + it("knows a knockdown a moment after a body shot from one that came with its punch", () => { + const frame = snapshot(20); + const late = combat("knockdown", { tick: 20, detail: "body", action_id: "liver" }); + expect(isDelayedBodyKnockdown(contactPresentationPlan([late], frame)[0]!.presentationEvent)).toBe(true); + const hit = combat("hit", { tick: 20, detail: "hook:body", action_id: "dig" }); + const withPunch = combat("knockdown", { tick: 20, detail: "body", action_id: "dig" }); + const paired = contactPresentationPlan([hit, withPunch], frame)[1]!; + expect(paired.presentationEvent.detail).toBe("hook:body"); + expect(isDelayedBodyKnockdown(paired.presentationEvent)).toBe(false); + }); + + it("staggers the puncher whose power shot was parried and calls it", () => { + const graphs = fakeGraphs(); + const stub = { graphs, buffer: { latest: () => snapshot(10) }, simulation: { tick_rate: 30 }, frameSeconds: 5, eventCallout: null as { text: string; until: number } | null }; + methods.presentFightEvent.call(stub, combat("parry", { actor_id: "two", target_id: "one" }), 0, 1); + expect(graphs[0].stagger).toHaveBeenCalledTimes(1); + expect(graphs[1].stagger).not.toHaveBeenCalled(); + expect(stub.eventCallout).toEqual({ text: "PARRIED", until: 6 }); + methods.presentFightEvent.call(stub, combat("eye_shut", { detail: "left" }), 1, 0); + expect(stub.eventCallout?.text).toBe("EYE SWOLLEN SHUT"); + }); + + it("folds the fighter over the body shot on the side it landed until he goes down", () => { + const graphs = fakeGraphs(); + const frame = { ...snapshot(10), fighters: [{ ...fighter("one"), action_key: "hook:left:body:normal" }, fighter("two")] as const }; + const stub = { graphs, buffer: { latest: () => frame }, simulation: { tick_rate: 30 }, frameSeconds: 0, eventCallout: null }; + methods.presentFightEvent.call(stub, combat("body_collapse", { amount: 9 }), 1, 0); + expect(graphs[1].windedFor).toHaveBeenCalledWith(expect.closeTo(9 / 30 + 0.15, 5), -1); + expect(bodySideStruck("hook:right:body:power")).toBe(1); + expect(bodySideStruck(null)).toBe(-1); + }); + + it("puts a body-shot knockdown on one knee and replays it from the punch that caused it", () => { + const graphs = fakeGraphs(); + const stub = pushStub(graphs); + const hit = combat("hit", { event_id: 3, tick: 30, detail: "hook:body", amount: 70, action_id: "liver" }); + methods.push.call(stub, { ...snapshot(30), events: [hit] }); + const knockdown = combat("knockdown", { event_id: 9, tick: 40, detail: "body", amount: 1, action_id: "liver" }); + const downed = { ...snapshot(40), fighters: [fighter("one"), { ...fighter("two"), is_downed: true }] as const, events: [knockdown] }; + methods.push.call(stub, downed); + expect(graphs[1].fallToKnee).toHaveBeenLastCalledWith(true); + expect((stub.lastKnockdown as { hit: CombatEvent | null }).hit).toEqual(hit); + + const headHit = combat("hit", { event_id: 12, tick: 70, detail: "uppercut:head", amount: 300 }); + const headDown = combat("knockdown", { event_id: 13, tick: 70, amount: 2 }); + methods.push.call(stub, { ...snapshot(70), fighters: [fighter("one"), { ...fighter("two"), is_downed: true }] as const, events: [headHit, headDown] }); + expect(graphs[1].fallToKnee).toHaveBeenLastCalledWith(false); + expect((stub.lastKnockdown as { hit: CombatEvent | null }).hit).toEqual(headHit); + }); + + it("replays a body-shot knockdown on one knee, as it was seen live", () => { + const startReplay = (FightRenderer.prototype as unknown as { startReplay(this: unknown, plan: unknown): void }).startReplay; + const replayFor = (detail: string) => { + const graphs = fakeGraphs(); + const hit = combat("hit", { event_id: 3, tick: 30, detail: "hook:body", amount: 70, action_id: "liver" }); + const stub = { + simulation: { tick_rate: 30 }, frameSeconds: 0, final: null, graphs, + commentary: { replay: vi.fn() }, arcadeInjuries: [null, null], arcadeInjuryEvents: [null, null], replayInjuries: [null, null], + lastKnockdown: { knockdown: combat("knockdown", { event_id: 9, tick: 40, detail, amount: 1, action_id: "liver" }), hit, finisher: null }, + pendingContacts: [], effects: { returnMouthpiece: vi.fn() }, liveFallTicks: [null, null], + }; + startReplay.call(stub, { snapshots: [snapshot(20), snapshot(40)], impact: hit, durationSeconds: 3 }); + return graphs; + }; + const body = replayFor("body"); + // The knee is the animation's, so there is no fall under the physics to run again. + for (const replayed of body) expect(replayed.primeReplayFall).not.toHaveBeenCalled(); + expect(body[1].fallToKnee).toHaveBeenCalledWith(true); + expect(body[0].fallToKnee).not.toHaveBeenCalled(); + expect(replayFor("")[1].fallToKnee).not.toHaveBeenCalled(); + }); + + it("lets the commentary team see every snapshot and the events in it", () => { + const stub = pushStub(fakeGraphs()); + const state = { ...snapshot(30), events: [combat("hit", { event_id: 6, tick: 30, detail: "jab:head", amount: 30 })] }; + methods.push.call(stub, state); + const observe = (stub.commentary as { observe: ReturnType }).observe; + expect(observe).toHaveBeenCalledOnce(); + expect(observe.mock.calls[0]![0]).toBe(state); + expect((observe.mock.calls[0]![1] as CombatEvent[]).map((event) => event.event_id)).toEqual([6]); + }); + + it("calls BOX! when the referee sends them back to it after the eight", () => { + const stub = { ...pushStub(fakeGraphs()), eventCallout: null as { text: string; until: number } | null }; + methods.push.call(stub, { ...snapshot(300), phase: "knockdown", events: [combat("box", { event_id: 7, tick: 300, actor_id: null, target_id: "two" })] }); + expect(stub.eventCallout).toMatchObject({ text: "BOX!" }); + }); + + it("queues the parry and the body collapse to be shown with their punch", () => { + const stub = pushStub(fakeGraphs()); + methods.push.call(stub, { ...snapshot(30), events: [combat("parry", { event_id: 4, actor_id: "two", target_id: "one" }), combat("body_collapse", { event_id: 5, amount: 10 })] }); + expect((stub.pendingContacts as { event: CombatEvent }[]).map((pending) => pending.event.kind)).toEqual(["parry", "body_collapse"]); + }); +}); + +describe("the cutman's work", () => { + const treated = (corner_choice: "cut" | "swelling" | "breath" | null, trauma: Partial["trauma"]>) => ({ ...fighter("one"), corner_choice, trauma: { ...fighter("one").trauma, ...trauma } }); + + it("follows the corner's instruction", () => { + expect(cutmanWork(treated("cut", { left_cut: 100, right_cut: 400, left_eye: 900 }))).toMatchObject({ side: -1, prop: "enswell" }); + expect(cutmanWork(treated("swelling", { left_cut: 100, right_cut: 400, left_eye: 900 }))).toMatchObject({ side: 1, prop: "enswell" }); + expect(cutmanWork(treated("breath", { left_eye: 900 }))).toMatchObject({ lateral: 0, prop: "bottle" }); + expect(cutmanWork(treated("cut", {})).lift).toBeGreaterThan(cutmanWork(treated("swelling", {})).lift); + expect(cutmanWork(treated(null, { right_eye: 300 }))).toMatchObject({ side: -1, prop: "enswell" }); + expect(cutmanWork(undefined)).toMatchObject({ prop: "enswell" }); + }); +}); + +describe("a bout that ends on the punch itself", () => { + const setFinal = (FightRenderer.prototype as unknown as { setFinal(this: unknown, final: unknown): void }).setFinal; + const result = (method: "tko" | "flash_ko") => ({ match_id: "m", activity_instance_id: "i", guild_id: "g", player_one_id: "one", player_two_id: "two", winner_id: "one", finish_method: method, round_number: 2, tick: 160, scorecards: [], player_one_knockdowns: 0, player_two_knockdowns: 3, player_one_damage: 900, player_two_damage: 300 }); + + /** The room's own sequence: a snapshot every tick, and none after the one that carries the result. */ + const fightTo = (method: "tko" | "flash_ko") => { + const graphs = fakeGraphs().map((graph) => ({ ...graph, awaitVerdict: vi.fn(), knockOut: vi.fn() })) as unknown as [FakeGraph, FakeGraph]; + const spies = { presentFinish: vi.fn(), applyArcadeInjury: vi.fn(), returnMouthpiece: vi.fn() }; + const stub = Object.assign(Object.create(FightRenderer.prototype) as Record, pushStub(graphs), { + commentary: { observe: vi.fn(), finish: vi.fn(), replay: vi.fn() }, + effects: { addEvent: vi.fn(), returnMouthpiece: spies.returnMouthpiece }, + endCeremony: vi.fn(), presentFinish: spies.presentFinish, applyArcadeInjury: spies.applyArcadeInjury, restoreInjury: vi.fn(), + arcadeInjuries: [null, null], arcadeInjuryEvents: [null, null], replayInjuries: [null, null], + flashKnockout: null, replay: null, ceremony: null, finalRevealAt: 0, ovationUntil: 0, + }); + for (let tick = 100; tick < 160; tick += 1) methods.push.call(stub, { ...snapshot(tick), fighters: [fighter("one", -60), fighter("two", 60)] }); + const punch = combat("counter_hit", { event_id: 900, tick: 160, detail: "uppercut:head", amount: 140, action_id: "upper" }); + const events = method === "tko" ? [punch, combat("knockdown", { event_id: 901, tick: 160, amount: 3 }), combat("result", { event_id: 902, tick: 160, target_id: null, detail: "tko" })] : [punch, combat("result", { event_id: 902, tick: 160, target_id: null, detail: "flash_ko" })]; + const downed = { ...fighter("two", 60), is_downed: method === "tko" }; + methods.push.call(stub, { ...snapshot(160), phase: "complete", fighters: [{ ...fighter("one", -60), action: "uppercut", action_key: "uppercut:right:head:power", action_contact_tick: 160 }, downed], events, result: result(method) }); + // The final follows at once, while the render clock is still a few ticks behind the punch. + setFinal.call(stub, { version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "one", method, round: 2, scorecards: [], ratings: {} }); + return { stub, ...spies }; + }; + + it("still gets its slow-motion replay, holding the last picture for the fall", () => { + for (const method of ["tko", "flash_ko"] as const) { + const { stub, presentFinish } = fightTo(method); + const replay = stub.replay as { plan: { toTick: number; impact: CombatEvent } } | null; + expect(replay).not.toBeNull(); + expect(replay!.plan.impact.event_id).toBe(900); + expect(replay!.plan.toTick).toBeGreaterThan(160); + expect(presentFinish).not.toHaveBeenCalled(); + } + }); + + it("does the finisher at the replay's impact, not before the punch is shown", () => { + const { stub, applyArcadeInjury, returnMouthpiece } = fightTo("tko"); + expect(applyArcadeInjury).not.toHaveBeenCalled(); + // The punch is the replay's own; the knockdown that came with it waits for the replay's impact. + expect((stub.pendingContacts as { event: CombatEvent; contactTick: number; injury: unknown }[]).map(({ event, contactTick, injury }) => ({ kind: event.kind, contactTick, injury }))).toEqual([{ kind: "knockdown", contactTick: Number.POSITIVE_INFINITY, injury: null }]); + expect((stub.replayInjuries as unknown[])[1]).not.toBeNull(); + expect(returnMouthpiece).toHaveBeenCalledWith(1); + }); + + it("gives the knockdown that ended the bout its thud, roar and rumble at the replay's impact", () => { + const { stub } = fightTo("tko"); + const heard: string[] = []; + const excite = vi.fn(); + const splashed: THREE.Vector3[] = []; + Object.assign(stub, { + onContact: (event: CombatEvent) => heard.push(event.kind), arena: { excite }, viewerId: "two", viewerHitFlash: 0, + knockOutMouthpiece: vi.fn(), headWorldPose: () => null, mouthPoint: new THREE.Vector3(), reapplyReplayInjuries: vi.fn(), + // Where the frame draws them, eased apart: fighter two 0.6 m out, his engine place 0.37 m. + tmpA: new THREE.Vector3(-0.6, 0, 0), tmpB: new THREE.Vector3(0.6, 0, 0), + effects: { addEvent: (_event: CombatEvent, at: THREE.Vector3) => splashed.push(at.clone()), returnMouthpiece: vi.fn() }, + }); + // The graphs' rendered roots are where the frame draws the fighters. + for (const [index, graph] of (stub.graphs as unknown as Record[]).entries()) { + Object.assign(graph, { react: vi.fn(), landedHit: vi.fn(), currentRoot: { x: index === 0 ? -0.6 : 0.6, z: 0 } }); + } + const replay = stub.replay as unknown as { plan: { snapshots: EngineSnapshot[] } }; + const fireContacts = (FightRenderer.prototype as unknown as { fireContacts(this: unknown, tick: number): void }).fireContacts; + const fireReplayImpact = (FightRenderer.prototype as unknown as { fireReplayImpact(this: unknown, snapshot: EngineSnapshot): void }).fireReplayImpact; + // The replay's lead-up shows nothing of it. + fireContacts.call(stub, 150); + expect(heard).toEqual([]); + fireReplayImpact.call(stub, replay.plan.snapshots.find((one) => one.tick === 160)!); + fireContacts.call(stub, 160.3); + // Before, the knockdown's contact was dropped when the replay began: no thud, no roar, no rumble. + expect(heard).toEqual(["counter_hit", "knockdown"]); + // The replayed punch's blood leaves the head where it is drawn. + expect(splashed[0]!.x).toBeCloseTo(0.6, 6); + expect(excite).toHaveBeenCalledWith(1); + expect(stub.viewerHitFlash).toBeGreaterThan(0); + expect(stub.pendingContacts).toEqual([]); + }); +}); + +describe("the knockout replay's fall", () => { + const startReplay = (FightRenderer.prototype as unknown as { startReplay(this: unknown, plan: unknown): void }).startReplay; + const impact = combat("counter_hit", { event_id: 30, tick: 400, detail: "hook:head", amount: 120 }); + const replayed = (graphs: unknown[], liveFallTicks: [number | null, number | null]) => startReplay.call({ + simulation: { tick_rate: 30 }, frameSeconds: 0, final: null, graphs, commentary: { replay: vi.fn() }, + arcadeInjuries: [null, null], arcadeInjuryEvents: [null, null], replayInjuries: [null, null], pendingContacts: [], effects: { returnMouthpiece: vi.fn() }, + lastKnockdown: { knockdown: combat("knockdown", { event_id: 31, tick: 400, amount: 3 }), hit: impact, finisher: null }, liveFallTicks, + }, { snapshots: [snapshot(380), snapshot(400)], impact, durationSeconds: 3 }); + + it("runs the recorded fall again only when it is this knockdown's", () => { + const fallen = fakeGraphs(); + replayed(fallen, [null, 401]); + expect(fallen[1].primeReplayFall).toHaveBeenCalledOnce(); + expect(fallen[0].primeReplayFall).not.toHaveBeenCalled(); + // The record is an earlier knockdown's: the bout ended on the punch before the render clock showed this fall. + const earlier = fakeGraphs(); + replayed(earlier, [null, 120]); + expect(earlier[1].primeReplayFall).not.toHaveBeenCalled(); + }); + + it("falls where he stood when the bout ended on the punch, not where an earlier knockdown left him", async () => { + const gltf = await loadBoxerGlb(); + const mapping = worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }); + const boxer = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); + const graph = new BoxingGraph(boxer, mapping); + let time = 0; + const frames = (self: ReturnType, other: ReturnType, count: number): void => { + for (let frame = 0; frame < count; frame += 1) { + time += 1 / 60; + graph.update(self, other, 1 / 60, time, false, "full", time * 30); + } + }; + const facing = (x: number) => ({ ...fighter("two", x), y: 0, facing_x: 0, facing_y: -1000 }); + const opponent = (x: number) => ({ ...fighter("one", x), y: -150, facing_x: 0, facing_y: 1000 }); + frames(facing(0), opponent(0), 12); + // An earlier knockdown under the physics at x = 0, beaten. + graph.react("hit", "head", 1, "straight", "right", 140); + frames({ ...facing(0), is_downed: true, get_up_required: 100 }, opponent(0), 120); + frames(facing(0), opponent(0), 150); + frames(facing(250), opponent(250), 60); + // The final arrives before the render clock shows this knockdown's fall: the replay starts and replays it. + replayed([fakeGraphs()[0], graph], [null, 120]); + frames(facing(250), opponent(250), 10); + graph.react("hit", "head", 1, "hook", "left", 120); + frames({ ...facing(250), is_downed: true }, opponent(250), 2); + const root = boxer.root.position.clone(); + const start = graph.fallBody!.pelvis(new THREE.Vector3()); + frames({ ...facing(250), is_downed: true }, opponent(250), 300); + const rest = graph.fallBody!.pelvis(new THREE.Vector3()); + // Before, the replayed body started at x = 0.01, where the earlier knockdown fell, and lay 1.58 m from him. + expect(Math.hypot(start.x - root.x, start.z - root.z)).toBeLessThan(0.35); + expect(Math.hypot(rest.x - root.x, rest.z - root.z)).toBeLessThan(1.1); + }, 30_000); +}); + +describe("the player's own fighter folding over a body shot", () => { + const predict = (FightRenderer.prototype as unknown as { applyLocalPrediction(this: unknown, state: EngineSnapshot, dt: number, timeMs: number): EngineSnapshot }).applyLocalPrediction; + + it("is held still from the body shot until he drops, then moves with the controls again", () => { + const stub = { ...pushStub(fakeGraphs()), viewerId: "two", ownCollapseUntil: 0 }; + methods.push.call(stub, { ...snapshot(30), events: [combat("body_collapse", { event_id: 5, tick: 30, actor_id: "one", target_id: "two", amount: 10 })] }); + expect(stub.ownCollapseUntil).toBe(40); + + const viewer = { + localInput: () => ({ moveX: 1000, moveY: 0, defense: "none" as const }), viewerId: "two", graphs: null, buffer: { interpolationDelayTicks: 2 }, + movement: new MovementPrediction(), evasion: new EvasionPrediction(), simulation: { tick_rate: 30 }, inputLatencyMs: null, ownCollapseUntil: 40, + }; + const held = { ...snapshot(35), phase: "fight" as const }; + expect(predict.call(viewer, held, 1 / 30, 1000).fighters[1].x).toBe(held.fighters[1].x); + viewer.ownCollapseUntil = 0; + expect(predict.call(viewer, held, 1 / 30, 1033).fighters[1].x).toBeGreaterThan(held.fighters[1].x); + }); + + it("is not set by a body shot folding the opponent", () => { + const stub = { ...pushStub(fakeGraphs()), viewerId: "one", ownCollapseUntil: 0 }; + methods.push.call(stub, { ...snapshot(30), events: [combat("body_collapse", { event_id: 6, tick: 30, actor_id: "one", target_id: "two", amount: 10 })] }); + expect(stub.ownCollapseUntil).toBe(0); + }); +}); diff --git a/web/hands/src/render/renderer-scene.test.ts b/web/hands/src/render/renderer-scene.test.ts new file mode 100644 index 00000000..9647d658 --- /dev/null +++ b/web/hands/src/render/renderer-scene.test.ts @@ -0,0 +1,301 @@ +import * as THREE from "three"; +import { fighter, snapshot } from "../test/fixtures"; +import type { EngineSnapshot, FighterSnapshot } from "../types"; +import { CameraDirector, FighterCam } from "./camera"; +import { cameraOnPlatform } from "./crowd"; +import { RoundStatsTracker } from "./hud"; +import { BODY_SITES, BODY_SWELL_CORE, InjuryShading, applyBodyTrauma } from "./injury"; +import { FightRenderer } from "./renderer"; +import { worldMapping } from "./world"; + +/** + * Runs the renderer's real frame (draw) against stand-ins for the GPU and the scene, for what the frame + * does with the stands and the blood on the canvas. + */ +const SIMULATION = { tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }; + +type Settings = { reducedMotion: boolean; blood: "full" | "reduced" | "off"; camera: "broadcast" | "close" | "fighter"; commentary: boolean; announcer: boolean; volume: number; haptics: boolean }; + +function graph() { + return { + boxer: { root: new THREE.Object3D(), bone: () => null, setLook: vi.fn(), metrics: { headRestY: 1.52 }, rig: { bones: { gloveL: new THREE.Object3D(), gloveR: new THREE.Object3D() } } }, + setResting: vi.fn(), + setCountdown: vi.fn(), + setObstacle: vi.fn(), + stayDown: vi.fn(), + update: vi.fn(), + fallBody: null, + isDown: false, + }; +} + +function frame(state: EngineSnapshot, overrides: Record = {}) { + const settings: Settings = { reducedMotion: false, blood: "full", camera: "broadcast", commentary: true, announcer: true, volume: 0, haptics: false }; + const latest = { current: state }; + const camera = new THREE.PerspectiveCamera(36, 16 / 9, 0.1, 100); + const renderer = Object.assign(Object.create(FightRenderer.prototype) as Record, { + destroyed: false, + previous: 0, + canvas: { clientWidth: 1280, clientHeight: 720 }, + renderer: { getSize: (out: THREE.Vector2) => out.set(1280, 720), setSize: vi.fn() }, + composer: { setSize: vi.fn(), render: vi.fn() }, + camera, + portraitPull: 1, + baseFov: 36, + sizeCheck: new THREE.Vector2(), + frameSeconds: 0, + finishPass: { uniforms: { uTime: { value: 0 } } }, + lastManualTime: 0, + settings: () => settings, + setBloodLevel: vi.fn(), + buffer: { latest: () => latest.current, sample: () => latest.current, renderTick: () => latest.current.tick, interpolationDelayTicks: 2 }, + updateRocked: vi.fn(), + finishSeen: false, + finishSlowMotion: 0, + ovationUntil: 0, + arena: { excite: vi.fn(), update: vi.fn(), makeRoomForCamera: vi.fn() }, + viewerHitFlash: 0, + localInput: null, + replay: null, + ceremony: null, + drawnFighters: [fighter("one"), fighter("two")], + arcadeInjuries: [null, null], + observedInjuryDown: [false, false], + restoreInjury: vi.fn(), + graphs: [graph(), graph()], + lookIds: [null, null], + headCache: [new THREE.Vector3(), new THREE.Vector3()], + headCacheValid: [false, false], + simulation: SIMULATION, + lastPhase: state.phase, + roundCalloutUntil: 0, + roundCalloutRound: 0, + enterRest: vi.fn(), + anticipatePunches: vi.fn(), + mapping: worldMapping(SIMULATION), + bodyPoint: new THREE.Vector3(), + ring: { setRopeContacts: vi.fn(), setNearRopeOpacity: vi.fn(), nearRopeOpacity: () => 1 }, + tmpA: new THREE.Vector3(), + tmpB: new THREE.Vector3(), + tmpHead: new THREE.Vector3(), + tmpCamera: new THREE.Vector3(), + downedPoolAccumulators: [0, 0], + downedPoolCounts: [0, 0], + effects: { drip: vi.fn(), stopDrip: vi.fn(), pool: vi.fn(), update: vi.fn(), shakeAmount: 0, setViewDistance: vi.fn() }, + updateIdleFighters: vi.fn(), + updateTrails: vi.fn(), + updateReferee: vi.fn(), + updateCornermen: vi.fn(), + updateCutmen: vi.fn(), + updateBlobShadows: vi.fn(), + fireContacts: vi.fn(), + followSpot: null, + director: new CameraDirector(), + viewerId: "one", + fighterCam: new FighterCam(), + ownViewActive: false, + ownForward: { x: 0, z: -1 }, + cameraOverride: null, + finishCloseUpUntil: 0, + finishCloseUpIndex: -1, + restStartedAt: 0, + cutmen: null, + cutmanProgress: [0, 0], + cornerPosition: new THREE.Vector3(), + cornerLookAt: new THREE.Vector3(), + final: null, + players: {}, + roundStats: new RoundStatsTracker(), + hudViewport: { width: 1280, height: 720 }, + manualClock: true, + pixelRatioCap: 2, + basePixelRatio: 2, + pendingFinish: null, + history: [], + drawnOffsets: [{ x: 0, y: 0 }, { x: 0, y: 0 }], + drawnTargets: [{ x: 0, y: 0 }, { x: 0, y: 0 }], + liveFallTicks: [null, null], + fallingLive: [false, false], + stoppageSpot: null, + raiseCentre: new THREE.Vector3(), + raiseFramed: false, + ...overrides, + }); + const draw = (FightRenderer.prototype as unknown as { draw(this: unknown, time: number, manual: boolean, render: boolean): void }).draw; + let time = 1000; + const run = (frames = 1): void => { + for (let index = 0; index < frames; index += 1) { + time += 1000 / 60; + draw.call(renderer, time, true, false); + } + }; + return { renderer, settings, camera, latest, run, graphs: renderer.graphs as ReturnType[] }; +} + +const fighting = (one: Partial = {}, two: Partial = {}, extra: Partial = {}): EngineSnapshot => { + const base = snapshot(40); + return { ...base, fighters: [{ ...base.fighters[0], ...one }, { ...base.fighters[1], ...two }], ...extra }; +}; + +describe("blood from a fighter held on the ropes", () => { + it("leaves from where he is drawn, not from his place in the engine past the ropes", () => { + const event = { event_id: 9, tick: 40, kind: "hit", actor_id: "one", target_id: "two", amount: 80, detail: "straight:head", blood: 30, direction: 1, action_id: null }; + const engine = { ...snapshot(), fighters: [fighter("one", 345), fighter("two", 462)] as [FighterSnapshot, FighterSnapshot] }; + const addEvent = vi.fn(); + const stub = Object.assign(Object.create(FightRenderer.prototype) as Record, { + pendingContacts: [{ event, presentationEvent: event, presentImpact: true, reactAmount: null, contactTick: 40, recipientIndex: 1, puncherIndex: 0, injury: null }], + buffer: { latest: () => engine }, presentFightEvent: vi.fn(), mapping: worldMapping(SIMULATION), contactPoint: new THREE.Vector3(), mouthPoint: new THREE.Vector3(), + effects: { addEvent, spawnTeeth: vi.fn() }, settings: () => ({ reducedMotion: false, blood: "full" }), arena: { excite: vi.fn() }, viewerId: null, + arcadeInjuries: [null, null], graphs: [{ currentRoot: { x: 1.6, z: 0 }, landedHit: vi.fn() }, { currentRoot: { x: 2.33, z: 0.05 }, react: vi.fn() }], headWorldPose: () => null, knockOutMouthpiece: vi.fn(), onContact: null, viewerHitFlash: 0, + }); + (FightRenderer.prototype as unknown as { fireContacts(this: unknown, tick: number): void }).fireContacts.call(stub, 50); + const [, at] = addEvent.mock.calls[0]! as [unknown, THREE.Vector3]; + expect(at.x).toBeCloseTo(2.33, 6); + expect(at.z).toBeCloseTo(0.05, 6); + }); +}); + +describe("ribs caved in by the knockout punch", () => { + const punch = { event_id: 31, tick: 40, kind: "hit", actor_id: "one", target_id: "two", amount: 72, detail: "straight:body", blood: 20, direction: 1, action_id: null }; + const apply = (chestHeight: number) => { + const chest = new THREE.Object3D(); + chest.position.y = chestHeight; + chest.updateMatrixWorld(true); + const addEvent = vi.fn(); + const onArcadeInjury = vi.fn(); + const stub = Object.assign(Object.create(FightRenderer.prototype) as Record, { + graphs: [{ currentRoot: { x: -0.6, z: 0 } }, { currentRoot: { x: 0.62, z: 0.1 }, boxer: { rig: { bones: { upperChest: chest } } } }], + buffer: { latest: () => snapshot() }, mapping: worldMapping(SIMULATION), contactPoint: new THREE.Vector3(), tmpPart: new THREE.Vector3(), + effects: { addEvent }, arcadeInjuries: [null, null], arcadeInjuryEvents: [null, null], observedInjuryDown: [false, false], + syncInjuryPresentation: vi.fn(), onArcadeInjury, + }); + const applied = (FightRenderer.prototype as unknown as { applyArcadeInjury(this: unknown, index: number, injury: string, event: unknown, spray?: unknown): boolean }).applyArcadeInjury.call(stub, 1, "ribs_left", punch, { x: 1, z: 0 }); + return { applied, addEvent, onArcadeInjury, stub }; + }; + + it("burst blood from his side as the punch lands, and are kept for the rest of the bout", () => { + const { applied, addEvent, onArcadeInjury, stub } = apply(1.3); + expect(applied).toBe(true); + expect((stub.arcadeInjuries as unknown[])[1]).toBe("ribs_left"); + expect(onArcadeInjury).toHaveBeenCalledWith("ribs_left", punch); + const [burst, at, reducedMotion, spray] = addEvent.mock.calls[0]! as [{ kind: string; detail: string; amount: number; blood: number; event_id: number }, THREE.Vector3, boolean, unknown]; + // A body burst as big as any blow throws, from where he is drawn, along the punch. + expect(burst).toMatchObject({ kind: "counter_hit", detail: "hook:body", blood: 100 }); + expect(burst.amount).toBeGreaterThanOrEqual(95); + expect(burst.event_id).not.toBe(punch.event_id); + expect([at.x, at.z]).toEqual([0.62, 0.1]); + expect(reducedMotion).toBe(false); + expect(spray).toEqual({ x: 1, z: 0 }); + }); + + it("throw no blood from rib height over a man already lying on the canvas", () => { + const { applied, addEvent } = apply(0.25); + expect(applied).toBe(true); + expect(addEvent).not.toHaveBeenCalled(); + }); + + it("stay caved in every frame over the engine's own bruising", () => { + const bodyInjury = new InjuryShading(null, BODY_SITES, { core: BODY_SWELL_CORE, wash: true }); + const { run, graphs } = frame(fighting({}, { is_downed: true }, { phase: "knockdown" }), { arcadeInjuries: [null, "ribs_left"] }); + (graphs[1]!.boxer as Record).bodyInjury = bodyInjury; + // The graph's own update springs a punch's dent back and paints the engine's trauma. + graphs[1]!.update.mockImplementation(() => { + bodyInjury.update(1 / 60); + applyBodyTrauma(bodyInjury, fighter("two").trauma, "full"); + }); + run(30); + expect(bodyInjury.impactDepth).toBeGreaterThan(5); + expect(bodyInjury.level("leftRibs").blood).toBeCloseTo(1.4, 6); + expect(bodyInjury.level("rightRibs").blood).toBe(0); + }); +}); + +describe("blood on the canvas", () => { + const cut = { ...fighter("two").trauma, bleeding: 200, left_cut: 60 }; + const pools = (renderer: Record): number[][] => (renderer.effects as { pool: ReturnType }).pool.mock.calls as number[][]; + const drips = (renderer: Record): unknown[][] => (renderer.effects as { drip: ReturnType }).drip.mock.calls.filter((call) => call[3] === 1); + + it("pools under a flash knockout's loser, whom the engine never puts down, instead of raining on his old spot", () => { + const result = { match_id: "m", activity_instance_id: "a", guild_id: "g", player_one_id: "one", player_two_id: "two", winner_id: "one", finish_method: "flash_ko" as const, round_number: 1, tick: 40, scorecards: [], player_one_knockdowns: 0, player_two_knockdowns: 0, player_one_damage: 0, player_two_damage: 500 }; + const { renderer, run, graphs } = frame(fighting({}, { trauma: cut }, { phase: "complete", result }), { flashKnockout: { hitEventId: 3, loserId: "two" } }); + graphs[1]!.isDown = true; + // The finish plays in slow motion for its first seconds. + run(300); + expect(drips(renderer)).toHaveLength(0); + expect(pools(renderer).length).toBeGreaterThan(0); + }); + + it("drips from under the chin of the head as it is drawn: seated on his stool, not at standing height over his spot", () => { + const root = new THREE.Object3D(); + const head = new THREE.Bone(); + root.add(head); + // Seated in his corner, his head forward of his place and 40 cm below a standing man's. + head.position.set(0.25, 1.12, 0.3); + head.rotation.y = 0.6; + const { renderer, run, graphs } = frame(fighting({}, { trauma: cut }, { phase: "rest", phase_ticks_remaining: 600 }), { + tmpHeadQuaternion: new THREE.Quaternion(), tmpStumpOffset: new THREE.Vector3(), + }); + Object.assign(graphs[1]!.boxer, { root, bone: (name: string) => (name === "head" ? head : null) }); + run(2); + const [at] = drips(renderer).at(-1)! as [THREE.Vector3]; + root.updateMatrixWorld(true); + const skull = new THREE.Vector3(0, 0.12, 0.02).applyQuaternion(head.quaternion).add(head.getWorldPosition(new THREE.Vector3())); + const chin = skull.add(new THREE.Vector3(0, -0.1, 0.08).applyQuaternion(head.quaternion)); + expect(at.distanceTo(chin)).toBeLessThan(1e-6); + }); + + it("pools from a finisher's open wound however little the engine says he was cut, and further than any cut", () => { + const { renderer, run, graphs } = frame(fighting({}, { is_downed: true }, { phase: "knockdown" }), { arcadeInjuries: [null, "decapitation"] }); + graphs[1]!.isDown = true; + Object.assign(renderer, { headCacheValid: [false, true], headCache: [new THREE.Vector3(), new THREE.Vector3(0.6, 0.15, -0.2)] }); + // Ten seconds on the canvas with no cuts in the engine at all. + run(600); + const calls = pools(renderer); + expect(calls.length).toBeGreaterThan(50); + const widest = Math.max(...calls.map((call) => call[2]!)); + expect(widest).toBeGreaterThan(0.55); + expect(widest).toBeLessThanOrEqual(0.8); + for (const [x, z] of calls) expect(Math.hypot(x! - 0.6, z! + 0.2)).toBeLessThan(0.25); + }); + + it("leaves a dislocation, which opens no wound, to the engine's cuts", () => { + const { renderer, run, graphs } = frame(fighting({}, { is_downed: true }, { phase: "knockdown" }), { arcadeInjuries: [null, "jaw_dislocation"] }); + graphs[1]!.isDown = true; + run(120); + expect(pools(renderer)).toHaveLength(0); + }); + + it("lays a flash knockout's loser's shadow out flat with him", () => { + const blobs = [new THREE.Mesh(), new THREE.Mesh(), new THREE.Mesh()]; + const stub = Object.assign(Object.create(FightRenderer.prototype) as Record, { + blobShadows: blobs, tmpA: new THREE.Vector3(-0.5, 0, 0), tmpB: new THREE.Vector3(0.5, 0, 0), refereePosition: new THREE.Vector3(0, 0, -2), bodyPoint: new THREE.Vector3(), + graphs: [{ fallBody: null, isDown: false, boxer: { root: { visible: false } } }, { fallBody: null, isDown: true, boxer: { root: { visible: false } } }], + buffer: { latest: () => snapshot() }, flashKnockout: { hitEventId: 3, loserId: "two" }, + }); + (FightRenderer.prototype as unknown as { updateBlobShadows(this: unknown): void }).updateBlobShadows.call(stub); + expect(blobs[1]!.scale.x).toBeCloseTo(2.1, 6); + expect(blobs[0]!.scale.x).toBeCloseTo(1.25, 6); + // The engine's word alone, as before: the loser of a flash knockout, never put down by it. + stub.graphs = [{ fallBody: null, isDown: false, boxer: { root: { visible: false } } }, { fallBody: null, isDown: false, boxer: { root: { visible: false } } }]; + (FightRenderer.prototype as unknown as { updateBlobShadows(this: unknown): void }).updateBlobShadows.call(stub); + expect(blobs[1]!.scale.x).toBeCloseTo(2.1, 6); + }); +}); + +describe("the stands in a rendered frame", () => { + it("make room for the broadcast camera only while it stands on its platform", () => { + const placed = (camera: Settings["camera"], portraitPull = 1): boolean => { + const { renderer, settings, camera: lens, run } = frame(fighting(), { portraitPull }); + settings.camera = camera; + run(30); + const call = (renderer.arena as { makeRoomForCamera: ReturnType }).makeRoomForCamera.mock.calls.at(-1)![0] as THREE.Vector3; + // Asked once the frame's camera is in place, with where it stands. + expect(call.x).toBe(lens.position.x); + expect(call.z).toBe(lens.position.z); + return cameraOnPlatform(call.x, call.z); + }; + // The broadcast camera on a tall phone stands among the first rows; over the player's shoulder it is inside the ring. + expect(placed("broadcast", 2.2)).toBe(true); + expect(placed("fighter")).toBe(false); + }); +}); diff --git a/web/hands/src/render/renderer.test.ts b/web/hands/src/render/renderer.test.ts index aa8ca9ed..30bd86b7 100644 --- a/web/hands/src/render/renderer.test.ts +++ b/web/hands/src/render/renderer.test.ts @@ -1,8 +1,11 @@ -import { fighter, snapshot } from "../test/fixtures"; -import type { CombatEvent, MatchResult } from "../types"; import * as THREE from "three"; +import { fighter, mockHudContext, snapshot, type DrawnPicture } from "../test/fixtures"; +import { Avatars } from "./avatars"; +import { RoundClock } from "./hud"; +import { PROTOCOL_VERSION, type CombatEvent, type EngineSnapshot, type MatchResult } from "../types"; import { releaseSharedGpu, disposeSkeletons } from "./graph"; -import { arcadeInjuryFor, canStartPunch, compileForComposer, contactParticipants, contactPresentationPlan, disposeComposer, FightRenderer, isArcadeInjuryCandidate, presentationTickFor, refereeSpacing, replayCameraSide, replayReattaches, sprayDirection } from "./renderer"; +import { arcadeInjuryFor, canStartPunch, compileForComposer, CEREMONY_MARKS, ceremonyStep, contactParticipants, contactPresentationPlan, disposeComposer, cornersAtWork, FightRenderer, ownViewPhase, isArcadeInjuryCandidate, knockdownFinisher, presentationTickFor, refereeSpacing, replayCameraSide, replayReattaches, sprayDirection, visualSeparation, resultCardTop } from "./renderer"; +import { RockedVision } from "./rocked"; import { worldMapping } from "./world"; const event = (kind: string, detail: string): CombatEvent => ({ @@ -103,33 +106,81 @@ describe("spray direction", () => { }); describe("arcade injury candidate routing", () => { - it("accepts authoritative downing anatomical hits and winning flash KOs", () => { - expect(isArcadeInjuryCandidate(event("hit", "straight:head"), { ...fighter("two"), is_downed: true }, null)).toBe(true); - expect(isArcadeInjuryCandidate(event("hit", "hook:body"), { ...fighter("two"), is_downed: true }, null)).toBe(true); + const tko: MatchResult = { ...flashKo, finish_method: "tko" }; + + it("accepts only the blow that ends the bout: a stoppage on the canvas or a winning flash KO", () => { + expect(isArcadeInjuryCandidate(event("hit", "straight:head"), { ...fighter("two"), is_downed: true }, tko)).toBe(true); + expect(isArcadeInjuryCandidate(event("hit", "hook:body"), { ...fighter("two"), is_downed: true }, tko)).toBe(true); expect(isArcadeInjuryCandidate(event("counter_hit", "hook:head"), fighter("two"), flashKo)).toBe(true); }); - it("routes head trauma to head injuries and body trauma to the struck-side limbs", () => { + it("leaves a fighter who is going to get up in one piece", () => { + expect(isArcadeInjuryCandidate(event("hit", "straight:head"), { ...fighter("two"), is_downed: true }, null)).toBe(false); + expect(isArcadeInjuryCandidate(event("hit", "straight:head"), fighter("two"), tko)).toBe(false); + expect(isArcadeInjuryCandidate(event("hit", "straight:head"), { ...fighter("two"), is_downed: true }, { ...tko, finish_method: "decision" })).toBe(false); + }); + + it("routes head trauma to head injuries and body trauma to the struck side", () => { const downed = { ...fighter("two"), is_downed: true }; const leftPunch = { ...fighter("one"), action_key: "hook:left:body:heavy" }; const rightPunch = { ...fighter("one"), action_key: "hook:right:body:heavy" }; - expect(arcadeInjuryFor({ ...event("hit", "hook:head"), event_id: 0 }, downed, null, leftPunch)).toBe("decapitation"); - expect(arcadeInjuryFor({ ...event("hit", "hook:head"), event_id: 1 }, downed, null, leftPunch)).toBe("jaw_dislocation"); - expect(arcadeInjuryFor({ ...event("hit", "hook:body"), event_id: 0 }, downed, null, leftPunch)).toBe("dismember_right"); - expect(arcadeInjuryFor({ ...event("hit", "hook:body"), event_id: 1 }, downed, null, leftPunch)).toBe("shoulder_right"); - expect(arcadeInjuryFor({ ...event("hit", "hook:body"), event_id: 0 }, downed, null, rightPunch)).toBe("dismember_left"); - expect(arcadeInjuryFor({ ...event("hit", "hook:body"), event_id: 1 }, downed, null, rightPunch)).toBe("shoulder_left"); + expect(arcadeInjuryFor({ ...event("hit", "hook:head"), event_id: 0 }, downed, tko, leftPunch)).toBe("eye_right"); + expect(arcadeInjuryFor({ ...event("hit", "hook:head"), event_id: 1 }, downed, tko, leftPunch)).toBe("eye_right"); + expect(arcadeInjuryFor({ ...event("hit", "uppercut:head"), event_id: 1 }, downed, tko, leftPunch)).toBe("decapitation"); + expect(arcadeInjuryFor({ ...event("hit", "hook:body"), event_id: 0 }, downed, tko, leftPunch)).toBe("dismember_right"); + expect(arcadeInjuryFor({ ...event("hit", "hook:body"), event_id: 1, amount: 80 }, downed, tko, leftPunch)).toBe("shoulder_right"); + expect(arcadeInjuryFor({ ...event("hit", "hook:body"), event_id: 0 }, downed, tko, rightPunch)).toBe("dismember_left"); + expect(arcadeInjuryFor({ ...event("hit", "hook:body"), event_id: 1, amount: 80 }, downed, tko, rightPunch)).toBe("shoulder_left"); + expect(arcadeInjuryFor({ ...event("hit", "straight:body"), event_id: 1 }, downed, tko, rightPunch)).toBe("ribs_left"); }); it.each([ - ["standing hit", event("hit", "left:jab:head"), fighter("two"), null], - ["unknown target", event("hit", "straight:arm"), { ...fighter("two"), is_downed: true }, null], - ["block", event("block", "left:straight:head"), { ...fighter("two"), is_downed: true }, null], - ["bleed", event("bleed", "left:straight:head"), { ...fighter("two"), is_downed: true }, null], + ["standing hit", event("hit", "left:jab:head"), fighter("two"), tko], + ["unknown target", event("hit", "straight:arm"), { ...fighter("two"), is_downed: true }, tko], + ["block", event("block", "left:straight:head"), { ...fighter("two"), is_downed: true }, tko], + ["bleed", event("bleed", "left:straight:head"), { ...fighter("two"), is_downed: true }, tko], ["wrong winner", event("counter_hit", "right:hook:head"), fighter("two"), { ...flashKo, winner_id: "two" }], ] as const)("rejects %s", (_name, combatEvent, target, result) => { expect(isArcadeInjuryCandidate(combatEvent, target, result)).toBe(false); }); + + it("works out at the knockdown what the punch earns if the count is beaten by no one", () => { + const floored = { ...snapshot(), fighters: [{ ...fighter("one"), action_key: "hook:right:body:power" }, { ...fighter("two"), is_downed: true }] as const }; + expect(knockdownFinisher({ ...event("hit", "hook:body"), event_id: 0 }, floored)).toBe("dismember_left"); + expect(knockdownFinisher({ ...event("hit", "uppercut:head"), event_id: 0 }, floored)).toBe("decapitation"); + expect(knockdownFinisher(null, floored)).toBeNull(); + }); +}); + +describe("a knockout by the count", () => { + type Finisher = { index: number; injury: string; event: CombatEvent } | null; + const hit = { ...event("counter_hit", "uppercut:head"), event_id: 8 }; + const final = (method: string, winner: string | null = "one") => ({ version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: winner, method, round: 2, scorecards: [], ratings: {} }); + const finishing = (overrides: Record = {}, method = "ko", winner: string | null = "one"): Finisher => { + const stub = { + lastKnockdown: { knockdown: event("knockdown", ""), hit, finisher: "decapitation" }, + settings: () => ({ blood: "full", reducedMotion: false }), + buffer: { latest: () => snapshot() }, + arcadeInjuries: [null, null], + ...overrides, + }; + return (FightRenderer.prototype as unknown as { finishingInjury(this: unknown, final: unknown): Finisher }).finishingInjury.call(stub, final(method, winner)); + }; + + it("earns the beaten fighter the finisher of the punch that put him down", () => { + expect(finishing()).toEqual({ index: 1, injury: "decapitation", event: hit }); + expect(finishing({}, "tko")).toEqual({ index: 1, injury: "decapitation", event: hit }); + }); + + it("earns nothing after another ending, another winner, an injury already done or with gore turned down", () => { + expect(finishing({}, "decision")).toBeNull(); + expect(finishing({}, "ko", "two")).toBeNull(); + expect(finishing({ arcadeInjuries: [null, "jaw_dislocation"] })).toBeNull(); + expect(finishing({ settings: () => ({ blood: "reduced", reducedMotion: false }) })).toBeNull(); + expect(finishing({ settings: () => ({ blood: "full", reducedMotion: true }) })).toBeNull(); + expect(finishing({ lastKnockdown: { knockdown: event("knockdown", ""), hit, finisher: null } })).toBeNull(); + expect(finishing({ lastKnockdown: null })).toBeNull(); + }); }); describe("referee spacing", () => { @@ -142,12 +193,15 @@ describe("referee spacing", () => { }); describe("knockout replay injuries", () => { - it("reattaches severed parts for the replay but leaves dislocations in place", () => { + it("reattaches severed parts and caved-in ribs for the replay but leaves dislocations in place", () => { expect(replayReattaches("decapitation")).toBe(true); expect(replayReattaches("dismember_left")).toBe(true); expect(replayReattaches("dismember_right")).toBe(true); + expect(replayReattaches("ribs_left")).toBe(true); + expect(replayReattaches("ribs_right")).toBe(true); expect(replayReattaches("jaw_dislocation")).toBe(false); expect(replayReattaches("shoulder_left")).toBe(false); + expect(replayReattaches("shoulder_right")).toBe(false); }); }); @@ -189,6 +243,438 @@ describe("replay injuries", () => { }); }); +describe("drawn separation", () => { + it("leaves fighters alone at punching range", () => { + expect(visualSeparation(-60, 0, 60, 0, 104, 462, 462)).toBeNull(); + expect(visualSeparation(0, 0, 104, 0, 104, 462, 462)).toBeNull(); + }); + + it("steps both back evenly along the line between them", () => { + const apart = visualSeparation(-38, 10, 38, 10, 104, 462, 462)!; + expect(apart.bx - apart.ax).toBeCloseTo(104, 6); + expect(apart.ax + apart.bx).toBeCloseTo(0, 6); + expect(apart.ay).toBe(10); + expect(apart.by).toBe(10); + const angled = visualSeparation(0, 0, 30, 40, 104, 462, 462)!; + expect(Math.hypot(angled.bx - angled.ax, angled.by - angled.ay)).toBeCloseTo(104, 6); + expect((angled.ax + angled.bx) / 2).toBeCloseTo(15, 6); + expect((angled.ay + angled.by) / 2).toBeCloseTo(20, 6); + }); + + it("keeps a fighter on the ropes where they are and moves the other the whole way", () => { + const apart = visualSeparation(386, 0, 462, 0, 104, 462, 462)!; + expect(apart.bx).toBe(462); + expect(apart.ax).toBeCloseTo(358, 6); + }); + + it("separates fighters standing on one spot", () => { + const apart = visualSeparation(10, 10, 10, 10, 104, 462, 462)!; + expect(apart.bx - apart.ax).toBeCloseTo(104, 6); + expect(apart.ay).toBe(10); + }); +}); + +describe("decision ceremony", () => { + type Staged = { positions: { x: number; y: number }[] | null; winnerSeat: 0 | 1 | null; refereeArrived: boolean; arrivedAt: number | null; announced: boolean }; + const methods = FightRenderer.prototype as unknown as { + ceremonyFor: (final: unknown) => Staged | null; + ceremonyFighters: (ceremony: Staged, fighters: unknown, dt: number, seconds: number) => ReturnType[]; + }; + const final = (method: string, winner: string | null) => ({ version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: winner, method, round: 3, scorecards: [], ratings: {} }); + const standing = () => [{ ...fighter("one"), x: -300, y: 200 }, { ...fighter("two"), x: 250, y: -120, action: "jab" as const, action_id: "late", defense: "guard_high" as const }]; + + it("walks at no more than the step it is given and stops on the mark", () => { + expect(ceremonyStep(0, 0, { x: 30, y: 40 }, 10)).toEqual({ x: 6, y: 8, arrived: false }); + expect(ceremonyStep(27, 36, { x: 30, y: 40 }, 10)).toEqual({ x: 30, y: 40, arrived: true }); + expect(ceremonyStep(30, 40, { x: 30, y: 40 }, 0)).toEqual({ x: 30, y: 40, arrived: true }); + expect(ceremonyStep(0, 0, { x: 30, y: 40 }, 0)).toEqual({ x: 0, y: 0, arrived: false }); + }); + + it("is held for a bout that went to the cards, with both fighters on their feet", () => { + const stub = (fighters: unknown) => ({ buffer: { latest: () => ({ fighters }) } }); + expect(methods.ceremonyFor.call(stub(standing()), final("decision", "two"))).toMatchObject({ winnerSeat: 1, positions: null, announced: false }); + expect(methods.ceremonyFor.call(stub(standing()), final("draw", null))).toMatchObject({ winnerSeat: null }); + expect(methods.ceremonyFor.call(stub(standing()), final("ko", "one"))).toBeNull(); + expect(methods.ceremonyFor.call(stub(standing()), final("forfeit", "one"))).toBeNull(); + expect(methods.ceremonyFor.call(stub(standing()), null)).toBeNull(); + expect(methods.ceremonyFor.call(stub([{ ...fighter("one"), is_downed: true }, fighter("two")]), final("decision", "two"))).toBeNull(); + expect(methods.ceremonyFor.call({ buffer: { latest: () => null } }, final("decision", "two"))).toBeNull(); + }); + + it("walks both fighters to their marks, turns them to the camera and then raises the winner's arm", () => { + const calls: string[] = []; + const graph = (seat: number) => ({ + awaitVerdict: (side: number) => { if (!calls.includes(`await ${seat} ${side}`)) calls.push(`await ${seat} ${side}`); }, + announce: (verdict: string) => calls.push(`announce ${seat} ${verdict}`), + boxer: { rig: { bones: { gloveL: { getWorldPosition: (out: { set: (x: number, y: number, z: number) => unknown }) => out.set(-0.3, 2, 0.1) }, gloveR: { getWorldPosition: (out: { set: (x: number, y: number, z: number) => unknown }) => out.set(0.3, 2, 0.1) } } } }, + }); + const raised: unknown[][] = []; + const stub = { + drawnFighters: [fighter("a"), fighter("b")], + simulation: { tick_rate: 30 }, + graphs: [graph(0), graph(1)], + referee: { raise: (...wrists: unknown[]) => raised.push(wrists.map((wrist) => (wrist === null ? null : (wrist as { x: number }).x))) }, + arena: { excite: (amount: number) => calls.push(`excite ${amount}`) }, + ceremonyWrists: [{ x: 0, set(x: number) { this.x = x; return this; } }, { x: 0, set(x: number) { this.x = x; return this; } }], + frameSeconds: 0, + finalRevealAt: 99, + commentary: { verdict: () => {} }, + }; + const ceremony: Staged = { winnerSeat: 0, positions: null, refereeArrived: true, arrivedAt: null, announced: false }; + let drawn = methods.ceremonyFighters.call(stub, ceremony, standing(), 1 / 60, 0); + expect(drawn[0]!.x).toBeGreaterThan(-300); + expect(Math.hypot(drawn[0]!.x + 300, drawn[0]!.y - 200)).toBeCloseTo(200 / 60, 6); + expect(Math.hypot(drawn[0]!.velocity_x, drawn[0]!.velocity_y)).toBeCloseTo(200 / 30, 6); + expect(drawn[0]!.facing_x).toBeGreaterThan(0); + expect(drawn[1]!).toMatchObject({ action: null, action_id: null, defense: "none" }); + let seconds = 0; + let arrivedAt: number | null = null; + for (let frame = 0; frame < 400 && !ceremony.announced; frame += 1) { + seconds += 1 / 60; + stub.frameSeconds = seconds; + drawn = methods.ceremonyFighters.call(stub, ceremony, standing(), 1 / 60, seconds); + if (ceremony.arrivedAt !== null) arrivedAt ??= seconds; + } + expect(drawn.map(({ x, y }) => ({ x, y }))).toEqual(CEREMONY_MARKS.map(({ x, y }) => ({ x, y }))); + for (const stood of drawn) expect(stood).toMatchObject({ velocity_x: 0, velocity_y: 0, facing_x: 0, facing_y: -1000 }); + expect(arrivedAt).not.toBeNull(); + expect(seconds - arrivedAt!).toBeGreaterThanOrEqual(0.7 - 1e-9); + expect(seconds - arrivedAt!).toBeLessThan(0.75); + expect(calls).toEqual(["await 0 1", "await 1 -1", "announce 0 winner", "announce 1 loser", "excite 1"]); + expect(stub.finalRevealAt).toBeCloseTo(seconds + 0.35, 6); + expect(raised.at(-1)).toEqual([null, -0.3]); + }); + + it("sends fighters who finished on each other's side to the mark on their own side, so they never cross", () => { + const awaited: string[] = []; + const graph = (seat: number) => ({ + awaitVerdict: (side: number) => { if (!awaited.includes(`${seat} ${side}`)) awaited.push(`${seat} ${side}`); }, + announce: () => {}, + boxer: { rig: { bones: { gloveL: { getWorldPosition: (out: { set: (x: number, y: number, z: number) => unknown }) => out.set(-0.3, 2, 0.1) }, gloveR: { getWorldPosition: (out: { set: (x: number, y: number, z: number) => unknown }) => out.set(0.3, 2, 0.1) } } } }, + }); + const raised: unknown[][] = []; + const stub = { + drawnFighters: [fighter("a"), fighter("b")], simulation: { tick_rate: 30 }, graphs: [graph(0), graph(1)], + referee: { raise: (...wrists: unknown[]) => raised.push(wrists.map((wrist) => (wrist === null ? null : (wrist as { x: number }).x))) }, + arena: { excite: () => {} }, + ceremonyWrists: [{ x: 0, set(x: number) { this.x = x; return this; } }, { x: 0, set(x: number) { this.x = x; return this; } }], + frameSeconds: 0, finalRevealAt: 99, commentary: { verdict: () => {} }, + }; + const swapped = () => [{ ...fighter("one"), x: 250, y: 60 }, { ...fighter("two"), x: -300, y: -40 }]; + const ceremony: Staged = { winnerSeat: 0, positions: null, refereeArrived: true, arrivedAt: null, announced: false }; + let drawn = methods.ceremonyFighters.call(stub, ceremony, swapped(), 1 / 60, 0); + for (let frame = 1; frame < 400 && !ceremony.announced; frame += 1) { + stub.frameSeconds = frame / 60; + drawn = methods.ceremonyFighters.call(stub, ceremony, swapped(), 1 / 60, frame / 60); + expect(drawn[0]!.x).toBeGreaterThan(drawn[1]!.x); + } + expect(ceremony.announced).toBe(true); + expect({ x: drawn[0]!.x, y: drawn[0]!.y }).toEqual(CEREMONY_MARKS[1]); + expect({ x: drawn[1]!.x, y: drawn[1]!.y }).toEqual(CEREMONY_MARKS[0]); + expect(awaited).toEqual(["0 -1", "1 1"]); + expect(raised.at(-1)).toEqual([0.3, null]); + }); + + it("holds the announcement shot still under reduced motion", () => { + const frame = (FightRenderer.prototype as unknown as { ceremonyFrame(this: unknown, seconds: number): { position: THREE.Vector3 } | null }).ceremonyFrame; + const shotAt = (reducedMotion: boolean, seconds: number): number => { + const stub = { + ceremony: { winnerSeat: 0, positions: [], refereeArrived: true, arrivedAt: 10, announced: true }, replay: null, + final: { version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "one", method: "decision", round: 3, scorecards: [], ratings: {} }, + buffer: { latest: () => snapshot() }, hudViewport: { width: 1280, height: 720 }, roundStats: { total: () => ({ thrown: 0, landed: 0 }) }, + players: {}, viewerId: "one", camera: { aspect: 16 / 9, fov: 36 }, cornerPosition: new THREE.Vector3(), cornerLookAt: new THREE.Vector3(), + settings: () => ({ reducedMotion }), + }; + return frame.call(stub, seconds)!.position.x; + }; + expect(shotAt(false, 13)).not.toBeCloseTo(shotAt(false, 16), 3); + expect(shotAt(true, 13)).toBe(0); + expect(shotAt(true, 16)).toBe(0); + }); + + it("waits for the referee, and raises both arms after a draw", () => { + const raised: unknown[][] = []; + const announced: string[] = []; + const graph = () => ({ awaitVerdict: () => {}, announce: (verdict: string) => announced.push(verdict), boxer: { rig: { bones: { gloveL: { getWorldPosition: (out: unknown) => out }, gloveR: { getWorldPosition: (out: unknown) => out } } } } }); + const stub = { drawnFighters: [fighter("a"), fighter("b")], simulation: { tick_rate: 30 }, graphs: [graph(), graph()], referee: { raise: (...wrists: unknown[]) => raised.push(wrists) }, arena: { excite: () => {} }, ceremonyWrists: ["blue", "red"], frameSeconds: 0, finalRevealAt: 99, commentary: { verdict: () => {} } }; + const ceremony: Staged = { winnerSeat: null, positions: CEREMONY_MARKS.map(({ x, y }) => ({ x, y })), refereeArrived: false, arrivedAt: null, announced: false }; + for (let frame = 0; frame < 120; frame += 1) methods.ceremonyFighters.call(stub, ceremony, standing(), 1 / 60, frame / 60); + expect(ceremony.arrivedAt).toBeNull(); + expect(announced).toEqual([]); + ceremony.refereeArrived = true; + for (let frame = 120; frame < 200; frame += 1) methods.ceremonyFighters.call(stub, ceremony, standing(), 1 / 60, frame / 60); + expect(announced).toEqual(["level", "level"]); + expect(raised.at(-1)).toEqual(["red", "blue"]); + }); +}); + +describe("early punches", () => { + const anticipate = (FightRenderer.prototype as unknown as { anticipatePunches: (latest: unknown, sampledTick: number) => void }).anticipatePunches; + const watch = (replay: unknown = null) => { + const calls: { seat: number; id: string | null; lead: number }[] = []; + const graph = (seat: number) => ({ anticipate: (ahead: { action_id: string | null }, lead: number) => calls.push({ seat, id: ahead.action_id, lead }) }); + return { stub: { replay, graphs: [graph(0), graph(1)] }, calls }; + }; + const newest = (phase: string, downed = false) => ({ + ...snapshot(40), phase, + fighters: [{ ...fighter("one"), action_id: "a", action_start_tick: 40 }, { ...fighter("two"), action_id: "b", action_start_tick: 39, is_downed: downed }], + }); + + it("are offered to both fighters with the time the delayed clock needs to reach them", () => { + const { stub, calls } = watch(); + anticipate.call(stub, newest("fight"), 37.5); + expect(calls).toEqual([{ seat: 0, id: "a", lead: 2.5 }, { seat: 1, id: "b", lead: 1.5 }]); + }); + + it("are left alone outside the fight, during the replay, and for a fighter on the canvas", () => { + for (const phase of ["countdown", "knockdown", "rest", "complete", "foul_recovery"]) { + const { stub, calls } = watch(); + anticipate.call(stub, newest(phase), 37.5); + expect(calls).toEqual([]); + } + const replaying = watch({ plan: {} }); + anticipate.call(replaying.stub, newest("fight"), 37.5); + expect(replaying.calls).toEqual([]); + const downed = watch(); + anticipate.call(downed.stub, newest("fight", true), 37.5); + expect(downed.calls.map((call) => call.seat)).toEqual([0]); + const waiting = watch(); + anticipate.call(waiting.stub, null, 37.5); + expect(waiting.calls).toEqual([]); + }); +}); + +describe("ovation", () => { + const methods = FightRenderer.prototype as unknown as { setFinal: (final: unknown) => void; cheer: (seconds: number, dt: number) => void }; + const hall = (frameSeconds: number) => { + const excited: number[] = []; + return { excited, stub: { frameSeconds, buffer: { latest: () => null }, graphs: null, referee: null, arena: { excite: (amount: number) => excited.push(amount) }, ovationUntil: 0, finalRevealAt: 0, ceremony: null, final: null, ceremonyFor: () => null, endCeremony: () => {}, commentary: { finish: () => {} } } }; + }; + const decision = { version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "one", method: "forfeit", round: 3, scorecards: [], ratings: {} }; + + it("keeps the crowd up for sixteen seconds after the result and then lets it sit", () => { + const { stub, excited } = hall(10); + methods.cheer.call(stub, 10, 1 / 60); + expect(excited).toEqual([]); + methods.setFinal.call(stub, decision); + methods.cheer.call(stub, 10.5, 0.5); + methods.cheer.call(stub, 25.9, 0.5); + expect(excited).toHaveLength(2); + expect(excited[0]).toBeGreaterThan(0.5 * 0.3); + methods.cheer.call(stub, 26.1, 0.5); + methods.cheer.call(stub, 400, 0.5); + expect(excited).toHaveLength(2); + }); + + it("ends when the result is withdrawn", () => { + const { stub, excited } = hall(10); + methods.setFinal.call(stub, decision); + methods.setFinal.call(stub, null); + methods.cheer.call(stub, 11, 0.5); + expect(excited).toEqual([]); + }); +}); + +describe("players' pictures", () => { + it("come from Discord by way of the renderer, which draws them once they have arrived", () => { + const made: Array<{ src: string; naturalWidth: number; onload: (() => void) | null }> = []; + const avatars = new Avatars(() => { + const image = { src: "", naturalWidth: 0, onload: null }; + made.push(image); + return image as unknown as HTMLImageElement; + }); + const drawn: DrawnPicture[] = []; + const context = mockHudContext([], drawn); + const self = { + hudCanvas: { width: 0, height: 0, getBoundingClientRect: () => ({ width: 1280, height: 720 }), getContext: () => context }, + hudViewport: { width: 0, height: 0 }, + viewerHitFlash: 0, + settings: () => ({ reducedMotion: false }), + players: { one: { id: "123456789012345678", name: "Alpha", avatar: "abc123", rating: 1500, connected: true }, two: { id: "two", name: "Bravo", avatar: null, rating: 1500, connected: true } }, + viewerId: "one", + frameSeconds: 0, + finalRevealAt: Infinity, + final: null, + reconnectMs: 0, + simulation: { tick_rate: 30 }, + roundStats: null, + replay: null, + inputLatencyMs: null, + roundCalloutUntil: 0, + roundCalloutRound: 1, + eventCallout: null, + roundClock: new RoundClock(), + avatars, + drawCaption: () => {}, + }; + const overlay = (FightRenderer.prototype as unknown as { drawHudOverlay(this: unknown, frame: unknown): void }).drawHudOverlay; + overlay.call(self, snapshot()); + expect(self.hudViewport).toEqual({ width: 1280, height: 720 }); + expect(made.map((image) => image.src)).toEqual(["https://cdn.discordapp.com/avatars/123456789012345678/abc123.png?size=128"]); + expect(drawn).toHaveLength(0); + made[0]!.naturalWidth = 128; + made[0]!.onload!(); + overlay.call(self, snapshot()); + expect(drawn.map((picture) => picture.image)).toEqual([made[0]]); + expect(made).toHaveLength(1); + }); +}); + +describe("the rest between rounds", () => { + it("clears the corners three seconds before the bell", () => { + const rest = (remaining: number) => ({ phase: "rest" as const, phase_ticks_remaining: remaining }); + expect(cornersAtWork(rest(450), 30)).toBe(true); + expect(cornersAtWork(rest(91), 30)).toBe(true); + expect(cornersAtWork(rest(90), 30)).toBe(false); + expect(cornersAtWork(rest(0), 30)).toBe(false); + expect(cornersAtWork(rest(61), 20)).toBe(true); + expect(cornersAtWork(rest(60), 20)).toBe(false); + expect(cornersAtWork({ phase: "fight", phase_ticks_remaining: 450 }, 30)).toBe(false); + expect(cornersAtWork(null, 30)).toBe(false); + }); + + const shot = (FightRenderer.prototype as unknown as { cornerShotFrame(this: unknown, seconds: number, frame: unknown, reducedMotion: boolean): { position: THREE.Vector3; lookAt: THREE.Vector3 } | null }).cornerShotFrame; + const crew = (progress: [number, number], cutmen: unknown = [{}, {}]) => ({ + graphs: [{ stoolVisible: true }, { stoolVisible: true }], + cutmen, + cutmanProgress: progress, + viewerId: "one", + restStartedAt: 10, + simulation: { tick_rate: 30 }, + mapping: worldMapping({ tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }), + cornerPosition: new THREE.Vector3(), + cornerLookAt: new THREE.Vector3(), + }); + const resting = { ...snapshot(), phase: "rest" as const, phase_ticks_remaining: 300 }; + + it("stays on the wide shot until the cutman has got down to his work", () => { + expect(shot.call(crew([0.6, 1]), 13, resting, false)).toBeNull(); + expect(shot.call(crew([0.97, 1]), 13, resting, false)).toBeNull(); + const close = shot.call(crew([1, 0]), 13, resting, false); + expect(close).not.toBeNull(); + expect(close!.lookAt.x).toBeLessThan(-1); + expect(shot.call(crew([1, 0.5]), 13 + 5.5, resting, false)).toBeNull(); + expect(shot.call(crew([1, 1]), 13 + 5.5, resting, false)!.lookAt.x).toBeGreaterThan(1); + }); + + it("does not wait for a crew that was never loaded", () => { + expect(shot.call(crew([0, 0], null), 13, resting, false)).not.toBeNull(); + }); + + it("is back on the wide shot once the seconds are out", () => { + expect(shot.call(crew([1, 1]), 13, { ...resting, phase_ticks_remaining: 91 }, false)).not.toBeNull(); + expect(shot.call(crew([1, 1]), 13, { ...resting, phase_ticks_remaining: 89 }, false)).toBeNull(); + }); +}); + +describe("the broadcast caption", () => { + const draw = (FightRenderer.prototype as unknown as { drawCaption(this: unknown, ctx: CanvasRenderingContext2D, width: number, height: number, state: EngineSnapshot): void }).drawCaption; + const scene = (overrides: Record = {}) => { + const commentary = { resultShown: vi.fn(), current: vi.fn(() => ({ line: { speaker: "play", text: "Down goes Two!", priority: 95, urgent: true, hold: 3, card: null }, opacity: 1, entering: 0 })) }; + return { + commentary, + final: null, + finalRevealAt: 0, + frameSeconds: 10, + resultAnnounced: false, + captionText: "", + hudCanvas: { dataset: {} as Record }, + viewerId: "one", + players: {}, + roundStats: { total: () => ({ thrown: 0, landed: 0 }) }, + touchControls: false, + replay: null, + roundCalloutUntil: 0, + eventCallout: null, + cornerPanelTop: null, + settings: () => ({ commentary: true, reducedMotion: false }), + ...overrides, + }; + }; + + it("draws the line on screen and mirrors it for automation", () => { + const texts: string[] = []; + const self = scene(); + draw.call(self, mockHudContext(texts), 1280, 720, snapshot()); + expect(texts).toEqual(["CALLAHAN", "Down goes Two!"]); + expect(self.hudCanvas.dataset.caption).toBe("Down goes Two!"); + }); + + it("draws nothing when captions are switched off", () => { + const texts: string[] = []; + const self = scene({ settings: () => ({ commentary: false, reducedMotion: false }) }); + draw.call(self, mockHudContext(texts), 1280, 720, snapshot()); + expect(texts).toEqual([]); + expect(self.hudCanvas.dataset.caption).toBeUndefined(); + }); + + it("tells the announcer once that the result card is up", () => { + const final = { version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "one", method: "ko", round: 2, scorecards: [], ratings: {} }; + const self = scene({ final, finalRevealAt: 12 }); + draw.call(self, mockHudContext([]), 1280, 720, snapshot()); + expect(self.commentary.resultShown).not.toHaveBeenCalled(); + self.frameSeconds = 12.5; + draw.call(self, mockHudContext([]), 1280, 720, snapshot()); + draw.call(self, mockHudContext([]), 1280, 720, snapshot()); + expect(self.commentary.resultShown).toHaveBeenCalledOnce(); + }); +}); + +describe("hurt vision in the broadcast finish", () => { + const update = (FightRenderer.prototype as unknown as { updateRocked(this: unknown, latest: EngineSnapshot | null, dt: number, reducedMotion: boolean): void }).updateRocked; + const rig = (viewerId: string | null, replay: unknown = null) => ({ viewerId, replay, final: null as unknown, rocked: new RockedVision(), finishPass: { uniforms: { uRocked: { value: 0 } } } }); + const rocked = (): EngineSnapshot => ({ ...snapshot(), fighters: [{ ...fighter("one", -100), stunned_ticks: 60 }, { ...fighter("two", 100), stunned_ticks: 60 }] }); + + it("blurs the picture for the fighter who is rocked", () => { + const self = rig("one"); + for (let frame = 0; frame < 20; frame += 1) update.call(self, rocked(), 1 / 60, false); + expect(self.finishPass.uniforms.uRocked.value).toBeGreaterThan(0.85); + }); + + it("never blurs it for a spectator, or for a fighter whose opponent is the one rocked", () => { + const spectator = rig(null); + const other = rig("one"); + const opponentRocked: EngineSnapshot = { ...snapshot(), fighters: [fighter("one", -100), { ...fighter("two", 100), stunned_ticks: 60 }] }; + for (let frame = 0; frame < 20; frame += 1) { + update.call(spectator, rocked(), 1 / 60, false); + update.call(other, opponentRocked, 1 / 60, false); + } + expect(spectator.finishPass.uniforms.uRocked.value).toBe(0); + expect(other.finishPass.uniforms.uRocked.value).toBe(0); + }); + + it("clears once the bout is over, even when it ended without a last snapshot", () => { + const self = rig("one"); + for (let frame = 0; frame < 20; frame += 1) update.call(self, rocked(), 1 / 60, false); + self.final = { version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: "two", method: "forfeit", round: 1, scorecards: [], ratings: {} }; + for (let frame = 0; frame < 300; frame += 1) update.call(self, rocked(), 1 / 60, false); + expect(self.finishPass.uniforms.uRocked.value).toBeLessThan(0.02); + }); + + it("is off with reduced motion and during the knockout replay", () => { + const reduced = rig("one"); + const replaying = rig("one", {}); + for (let frame = 0; frame < 20; frame += 1) { + update.call(reduced, rocked(), 1 / 60, true); + update.call(replaying, rocked(), 1 / 60, false); + } + expect(reduced.finishPass.uniforms.uRocked.value).toBe(0); + expect(replaying.finishPass.uniforms.uRocked.value).toBe(0); + }); +}); + +describe("the player's own camera", () => { + it("is used only while the fighters are boxing", () => { + expect(ownViewPhase({ phase: "fight" })).toBe(true); + expect(ownViewPhase({ phase: "countdown" })).toBe(true); + expect(ownViewPhase({ phase: "foul_recovery" })).toBe(true); + expect(ownViewPhase({ phase: "knockdown" })).toBe(false); + expect(ownViewPhase({ phase: "rest" })).toBe(false); + expect(ownViewPhase({ phase: "complete" })).toBe(false); + expect(ownViewPhase(null)).toBe(false); + }); +}); + describe("graphics memory across rematches", () => { it("frees every composer pass before the composer itself", () => { const order: string[] = []; @@ -242,3 +728,38 @@ describe("graphics memory across rematches", () => { expect(calls).toEqual(["bind composer target", "compile", "unbind"]); }); }); + +describe("the result card's place on screen", () => { + it("is laid out once per result and screen size, though it is asked for every frame", () => { + const final = { version: PROTOCOL_VERSION, type: "final" as const, match_id: "m", winner_id: "one", method: "decision" as const, round: 3, scorecards: [], ratings: {} }; + const total = vi.fn(() => ({ thrown: 4, landed: 2, jabsThrown: 1, jabsLanded: 1 })); + const fighters = snapshot().fighters; + const first = resultCardTop(final, 1280, 720, fighters, {}, { total }, "one"); + for (let frame = 0; frame < 60; frame += 1) expect(resultCardTop(final, 1280, 720, fighters, {}, { total }, "one")).toBe(first); + expect(total).toHaveBeenCalledTimes(2); + const phone = resultCardTop(final, 390, 844, fighters, {}, { total }, "one"); + expect(total).toHaveBeenCalledTimes(4); + expect(phone).not.toBe(first); + }); +}); + +describe("the low quality tier", () => { + const apply = (FightRenderer.prototype as unknown as { applyResolutionScale(this: unknown): void }).applyResolutionScale; + const rig = (scale: number) => ({ + basePixelRatio: 2, scaler: { scale }, renderer: { setPixelRatio: vi.fn() }, composer: { setPixelRatio: vi.fn() }, + bloomPass: { enabled: true }, keyLight: null, arena: { setLowTier: vi.fn() }, effects: { setLowTier: vi.fn() }, + }); + + it("sheds the crowd's most expensive work along with the bloom, and gives it back", () => { + const struggling = rig(0.5); + apply.call(struggling); + expect(struggling.arena.setLowTier).toHaveBeenLastCalledWith(true); + expect(struggling.effects.setLowTier).toHaveBeenLastCalledWith(true); + expect(struggling.bloomPass.enabled).toBe(false); + const fine = rig(1); + apply.call(fine); + expect(fine.arena.setLowTier).toHaveBeenLastCalledWith(false); + expect(fine.effects.setLowTier).toHaveBeenLastCalledWith(false); + expect(fine.bloomPass.enabled).toBe(true); + }); +}); diff --git a/web/hands/src/render/renderer.ts b/web/hands/src/render/renderer.ts index 79a6e038..3e7de53d 100644 --- a/web/hands/src/render/renderer.ts +++ b/web/hands/src/render/renderer.ts @@ -6,39 +6,67 @@ import { OutputPass } from "three/examples/jsm/postprocessing/OutputPass.js"; import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass.js"; import { EventDeduplicator, SnapshotBuffer } from "../interpolation"; import { coarsePointer } from "../input/touch"; -import { REST_CORNER_OFFSET, punchTiming } from "../manifest"; -import { EVASION_STAMINA, EvasionPrediction, MovementPrediction, attackTicksRemaining, canAffordPunch, constrainPrediction, isEvasion, predictedDefense, predictedPunchTiming, type HeldInput } from "../prediction"; +import { FIGHTER_RADIUS, REST_CORNER_OFFSET, RING_CORNER_REACH, RING_HALF_HEIGHT, RING_HALF_WIDTH, punchTiming, styleTiming } from "../manifest"; +import { EVASION_STAMINA, EVASION_TICKS, EvasionPrediction, MovementPrediction, attackTicksRemaining, constrainPrediction, isEvasion, predictedDefense, predictedPunchTiming, ringPoint, type HeldInput } from "../prediction"; import type { BloodLevel, Settings } from "../settings"; import type { CombatEvent, EngineSnapshot, FighterSnapshot, FinalMessage, Hand, MatchResult, PublicPlayer, PunchClass, SemanticAction, SimulationInfo } from "../types"; import { buildArena, type BuiltArena } from "./arena"; -import { CameraDirector, CUTMAN_WORK_DEGREES, CUTMAN_WORK_DISTANCE, cornerFrame, cornerPoint, cornerShot, cornerShotProgress } from "./camera"; +import { CameraDirector, CUTMAN_WORK_DEGREES, CUTMAN_WORK_DISTANCE, FIGHTER_CAM_FOV_SCALE, FighterCam, SECONDS_OUT, ceremonyShot, cornerFrame, cornerPoint, cornerShot, cornerShotProgress } from "./camera"; +import { Avatars } from "./avatars"; +import { captionSlot, drawCaption } from "./caption"; +import { CommentaryDirector, type CrowdCue } from "./commentary"; import { Effects3D, type BakedPart, type SprayDirection } from "./effects"; -import { BoxingGraph, SkinnedBoxer, loadBoxerGlb, releaseFighterGpu, type ArcadeDislocation } from "./graph"; -import { drawHud, finalRevealDelay, RoundStatsTracker, STOPPAGE_METHODS, RoundClock } from "./hud"; +import { BoxingGraph, SkinnedBoxer, loadBoxerGlb, releaseFighterGpu, type ArcadeDislocation, type CutmanProp } from "./graph"; +import { drawHud, finalRevealDelay, hudScale, lagWarning, resultCard, resultCardLayout, RoundStatsTracker, STOPPAGE_METHODS, RoundClock, wasBlocked, type RoundPunchStats } from "./hud"; +import { BURST_CUT_HEIGHT, EYE_LIDS, NECK_CUT_DEPTH, NECK_CUT_HEIGHT, NECK_CUT_SLOPE, type InjuryShading } from "./injury"; +import { BIG_SHOT, closeCut, cutRim, teethFor, HARD_SHOT } from "./gore"; +import { mouthpieceFlies } from "./mouthpiece"; +import { OFFICIAL_LOOKS, lookFor, lookShape, type FighterLook } from "./looks"; +import { BLUE_CORNER_OUTFIT, CUTMAN_OUTFIT, RED_CORNER_OUTFIT, REFEREE_OUTFIT } from "./outfit"; import { ResolutionScaler } from "./quality"; +import { RockedVision, rockedLevel } from "./rocked"; import { planKnockoutReplay, replayTick, type ReplayPlan } from "./replay"; import { GloveTrail } from "./trails"; export type ArcadeInjury = | "decapitation" + | "head_burst" + | "eye_left" + | "eye_right" | "dismember_left" | "dismember_right" | "jaw_dislocation" | "shoulder_left" - | "shoulder_right"; + | "shoulder_right" + | "ribs_left" + | "ribs_right"; +/** Phases the player's own over-the-shoulder camera is used in; counts, rests and the finish go to the broadcast. */ +export function ownViewPhase(snapshot: Pick | null): boolean { + return snapshot !== null && (snapshot.phase === "countdown" || snapshot.phase === "fight" || snapshot.phase === "foul_recovery"); +} + +/** Whether the corners are at work: through the rest until the seconds are called out before the bell. */ +export function cornersAtWork(snapshot: Pick | null, tickRate: number): boolean { + return snapshot !== null && snapshot.phase === "rest" && snapshot.phase_ticks_remaining > SECONDS_OUT * tickRate; +} + +export type BoutEnding = Pick; + +/** + * Whether a blow earns an arcade finisher: only the one that ends the bout. A fighter who is going to + * get up keeps his head; the punch that floors a man for the count earns it in the knockout replay. + */ export function isArcadeInjuryCandidate( event: CombatEvent, target: FighterSnapshot | undefined, - result: MatchResult | null, + result: BoutEnding | null, ): boolean { const anatomicalTarget = event.detail.endsWith(":head") || event.detail.endsWith(":body"); if (target === undefined || !["hit", "counter_hit"].includes(event.kind) || !anatomicalTarget) return false; - return target.is_downed || ( - result?.finish_method === "flash_ko" - && result.winner_id !== null - && result.winner_id === event.actor_id - ); + if (result === null || result.winner_id === null || result.winner_id !== event.actor_id) return false; + if (result.finish_method === "flash_ko") return true; + return (result.finish_method === "ko" || result.finish_method === "tko") && target.is_downed; } /** EffectComposer.dispose frees only its own targets; each pass (bloom keeps a chain of them) is freed too. */ @@ -126,18 +154,69 @@ const HISTORY_LIMIT = 480; const LOW_TIER_SCALE = 0.56; const ROUND_CALLOUT_SECONDS = 1.8; const FINISH_CLOSE_UP_SECONDS = 1.7; +/** The close-up waits this long at most for a body still falling to come to rest before it picks its side. */ +const CLOSE_UP_SETTLE_SECONDS = 1.2; +/** + * On a screen narrower than this (as a pull), the rest's wide shot looks down the ring's diagonal from high + * over the near corner: this far back from its stool, swung this many degrees off the diagonal (clear of the + * post, its pads and the cornerman leaning over the ropes), this high, aimed this far toward the far stool. + */ +const REST_DIAGONAL_PULL = 1.6; +const REST_DIAGONAL_BACK = 3.5; +const REST_DIAGONAL_SWING = 30; +const REST_DIAGONAL_HEIGHT = 6.5; +const REST_DIAGONAL_AIM = 0.4; +/** After a stoppage the winner celebrates until the bout is gone, and the referee lifts his arm once the fight is waved off. */ +const WINNER_CELEBRATION_SECONDS = 600; +const STOPPAGE_RAISE_DELAY_SECONDS = 2.8; +const STOPPAGE_RAISE_SPACING = 0.6; +/** + * The spots beside the winner the referee tries in turn for the arm raise, each as [toward the middle of + * the ring, toward the broadcast camera] in STOPPAGE_RAISE_SPACINGs: either side of him, then a little + * behind, then a little in front. + */ +const RAISE_SPOTS = [[1, 0], [-1, 0], [0.75, -0.66], [-0.75, -0.66], [0.75, 0.66], [-0.75, 0.66]] as const; +/** A spot is his once he stands this near it; it keeps this far from the beaten fighter and inside this square. */ +const RAISE_ARRIVED = 0.15; +const RAISE_CLEARANCE = 0.6; +const RAISE_LIMIT = 2.3; /** Longest a finish waits for its punch to be shown (the render clock runs at most 6 ticks behind). */ const FINISH_WAIT_LIMIT_SECONDS = 0.5; const CORNERMAN_APRON_DISTANCE = 3.42; /** Close cameras stay inside the rope line (posts stand at 2.46 m) so a rope never fills the lens. */ const TIGHT_SHOT_LIMIT = 2.2; +/** How far the referee keeps from a head on the canvas. */ +const HEAD_CLEARANCE = 0.9; +/** Knockdown detail for a body shot that drops a fighter to one knee. */ +const BODY_KNOCKDOWN = "body"; +/** The referee keeps this far from any part of a fighter lying on the canvas. */ +const BODY_CLEARANCE = 0.55; +/** How far the standing fighter's middle keeps from any part of a body on the canvas, so his feet are off it. */ +const STANDING_BODY_CLEARANCE = 0.5; +/** How far from someone on their feet a close-up has to pass to see past them. */ +const STANDING_BLOCK_RADIUS = 0.5; +/** Parts of a body lying on the canvas that a close-up of its head does not shoot across. */ +const FALLEN_BLOCK_RADIUS = 0.28; +/** The engine lets fighters stand 76 units apart, which puts two drawn bodies inside each other. */ +const DRAWN_MINIMUM_GAP = 104; +/** + * How quickly (per second) a fighter drawn off his engine place follows where he should be drawn, and how + * fast at most (engine units per second, about a walk), so a clinch, a knockdown, the ropes or a body landing + * beside him never jump him. + */ +const DRAWN_SHIFT_RATE = 14; +const DRAWN_SHIFT_SPEED = 260; +/** How far, in metres, a fighter slides along the ropes at most to find a place clear of a body on the canvas, in steps of this much. */ +const ROPE_SLIDE_LIMIT = 2; +const ROPE_SLIDE_STEP = 0.05; const CORNERMAN_WORK_DISTANCE = 2.95; /** Metres per second the referee steps in at to break a clinch. */ const REFEREE_BREAK_SPEED = 2.4; const CUTMAN_WALK_SECONDS = 1.6; +const CUTMAN_IN_PLACE = 0.98; // Broadcast finish: a soft vignette and a whisper of grain, applied before tone mapping. const BROADCAST_FINISH_SHADER = { - uniforms: { tDiffuse: { value: null }, uTime: { value: 0 }, uVignette: { value: 0.32 }, uGrain: { value: 0.035 } }, + uniforms: { tDiffuse: { value: null }, uTime: { value: 0 }, uVignette: { value: 0.32 }, uGrain: { value: 0.035 }, uRocked: { value: 0 } }, vertexShader: ` varying vec2 vUv; void main() { @@ -149,6 +228,7 @@ uniform sampler2D tDiffuse; uniform float uTime; uniform float uVignette; uniform float uGrain; +uniform float uRocked; varying vec2 vUv; float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7)) + uTime * 43.7) * 43758.5453); @@ -156,14 +236,71 @@ float hash(vec2 p) { void main() { vec4 color = texture2D(tDiffuse, vUv); vec2 centered = vUv - 0.5; + if (uRocked > 0.001) { + // Hurt vision: a softened picture with a second image drifting off it, drained of colour. + vec2 spread = vec2(0.0035, 0.0) * uRocked; + vec3 soft = (texture2D(tDiffuse, vUv + spread).rgb + texture2D(tDiffuse, vUv - spread).rgb + texture2D(tDiffuse, vUv + spread.yx * 1.6).rgb + texture2D(tDiffuse, vUv - spread.yx * 1.6).rgb) * 0.25; + vec3 ghost = texture2D(tDiffuse, vUv + vec2(sin(uTime * 2.3), cos(uTime * 1.7)) * 0.011 * uRocked).rgb; + color.rgb = mix(color.rgb, mix(soft, ghost, 0.5), 0.75 * uRocked); + color.rgb = mix(color.rgb, vec3(dot(color.rgb, vec3(0.299, 0.587, 0.114))), 0.65 * uRocked); + } float falloff = smoothstep(0.35, 0.95, dot(centered, centered) * 2.2); color.rgb *= 1.0 - falloff * uVignette; + color.rgb *= mix(vec3(1.0), vec3(0.5, 0.1, 0.08), smoothstep(0.08, 0.9, dot(centered, centered) * 2.2) * uRocked * 0.85); float grain = (hash(floor(vUv * vec2(960.0, 540.0))) - 0.5) * uGrain; color.rgb += grain * (0.15 + color.rgb); gl_FragColor = color; }`, }; -const CROWD_EXCITEMENT: Readonly> = { hit: 0.18, counter_hit: 0.3, guard_break: 0.25, knockdown: 1, block: 0.04, perfect_block: 0.1 }; +/** Where the fighters stand for the decision, in engine units: either side of the referee, facing the camera. */ +export const CEREMONY_MARKS = [{ x: -102, y: -16 }, { x: 102, y: -16 }] as const; +const SEAT_MARKS = [0, 1] as const; +/** While he walks to his mark the referee keeps this far from the fighters, who stand 0.62 m either side of it. */ +const CEREMONY_WALK_CLEARANCE = 0.5; +const CEREMONY_REFEREE = { x: 0, z: -0.2 } as const; +const ANNOUNCEMENT_ROPE_OPACITY = 0; +const CEREMONY_WALK_SPEED = 200; +const CEREMONY_REFEREE_SPEED = 1.3; +const CEREMONY_PAUSE_SECONDS = 0.7; +/** The card comes up with the raised arm, and no later than this after the final bell. */ +const CEREMONY_REVEAL_LIMIT_SECONDS = 4.5; + +/** One step of a walk to a mark at no more than `reach`; `arrived` once it is on the mark. */ +export function ceremonyStep(x: number, y: number, mark: { readonly x: number; readonly y: number }, reach: number): { x: number; y: number; arrived: boolean } { + const dx = mark.x - x; + const dy = mark.y - y; + const distance = Math.hypot(dx, dy); + if (distance <= Math.max(reach, 1e-6)) return { x: mark.x, y: mark.y, arrived: true }; + return { x: x + (dx / distance) * reach, y: y + (dy / distance) * reach, arrived: false }; +} + +interface Ceremony { + readonly winnerSeat: 0 | 1 | null; + /** Where the fighters are drawn, once the walk to the marks has begun. */ + positions: [{ x: number; y: number }, { x: number; y: number }] | null; + /** The mark each seat walks to: the one on his own side, so fighters who ended the bout on each other's side never cross. */ + marks?: readonly [0 | 1, 0 | 1]; + refereeArrived: boolean; + arrivedAt: number | null; + announced: boolean; +} + +/** The crowd stays on its feet from the result until the replay and the verdict have played out. */ +const CROWD_OVATION_SECONDS = 16; +const CROWD_OVATION_RATE = 0.6; +const CROWD_EXCITEMENT: Readonly> = { hit: 0.18, counter_hit: 0.3, guard_break: 0.25, knockdown: 1, block: 0.04, perfect_block: 0.1, parry: 0.22, body_collapse: 0.45 }; +/** Events presented on the contact tick of the punch they belong to, alongside its impact. */ +const CONTACT_KINDS = new Set(["hit", "counter_hit", "block", "perfect_block", "guard_break", "knockdown", "parry", "body_collapse", "eye_shut"]); +/** Events that change how a fighter moves or what the broadcast says, but carry no impact of their own. */ +const UNIMPACTFUL_KINDS = new Set(["parry", "body_collapse", "eye_shut"]); +const BOX_CALLOUT_SECONDS = 1.2; +const EVENT_CALLOUTS: Readonly> = { + parry: { text: "PARRIED", seconds: 1 }, + eye_shut: { text: "EYE SWOLLEN SHUT", seconds: 1.6 }, +}; + +/** A knockdown from a body shot that put him down a moment after it landed, with no punch of its own to show. */ +export const isDelayedBodyKnockdown = (presentation: CombatEvent): boolean => presentation.kind === "knockdown" && presentation.detail === "body"; function pairedBlock(event: CombatEvent, events: readonly CombatEvent[]): CombatEvent | undefined { if (!isHit(event) || event.action_id === null) return undefined; @@ -186,6 +323,7 @@ export function contactPresentationPlan( snapshot: EngineSnapshot, ): readonly ContactPresentation[] { return events.map((event, eventIndex) => { + if (UNIMPACTFUL_KINDS.has(event.kind)) return { event, presentationEvent: event, presentImpact: false, reactAmount: null }; const { puncherIndex } = contactParticipants(event, snapshot); const puncher = snapshot.fighters[puncherIndex]; const actionParts = puncher?.action_key?.split(":") ?? []; @@ -232,24 +370,68 @@ export function contactPresentationPlan( }); } +/** + * Where the cutman works and with what, from the corner's instruction: the brow of the worse cut, the + * worse eye, or the mouth with a bottle. `side` is 1 for the fighter's left, `lift` is metres above the head bone. + */ +export function cutmanWork(fighter: FighterSnapshot | undefined): { readonly side: number; readonly lift: number; readonly lateral: number; readonly prop: CutmanProp } { + const trauma = fighter?.trauma; + if (trauma === undefined) return { side: 1, lift: 0.08, lateral: 0.035, prop: "enswell" }; + switch (fighter?.corner_choice) { + case "cut": + return { side: trauma.right_cut > trauma.left_cut ? -1 : 1, lift: 0.11, lateral: 0.04, prop: "enswell" }; + case "swelling": + return { side: trauma.right_eye > trauma.left_eye ? -1 : 1, lift: 0.08, lateral: 0.035, prop: "enswell" }; + case "breath": + return { side: 1, lift: -0.015, lateral: 0, prop: "bottle" }; + default: + return { side: trauma.right_eye + trauma.right_cut > trauma.left_eye + trauma.left_cut ? -1 : 1, lift: 0.08, lateral: 0.035, prop: "enswell" }; + } +} + +/** The side of the body a punch lands on, from the puncher's action key: a left hand lands on his right side (-1). */ +export function bodySideStruck(actionKey: string | null): number { + return actionKey?.split(":")[1] === "right" ? 1 : -1; +} + export function presentationTickFor(snapshot: EngineSnapshot): number { return snapshot.result === null ? snapshot.tick - 1 : snapshot.tick; } +/** A straight this hard to the head takes it off: most land for 30 to 80. */ +const DECAPITATING_STRAIGHT = 80; +const PUNCH_CLASSES: readonly string[] = ["jab", "straight", "hook", "uppercut"]; + +/** + * The finisher the punch that ends the bout earns, by where it landed, what it was and how hard it was: + * - to the head, a flash knockout or a big counter bursts it, a hard uppercut or straight takes it off, a + * hard hook forces out the eye on the side it lands, and anything lighter dislocates the jaw; + * - to the body, a hook comes round through the elbow covering the ribs, so a big one takes the forearm + * off and a hard one puts the shoulder out; otherwise the ribs it lands on cave in. + */ export function arcadeInjuryFor( event: CombatEvent, target: FighterSnapshot | undefined, - result: MatchResult | null, + result: BoutEnding | null, puncher?: FighterSnapshot, ): ArcadeInjury | null { if (!isArcadeInjuryCandidate(event, target, result)) return null; - const selection = Math.abs(event.event_id); - if (event.detail.endsWith(":head")) return selection % 2 === 0 ? "decapitation" : "jaw_dislocation"; - const hand = puncher?.action_key?.split(":")[1]; - const recipientSide = hand === "left" ? "right" : hand === "right" ? "left" : Math.floor(selection / 2) % 2 === 0 ? "left" : "right"; - return selection % 2 === 0 ? `dismember_${recipientSide}` : `shoulder_${recipientSide}`; + const key = puncher?.action_key?.split(":"); + const punch = event.detail.split(":").find((part) => PUNCH_CLASSES.includes(part)) ?? key?.[0]; + // A left hand lands on his right side; without the puncher's hand, on the side an orthodox lead lands on. + const struck = key?.[1] === "right" ? "left" : "right"; + const amount = event.amount; + if (event.detail.endsWith(":head")) { + if (result?.finish_method === "flash_ko" || (event.kind === "counter_hit" && amount >= BIG_SHOT)) return "head_burst"; + if ((punch === "uppercut" && amount >= HARD_SHOT) || (punch === "straight" && amount >= DECAPITATING_STRAIGHT)) return "decapitation"; + if (punch === "hook" && amount >= HARD_SHOT) return `eye_${struck}`; + return "jaw_dislocation"; + } + if (punch === "hook" && amount >= BIG_SHOT) return `dismember_${struck}`; + if (punch === "hook" && amount >= HARD_SHOT) return `shoulder_${struck}`; + return `ribs_${struck}`; } -import { buildRing, disposeRing, type BuiltRing } from "./ring"; +import { buildRing, disposeRing, nearRopeOpacityFor, type BuiltRing } from "./ring"; import { resizeHighDpi } from "./viewport"; import { worldMapping, type WorldMapping } from "./world"; @@ -269,22 +451,265 @@ function blobShadowTexture(): THREE.CanvasTexture { return new THREE.CanvasTexture(canvas); } +/** A severed head turns about the middle of the skull: this far from the head bone, in the bone's frame. */ +const HEAD_PIVOT_OFFSET = new THREE.Vector3(0, 0.12, 0.02); +const UP = new THREE.Vector3(0, 1, 0); +const neckRims = new WeakMap(); + +/** The edges of a head mesh along the cut through the neck. Every fighter shares the one mesh. */ +function neckRim(geometry: THREE.BufferGeometry): number[] { + let rim = neckRims.get(geometry); + if (rim === undefined) { + rim = cutRim(geometry, aboveNeckCut); + neckRims.set(geometry, rim); + } + return rim; +} + +/** True for the part of the head mesh a burst takes away: everything above the jaw, below the deepest dip of its ragged edge. */ +export function aboveBurstCut(bind: THREE.Vector3): boolean { + return bind.y > BURST_CUT_HEIGHT - 1.4; +} + +const burstRims = new WeakMap(); + +/** The edges of a head mesh round what a burst leaves on the neck. */ +function burstRim(geometry: THREE.BufferGeometry): number[] { + let rim = burstRims.get(geometry); + if (rim === undefined) { + rim = cutRim(geometry, aboveBurstCut); + burstRims.set(geometry, rim); + } + return rim; +} + +/** + * What a burst leaves on the neck, measured on the skin as it is posed: the middle of the opening, + * facing up the head, the head's own sideways axis so the jaw's teeth stay at the front, and the + * edge, both ends of each of its edges in turn, written into `rim` (or a new array of the right size). + */ +export function measureBurstStump( + boxer: SkinnedBoxer, + rim: Float32Array, + out: { readonly position: THREE.Vector3; readonly quaternion: THREE.Quaternion; readonly across: THREE.Vector3; readonly scratch: THREE.Vector3 }, +): Float32Array | null { + const head = boxer.bone("head"); + if (head === null) return null; + boxer.root.updateMatrixWorld(true); + const mesh = boxer.headMesh; + const edges = burstRim(mesh.geometry); + if (edges.length === 0) return null; + const written = rim.length === edges.length * 3 ? rim : new Float32Array(edges.length * 3); + out.position.set(0, 0, 0); + for (const [at, corner] of edges.entries()) { + posedHeadVertex(boxer, corner, out.scratch); + out.scratch.toArray(written, at * 3); + out.position.add(out.scratch); + } + out.position.multiplyScalar(1 / edges.length); + head.getWorldQuaternion(out.quaternion); + out.across.set(1, 0, 0).applyQuaternion(out.quaternion); + out.scratch.set(0, 1, 0).applyQuaternion(out.quaternion); + out.quaternion.setFromUnitVectors(UP, out.scratch); + return written; +} + +/** + * Where a fighter's blood, sweat and chunks leave from, into `out`: under him as he is drawn, which is not + * always his place in the engine (held on the ropes he is drawn in off them, and he gets up where a fall left him). + */ +function drawnPlace(graph: { readonly currentRoot?: { readonly x: number; readonly z: number } } | undefined, fighter: FighterSnapshot, mapping: WorldMapping, out: THREE.Vector3): THREE.Vector3 { + const drawn = graph?.currentRoot; + return out.set(drawn?.x ?? mapping.x(fighter.x), 0, drawn?.z ?? mapping.z(fighter.y)); +} + +/** The place nearest to (x, z) that is at least `clearance` from (fromX, fromZ). */ +export function keepClear(x: number, z: number, fromX: number, fromZ: number, clearance: number): { x: number; z: number } { + const distance = Math.hypot(x - fromX, z - fromZ); + if (distance >= clearance) return { x, z }; + if (distance < 1e-6) return { x: fromX, z: fromZ - clearance }; + return { x: fromX + ((x - fromX) / distance) * clearance, z: fromZ + ((z - fromZ) / distance) * clearance }; +} + +export interface Blocker { + readonly x: number; + readonly z: number; + readonly radius: number; +} + +const CLOSE_UP_TURNS = [0, 1.05, -1.05, 2.1, -2.1, Math.PI] as const; + +/** + * Bearing from a head to the camera for its close-up. The camera goes to the side the face points + * to when the face points sideways, and otherwise to `fallback`. A side is passed over when the + * ropes leave no room to stand back, or when someone stands in it or in the way of the shot. + */ +export function closeUpAngle( + x: number, + z: number, + facing: { readonly x: number; readonly y: number; readonly z: number } | null, + reach: number, + limit: number, + fallback: number, + blockers: readonly Blocker[] = [], +): number { + const level = facing === null ? 0 : Math.hypot(facing.x, facing.z); + const sides = facing !== null && level >= 0.35 ? [Math.atan2(facing.x, facing.z)] : []; + for (const turn of CLOSE_UP_TURNS) sides.push(fallback + turn); + for (const side of sides) { + const cameraX = THREE.MathUtils.clamp(x + Math.sin(side) * reach, -limit, limit); + const cameraZ = THREE.MathUtils.clamp(z + Math.cos(side) * reach, -limit, limit); + const length = Math.hypot(cameraX - x, cameraZ - z); + if (length < reach * 0.7) continue; + const blocked = blockers.some((blocker) => { + // Nearest point of the shot to the blocker, from just off the head to the camera. + const along = THREE.MathUtils.clamp(((blocker.x - x) * (cameraX - x) + (blocker.z - z) * (cameraZ - z)) / (length * length), 0.25, 1); + return Math.hypot(x + (cameraX - x) * along - blocker.x, z + (cameraZ - z) * along - blocker.z) < blocker.radius; + }); + if (!blocked) return side; + } + return fallback; +} + +/** + * How far a fighter at (x, y) can step along the unit direction (dx, dy) before the ropes or a corner pad + * stop him: inside |x| <= limitX, |y| <= limitY and |x| + |y| <= cornerReach. + */ +function roomAlong(x: number, y: number, dx: number, dy: number, limitX: number, limitY: number, cornerReach: number): number { + let room = Number.POSITIVE_INFINITY; + if (Math.abs(dx) > 1e-9) room = Math.min(room, (Math.sign(dx) * limitX - x) / dx); + if (Math.abs(dy) > 1e-9) room = Math.min(room, (Math.sign(dy) * limitY - y) / dy); + for (const sx of [-1, 1]) { + for (const sy of [-1, 1]) { + const toward = sx * dx + sy * dy; + if (toward > 1e-9) room = Math.min(room, (cornerReach - sx * x - sy * y) / toward); + } + } + return Math.max(0, room); +} + +/** + * Positions for drawing two fighters the engine has closer than `minimum`: both step back along the + * line between them. One held by the ropes or a corner pad steps back as far as he has room to, and the + * other gives the rest, so the split changes as smoothly as the room does. Null when they are already far + * enough apart. + */ +export function visualSeparation( + ax: number, + ay: number, + bx: number, + by: number, + minimum: number, + limitX: number, + limitY: number, + cornerReach = Number.POSITIVE_INFINITY, +): { ax: number; ay: number; bx: number; by: number } | null { + const gap = Math.hypot(bx - ax, by - ay); + if (gap >= minimum) return null; + const ux = gap < 1e-6 ? 1 : (bx - ax) / gap; + const uy = gap < 1e-6 ? 0 : (by - ay) / gap; + const short = minimum - gap; + const roomA = roomAlong(ax, ay, -ux, -uy, limitX, limitY, cornerReach); + const roomB = roomAlong(bx, by, ux, uy, limitX, limitY, cornerReach); + let aBack = Math.min(short / 2, roomA); + const bBack = Math.min(short - aBack, roomB); + aBack = Math.min(short - bBack, roomA); + const clamp = THREE.MathUtils.clamp; + return { + ax: clamp(ax - ux * aBack, -limitX, limitX), + ay: clamp(ay - uy * aBack, -limitY, limitY), + bx: clamp(bx + ux * bBack, -limitX, limitX), + by: clamp(by + uy * bBack, -limitY, limitY), + }; +} + +const cardTops = new WeakMap(); + +/** + * The top of the result card on a screen this size. The caption and the announcement shot ask on every + * frame, and the card cannot change once the result is in, so it is laid out once per result and screen size. + */ +export function resultCardTop( + final: FinalMessage, + width: number, + height: number, + fighters: readonly [FighterSnapshot, FighterSnapshot], + players: Readonly>, + roundStats: Pick, + viewerId: string | null, +): number { + const known = cardTops.get(final); + if (known !== undefined && known.width === width && known.height === height) return known.top; + const punches = fighters.map((fighter) => roundStats.total(fighter.player_id)) as [RoundPunchStats, RoundPunchStats]; + // Every viewer gets a button under the card, a spectator too (Next bout). + const top = resultCardLayout(width, height, resultCard(final, fighters, players, punches), true).y; + cardTops.set(final, { width, height, top }); + return top; +} + +/** A head vertex in world space as the GPU draws it: reshaped by the fighter's look, then posed. */ +function posedHeadVertex(boxer: SkinnedBoxer, vertex: number, out: THREE.Vector3): THREE.Vector3 { + const mesh = boxer.headMesh; + lookShape(out.fromBufferAttribute(mesh.geometry.getAttribute("position"), vertex), boxer.look, out); + return mesh.applyBoneTransform(vertex, out).applyMatrix4(mesh.matrixWorld); +} + +const skinIndexScratch = new THREE.Vector4(); +const skinWeightScratch = new THREE.Vector4(); +const boneScratch = new THREE.Matrix4(); + +/** + * The matrix a skinned vertex is posed by, into `out` (in the mesh's own space): its bones' matrices blended by + * its weights, as the skinning shader blends them for the vertex's normal. + */ +function skinMatrixOf(mesh: THREE.SkinnedMesh, vertex: number, out: THREE.Matrix4): THREE.Matrix4 { + const geometry = mesh.geometry; + skinIndexScratch.fromBufferAttribute(geometry.getAttribute("skinIndex") as THREE.BufferAttribute, vertex); + skinWeightScratch.fromBufferAttribute(geometry.getAttribute("skinWeight") as THREE.BufferAttribute, vertex); + out.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + for (let influence = 0; influence < 4; influence += 1) { + const weight = skinWeightScratch.getComponent(influence); + if (weight === 0) continue; + const bone = skinIndexScratch.getComponent(influence); + boneScratch.multiplyMatrices(mesh.skeleton.bones[bone]!.matrixWorld, mesh.skeleton.boneInverses[bone]!); + for (let element = 0; element < 16; element += 1) out.elements[element]! += boneScratch.elements[element]! * weight; + } + return out.premultiply(mesh.bindMatrixInverse).multiply(mesh.bindMatrix); +} + +/** Across a severed head's cut, in the head bone's frame: mirrored, because the cut faces down it. */ +const SEVERED_HEAD_ACROSS = new THREE.Vector3(-1, 0, 0); + +/** True for the part of the head mesh that leaves with the head. */ +export function aboveNeckCut(bind: THREE.Vector3): boolean { + return bind.y > NECK_CUT_HEIGHT - NECK_CUT_SLOPE * (bind.z - NECK_CUT_DEPTH) - 0.3; +} + const DEFAULT_SIM: SimulationInfo = { tick_rate: 30, ring_half_width: 500, ring_half_height: 500 }; /** * Freezes a skinned mesh's current deformed surface into a static geometry - * expressed relative to `pivot` so it can fly as a rigid severed part. Only the - * vertices its triangles use are kept: each glove of the model indexes its own - * half of a vertex buffer the two gloves share. + * expressed relative to `pivot` so it can fly as a rigid severed part. With `rigid`, every vertex + * follows that bone alone, so skin the part shares with the next bone keeps the part's own shape. A part + * cut out of its mesh by `keep` (a severed head) keeps the mesh's numbering, which its cut's rim uses; a + * whole part keeps only the vertices its triangles use: each glove of the model indexes its own half of a + * vertex buffer the two gloves share. */ -export function bakeSkinnedPart(mesh: THREE.SkinnedMesh, pivotPosition: THREE.Vector3, pivotQuaternion: THREE.Quaternion): BakedPart { +export function bakeSkinnedPart( + mesh: THREE.SkinnedMesh, + pivotPosition: THREE.Vector3, + pivotQuaternion: THREE.Quaternion, + keep?: (bind: THREE.Vector3) => boolean, + look?: FighterLook, + rigid?: THREE.Bone, +): BakedPart { const source = mesh.geometry; const positions = source.getAttribute("position"); const index = source.getIndex(); - // The source vertex behind each kept vertex, and the triangles renumbered to the kept vertices. + // The source vertex behind each baked vertex, and for a whole part its triangles renumbered to them. const kept: number[] = []; - const triangles: number[] = []; - if (index === null) { + const triangles: number[] | null = index !== null && keep === undefined ? [] : null; + if (index === null || triangles === null) { for (let vertex = 0; vertex < positions.count; vertex += 1) kept.push(vertex); } else { const renumbered = new Int32Array(positions.count).fill(-1); @@ -298,18 +723,44 @@ export function bakeSkinnedPart(mesh: THREE.SkinnedMesh, pivotPosition: THREE.Ve const vertex = new THREE.Vector3(); const inverse = pivotQuaternion.clone().invert(); mesh.updateMatrixWorld(true); + const bone = rigid === undefined ? -1 : mesh.skeleton.bones.indexOf(rigid); + const follow = bone < 0 ? null : new THREE.Matrix4().multiplyMatrices(mesh.bindMatrixInverse, new THREE.Matrix4().multiplyMatrices(mesh.skeleton.bones[bone]!.matrixWorld, mesh.skeleton.boneInverses[bone]!)).multiply(mesh.bindMatrix); + // The scan's own normals go along, posed as the skin is: recomputed, every UV seam of its fragmented atlas + // would split the shading into facets, as each side of a seam would be averaged over its own island only. + const normals = source.getAttribute("normal"); + const bakedNormals = normals === undefined ? null : new Float32Array(kept.length * 3); + const normalTurn = new THREE.Matrix3(); + const skinned = new THREE.Matrix4(); + if (follow !== null) normalTurn.getNormalMatrix(skinned.multiplyMatrices(mesh.matrixWorld, follow)); for (const [target, from] of kept.entries()) { vertex.fromBufferAttribute(positions, from); - mesh.applyBoneTransform(from, vertex); + // A head is drawn reshaped by its owner's look; the severed head keeps that shape. + if (look !== undefined) lookShape(vertex, look, vertex); + if (follow !== null) vertex.applyMatrix4(follow); + else mesh.applyBoneTransform(from, vertex); vertex.applyMatrix4(mesh.matrixWorld).sub(pivotPosition).applyQuaternion(inverse); baked[target * 3] = vertex.x; baked[target * 3 + 1] = vertex.y; baked[target * 3 + 2] = vertex.z; + if (normals === undefined || bakedNormals === null) continue; + if (follow === null) normalTurn.getNormalMatrix(skinned.multiplyMatrices(mesh.matrixWorld, skinMatrixOf(mesh, from, skinned))); + vertex.fromBufferAttribute(normals, from).applyMatrix3(normalTurn).applyQuaternion(inverse).normalize(); + bakedNormals[target * 3] = vertex.x; + bakedNormals[target * 3 + 1] = vertex.y; + bakedNormals[target * 3 + 2] = vertex.z; } const geometry = new THREE.BufferGeometry(); geometry.setAttribute("position", new THREE.BufferAttribute(baked, 3)); + // The shading of a baked part reads its bind positions, numbered as the baked vertices are. + if (triangles === null) geometry.setAttribute("bindPosition", positions.clone()); + else { + const bind = new Float32Array(kept.length * 3); + for (const [target, from] of kept.entries()) bind.set([positions.getX(from), positions.getY(from), positions.getZ(from)], target * 3); + geometry.setAttribute("bindPosition", new THREE.BufferAttribute(bind, 3)); + } const uv = source.getAttribute("uv"); - if (uv !== undefined) { + if (uv !== undefined && triangles === null) geometry.setAttribute("uv", uv.clone()); + else if (uv !== undefined) { const uvs = new Float32Array(kept.length * 2); for (const [target, from] of kept.entries()) { uvs[target * 2] = uv.getX(from); @@ -317,61 +768,56 @@ export function bakeSkinnedPart(mesh: THREE.SkinnedMesh, pivotPosition: THREE.Ve } geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2)); } - if (index !== null) geometry.setIndex(triangles); - geometry.computeVertexNormals(); + if (index !== null && keep !== undefined) { + const held = new Uint8Array(positions.count); + for (let at = 0; at < positions.count; at += 1) held[at] = keep(vertex.fromBufferAttribute(positions, at)) ? 1 : 0; + const whole: number[] = []; + for (let at = 0; at < index.count; at += 3) { + const a = index.getX(at), b = index.getX(at + 1), c = index.getX(at + 2); + if (held[a] === 1 && held[b] === 1 && held[c] === 1) whole.push(a, b, c); + } + geometry.setIndex(whole); + } else if (triangles !== null) geometry.setIndex(triangles); + if (bakedNormals !== null) geometry.setAttribute("normal", new THREE.BufferAttribute(bakedNormals, 3)); + else geometry.computeVertexNormals(); const material = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material; const map = material instanceof THREE.MeshStandardMaterial ? material.map : null; const color = material instanceof THREE.MeshStandardMaterial ? material.color.getHex() : 0xffffff; - return { geometry, map, color }; -} - -function cornerShirtTexture(color: string): THREE.CanvasTexture { - const canvas = document.createElement("canvas"); - canvas.width = 256; - canvas.height = 256; - const ctx = canvas.getContext("2d"); - if (ctx !== null) { - ctx.fillStyle = color; - ctx.fillRect(0, 0, 256, 256); - ctx.fillStyle = "rgba(0,0,0,0.35)"; - ctx.fillRect(0, 0, 256, 16); - ctx.fillStyle = "rgba(255,255,255,0.12)"; - ctx.fillRect(96, 60, 64, 22); - } - const texture = new THREE.CanvasTexture(canvas); - texture.colorSpace = THREE.SRGBColorSpace; - texture.flipY = false; - return texture; + const rim = keep === undefined ? [] : keep === aboveNeckCut ? neckRim(source) : cutRim(source, keep); + if (rim.length === 0) return { geometry, map, color }; + // The skin of the neck follows the neck as well as the head, so the cut is measured on the part as it was posed. + const edge = new Float32Array(rim.length * 3); + const position = new THREE.Vector3(); + for (const [at, corner] of rim.entries()) { + edge.set(baked.subarray(corner * 3, corner * 3 + 3), at * 3); + position.add(vertex.fromArray(baked, corner * 3)); + } + position.multiplyScalar(1 / rim.length); + const middle = new THREE.Vector3(); + const used = geometry.getIndex()!; + for (let at = 0; at < used.count; at += 1) middle.add(vertex.fromArray(baked, used.getX(at) * 3)); + middle.multiplyScalar(1 / Math.max(1, used.count)); + const flesh = new THREE.BufferGeometry(); + // A severed head's cut faces down its own frame, so the head's sideways axis runs the other way across it + // for the windpipe to sit under the chin, as on the neck it came off. + closeCut(flesh, edge, position, position.clone().sub(middle).normalize(), 0.01, keep === aboveNeckCut ? SEVERED_HEAD_ACROSS : undefined); + return { geometry, map, color, cut: { position, flesh } }; } -function refereeShirtTexture(): THREE.CanvasTexture { - const canvas = document.createElement("canvas"); - canvas.width = 256; - canvas.height = 256; - const ctx = canvas.getContext("2d"); - if (ctx !== null) { - ctx.fillStyle = "#9fb4d8"; - ctx.fillRect(0, 0, 256, 256); - ctx.fillStyle = "rgba(255,255,255,0.08)"; - for (let x = 0; x < 256; x += 8) ctx.fillRect(x, 0, 2, 256); - ctx.fillStyle = "#1b2230"; - ctx.fillRect(0, 0, 256, 18); - } - const texture = new THREE.CanvasTexture(canvas); - texture.colorSpace = THREE.SRGBColorSpace; - texture.flipY = false; - return texture; +/** A severed head as it leaves the neck: the head bone's own shape of it, in its owner's look and with his injuries, measured from the given pivot. */ +export function bakeSeveredHead(boxer: SkinnedBoxer, pivotPosition: THREE.Vector3, pivotQuaternion: THREE.Quaternion): BakedPart { + return { ...bakeSkinnedPart(boxer.headMesh, pivotPosition, pivotQuaternion, aboveNeckCut, boxer.look, boxer.bone("head") ?? undefined), look: boxer.look, injury: boxer.headInjury }; } function blankFighter(playerId: string): FighterSnapshot { return { player_id: playerId, x: 0, y: 0, facing: 1, facing_x: 1000, facing_y: 0, velocity_x: 0, velocity_y: 0, - stance: "orthodox", defense: "guard_high", stamina: 1000, maximum_stamina: 1000, conditioning: 1000, guard: 700, poise: 600, + stance: "orthodox", style: "balanced", defense: "guard_high", stamina: 1000, maximum_stamina: 1000, conditioning: 1000, guard: 700, poise: 600, trauma: { head: 0, body: 0, left_eye: 0, right_eye: 0, left_cut: 0, right_cut: 0, swelling: 0, bleeding: 0 }, knockdowns: 0, warnings: 0, deductions: 0, stunned_ticks: 0, is_downed: false, action: null, action_hand: null, action_target: null, action_power: null, action_id: null, action_key: null, action_start_tick: 0, action_startup_ticks: 0, action_active_ticks: 0, action_recovery_ticks: 0, action_contact_tick: null, - queued_actions: 0, clinch_startup_ticks: 0, clinch_ticks: 0, is_foul_recovery_target: false, taunt_ticks: 0, + queued_actions: 0, clinch_startup_ticks: 0, clinch_ticks: 0, is_foul_recovery_target: false, taunt_ticks: 0, corner_choice: null, get_up_prompt: null, get_up_meter: 0, get_up_required: 0, get_up_count: 0, get_up_window_start_tick: 0, get_up_window_end_tick: 0, last_input_sequence: -1, }; @@ -429,9 +875,108 @@ export function canStartPunch(fighter: FighterSnapshot): boolean { && !fighter.is_foul_recovery_target; } +/** The finisher the punch that floored a fighter earns him if he does not beat the count. */ +export function knockdownFinisher(hit: CombatEvent | null, snapshot: EngineSnapshot): ArcadeInjury | null { + if (hit === null) return null; + const target = snapshot.fighters.find((fighter) => fighter.player_id === hit.target_id); + const puncher = snapshot.fighters.find((fighter) => fighter.player_id === hit.actor_id); + return arcadeInjuryFor(hit, target === undefined ? undefined : { ...target, is_downed: true }, { finish_method: "ko", winner_id: hit.actor_id }, puncher); +} + +const POOL_SEVERITY = 0.2; +const POOL_RATE = 2; +const POOL_REACH = 0.42; +/** + * A finisher's open wound bleeds out whatever the engine says of his cuts: as badly as the worst cut does + * (the pool's ceiling), three times as fast, and spreading out to 0.8 m from it, which takes about 18 s. + */ +const OPEN_WOUNDS: ReadonlySet = new Set(["decapitation", "head_burst", "eye_left", "eye_right", "dismember_left", "dismember_right", "ribs_left", "ribs_right"]); +const OPEN_WOUND_SEVERITY = 1.6; +const OPEN_WOUND_POOL_RATE = 3; +const OPEN_WOUND_POOL_REACH = 0.8; +/** Under the chin, from the middle of the skull, in the head's frame. */ +const UNDER_THE_CHIN = new THREE.Vector3(0, -0.1, 0.08); + +/** + * How far the pool under a downed fighter has spread after `count` spills, in metres: fast at first, then slower, + * wider the worse he bleeds, up to `reach`. + */ +export function poolRadius(count: number, severity: number, reach = POOL_REACH): number { + return Math.min(reach, 0.05 + 0.045 * Math.sqrt(Math.max(0, count)) * Math.min(1.6, Math.max(0, severity))); +} + +/** + * Whether a fighter is down for his blood and his shadow: counted on the canvas, falling or lying there as + * drawn, or the loser of a flash knockout, whom the engine never puts down (the bout ends with the punch). + */ +export function fighterDown(fighter: FighterSnapshot, graph: { readonly isDown: boolean } | undefined, flashLoserId: string | undefined): boolean { + return fighter.is_downed || graph?.isDown === true || (flashLoserId !== undefined && fighter.player_id === flashLoserId); +} + +/** How deep (bind-space centimetres) and how wide a torso finisher caves the ribs in: twice the deepest dent a punch leaves. */ +const RIBS_DENT = 6; +const RIBS_DENT_RADIUS = 11; +/** The burst of blood from caved-in ribs is thrown at rib height, while the chest is still this high off the canvas. */ +const RIBS_BURST_CHEST_HEIGHT = 0.9; +const RIBS_BURST_EVENT_OFFSET = 7; + +/** + * Ribs a torso finisher caved in on the fighter's `side`: a deep dent, black with bruising and running + * with blood. Set every frame over what the engine's trauma paints, which would let the dent spring back. + */ +export function caveInRibs(shading: InjuryShading, side: "left" | "right"): void { + const site = side === "left" ? "leftRibs" : "rightRibs"; + shading.impact(site, [side === "left" ? -RIBS_DENT : RIBS_DENT, -RIBS_DENT * 0.15, -RIBS_DENT * 0.25], RIBS_DENT_RADIUS); + shading.set(site, { bruise: 1.2, swell: 0, blood: 1.4 }); +} + /** Whether the knockout replay puts this injury back so it can happen again on screen. */ export function replayReattaches(injury: ArcadeInjury): boolean { - return injury === "decapitation" || injury === "dismember_left" || injury === "dismember_right"; + return injury !== "jaw_dislocation" && injury !== "shoulder_left" && injury !== "shoulder_right"; +} + +const eyeVertices = new WeakMap(); + +/** The vertex of a head mesh nearest the middle of each eye: left, then right. */ +function eyeVertex(geometry: THREE.BufferGeometry, side: "left" | "right"): number { + let found = eyeVertices.get(geometry); + if (found === undefined) { + const position = geometry.getAttribute("position"); + const vertex = new THREE.Vector3(); + const target = new THREE.Vector3(); + const nearest = EYE_LIDS.map((lid) => { + let best = 0; + let bestDistance = Infinity; + target.set(lid[0], lid[1], lid[2]); + for (let index = 0; index < position.count; index += 1) { + const distance = vertex.fromBufferAttribute(position, index).distanceToSquared(target); + if (distance < bestDistance) { + bestDistance = distance; + best = index; + } + } + return best; + }); + found = [nearest[0]!, nearest[1]!]; + eyeVertices.set(geometry, found); + } + return found[side === "left" ? 0 : 1]; +} + +/** + * Where an eye sits in its socket, on the skin as it is posed, and the way the face points. False when + * the fighter has no head bone. + */ +export function eyeSocket(boxer: SkinnedBoxer, side: "left" | "right", position: THREE.Vector3, forward: THREE.Vector3, turn: THREE.Quaternion): boolean { + const head = boxer.bone("head"); + if (head === null) return false; + boxer.root.updateMatrixWorld(true); + const mesh = boxer.headMesh; + posedHeadVertex(boxer, eyeVertex(mesh.geometry, side), position); + head.getWorldQuaternion(turn); + forward.set(0, 0, 1).applyQuaternion(turn); + position.addScaledVector(forward, -0.004); + return true; } /** How far the referee stands from the action and how close he may get to a fighter: tight over a count, in close for a clinch, between the two to break it, otherwise out of the way. */ @@ -448,12 +993,12 @@ function refereeSnapshot(position: THREE.Vector3, yaw: number, velocity: THREE.V const base: FighterSnapshot = { player_id: "referee", x: simX, y: simY, facing: 1, facing_x: Math.round(Math.sin(yaw) * 1000), facing_y: Math.round(-Math.cos(yaw) * 1000), velocity_x: velocity.x / mapping.x(1) / 30, velocity_y: velocity.z / mapping.z(1) / 30, - stance: "orthodox", defense: "none", stamina: 1000, maximum_stamina: 1000, conditioning: 1000, guard: 700, poise: 600, + stance: "orthodox", style: "balanced", defense: "none", stamina: 1000, maximum_stamina: 1000, conditioning: 1000, guard: 700, poise: 600, trauma: { head: 0, body: 0, left_eye: 0, right_eye: 0, left_cut: 0, right_cut: 0, swelling: 0, bleeding: 0 }, knockdowns: 0, warnings: 0, deductions: 0, stunned_ticks: 0, is_downed: false, action: null, action_hand: null, action_target: null, action_power: null, action_id: null, action_key: null, action_start_tick: 0, action_startup_ticks: 0, action_active_ticks: 0, action_recovery_ticks: 0, action_contact_tick: null, - queued_actions: 0, clinch_startup_ticks: 0, clinch_ticks: 0, is_foul_recovery_target: false, taunt_ticks: 0, + queued_actions: 0, clinch_startup_ticks: 0, clinch_ticks: 0, is_foul_recovery_target: false, taunt_ticks: 0, corner_choice: null, get_up_prompt: null, get_up_meter: 0, get_up_required: 0, get_up_count: 0, get_up_window_start_tick: 0, get_up_window_end_tick: 0, last_input_sequence: -1, }; @@ -472,7 +1017,6 @@ export class FightRenderer { private cornermen: [BoxingGraph, BoxingGraph] | null = null; private trails: GloveTrail[] = []; private readonly trailGlove = new THREE.Vector3(); - private readonly cornerShirts: THREE.CanvasTexture[] = []; private readonly cornermanPosition = new THREE.Vector3(); private readonly cornermanVelocity = new THREE.Vector3(); private cutmen: [BoxingGraph, BoxingGraph] | null = null; @@ -486,6 +1030,10 @@ export class FightRenderer { private readonly refereeVelocity = new THREE.Vector3(); private readonly effects: Effects3D; private readonly director = new CameraDirector(); + private readonly fighterCam = new FighterCam(); + private ownViewActive = false; + private readonly ownForward = { x: 0, z: -1 }; + private baseFov = 36; private readonly mapping: WorldMapping; private readonly buffer: SnapshotBuffer; private readonly localInput: (() => HeldInput | null) | null; @@ -514,26 +1062,44 @@ export class FightRenderer { private players: Readonly> = {}; private playerOrder: readonly string[] = []; private viewerId: string | null = null; + /** Until this tick the player's own fighter is folding over a body shot and cannot move. */ + private ownCollapseUntil = 0; private final: FinalMessage | null = null; private reconnectMs = 0; private destroyed = false; private graphs: [BoxingGraph, BoxingGraph] | null = null; private glbLoading = false; - private refereeShirt: THREE.CanvasTexture | null = null; private graphsReady: Promise = Promise.resolve(); private readonly headCache = [new THREE.Vector3(), new THREE.Vector3()]; private readonly headCacheValid = [false, false]; + /** The tick on screen when each fighter's knockout physics last began a fall, outside the replay; null before any. */ + private readonly liveFallTicks: [number | null, number | null] = [null, null]; + private readonly fallingLive = [false, false]; private readonly arcadeInjuries: [ArcadeInjury | null, ArcadeInjury | null] = [null, null]; private readonly observedInjuryDown: [boolean, boolean] = [false, false]; private readonly arcadeInjuryEvents: [CombatEvent | null, CombatEvent | null] = [null, null]; private readonly replayInjuries: [{ injury: ArcadeInjury; event: CombatEvent } | null, { injury: ArcadeInjury; event: CombatEvent } | null] = [null, null]; private readonly closeUpTarget = new THREE.Vector3(); + private readonly closeUpFacing = new THREE.Vector3(); + private readonly drawnFighters: [FighterSnapshot, FighterSnapshot] = [blankFighter("a"), blankFighter("b")]; + /** How far each fighter is drawn from his engine place, in engine units, eased; and where it is heading. */ + private readonly drawnOffsets: [{ x: number; y: number }, { x: number; y: number }] = [{ x: 0, y: 0 }, { x: 0, y: 0 }]; + private readonly drawnTargets: [{ x: number; y: number }, { x: number; y: number }] = [{ x: 0, y: 0 }, { x: 0, y: 0 }]; + /** False when the next frame takes the offsets as they are due, without easing: the first, and at a replay's cuts. */ + private drawnOffsetsLive = false; + private readonly lookIds: [string | null, string | null] = [null, null]; private readonly downedPoolAccumulators: [number, number] = [0, 0]; private readonly downedPoolCounts: [number, number] = [0, 0]; private bloodLevel: BloodLevel = "full"; private viewerHitFlash = 0; private finishSlowMotion = 0; private finishSeen = false; + private ceremony: Ceremony | null = null; + private readonly hudViewport = { width: 1280, height: 720 }; + private readonly ceremonyWrists = [new THREE.Vector3(), new THREE.Vector3()] as const; + private stumpRim = new Float32Array(0); + private readonly burstStump = { position: new THREE.Vector3(), quaternion: new THREE.Quaternion(), across: new THREE.Vector3(), scratch: new THREE.Vector3() }; + private ovationUntil = 0; private finalRevealAt = 0; private pendingFinish: { readonly final: FinalMessage; readonly tick: number; readonly latestAt: number } | null = null; private portraitPull = 1; @@ -546,17 +1112,38 @@ export class FightRenderer { private readonly cornerLookAt = new THREE.Vector3(); private finishCloseUpUntil = 0; private finishCloseUpIndex = -1; + private finishCloseUpBearing: number | null = null; + /** When the close-up was asked for, and whether the eye it shoots hangs in sight (decided with its side). */ + private finishCloseUpAt = 0; + private finishCloseUpHanging = false; + /** The seat whose arm the referee lifts after a stoppage, from `stoppageRaiseAt`; -1 for none. */ + private stoppageWinner = -1; + private stoppageRaiseAt = Number.POSITIVE_INFINITY; + /** Where the referee stands to lift the winner's arm, from the winner; chosen as he sets off for it. */ + private stoppageSpot: { readonly x: number; readonly z: number } | null = null; + /** The middle of the winner and the referee the arm-raise shot holds, eased, once it has started. */ + private readonly raiseCentre = new THREE.Vector3(); + private raiseFramed = false; + private readonly stoppageWrist = new THREE.Vector3(); + private readonly stoppageOtherWrist = new THREE.Vector3(); private readonly closeUpPosition = new THREE.Vector3(); private roundCalloutUntil = 0; private roundCalloutRound = 0; + private eventCallout: { text: string; until: number } | null = null; private readonly tmpCamera = new THREE.Vector3(); private readonly roundStats = new RoundStatsTracker(); private readonly history: EngineSnapshot[] = []; - private lastKnockdown: { readonly knockdown: CombatEvent; readonly hit: CombatEvent | null } | null = null; + private lastKnockdown: { readonly knockdown: CombatEvent; readonly hit: CombatEvent | null; readonly finisher: ArcadeInjury | null } | null = null; + /** A flash knockout ends the bout without a knockdown: the punch that did it and the fighter it put down. */ + private flashKnockout: { readonly hitEventId: number; readonly loserId: string } | null = null; + private readonly bodyPoint = new THREE.Vector3(); private replay: { readonly plan: ReplayPlan; readonly buffer: SnapshotBuffer; readonly startedAt: number; impactFired: boolean; side: 1 | -1 | null } | null = null; private readonly finishPass: ShaderPass; private readonly bloomPass: UnrealBloomPass; + private readonly avatars = new Avatars(); private inputLatencyMs: number | null = null; + /** The slow-connection warning is up (lagWarning). */ + private lagWarned = false; private frameMsAverage = 16.7; private readonly replayCameraPosition = new THREE.Vector3(); private readonly replayLookAt = new THREE.Vector3(); @@ -573,13 +1160,29 @@ export class FightRenderer { }> = []; onContact: ((event: CombatEvent) => void) | null = null; onArcadeInjury: ((injury: ArcadeInjury, event: CombatEvent) => void) | null = null; + /** The ring announcer's lines for a voice to read. */ + onAnnouncement: ((lines: readonly string[]) => void) | null = null; + onCrowdCue: ((cue: CrowdCue) => void) | null = null; + /** Every frame, how rocked the viewer's own fighter is on screen (0 to 1) and the newest server tick; never for a spectator. */ + onRocked: ((level: number, tick: number) => void) | null = null; + private readonly commentary = new CommentaryDirector({ speak: (lines) => this.onAnnouncement?.(lines), cue: (cue) => this.onCrowdCue?.(cue) }); + private readonly touchControls = coarsePointer(); + private resultAnnounced = false; + private captionText = ""; + private readonly rocked = new RockedVision(); + /** The two fighters' places on the canvas for this frame: written by the frame setup only, read by the cameras, the follow spot and the officials. */ private readonly tmpA = new THREE.Vector3(); private readonly tmpB = new THREE.Vector3(); + /** Where a contact's effects go, and the mouth teeth and a gum shield fly from; kept apart from the fighters' places above. */ + private readonly contactPoint = new THREE.Vector3(); + private cornerPanelTop: number | null = null; + private readonly mouthPoint = new THREE.Vector3(); private readonly tmpHead = new THREE.Vector3(); private readonly tmpHeadQuaternion = new THREE.Quaternion(); private readonly tmpStump = new THREE.Vector3(); private readonly tmpStumpOffset = new THREE.Vector3(); private readonly tmpStumpQuaternion = new THREE.Quaternion(); + private readonly tmpStumpAcross = new THREE.Vector3(); private readonly tmpPart = new THREE.Vector3(); private readonly tmpPartQuaternion = new THREE.Quaternion(); @@ -612,7 +1215,8 @@ export class FightRenderer { this.scene.add(this.ring.group); this.arena = buildArena(); this.scene.add(this.arena.group); - this.effects = new Effects3D(this.scene); + this.effects = new Effects3D(this.scene, coarsePointer() ? 1024 : 2048); + this.effects.useUploader(this.renderer); this.blobTexture = blobShadowTexture(); const blobGeometry = new THREE.PlaneGeometry(1, 1); @@ -654,21 +1258,16 @@ export class FightRenderer { const first = new SkinnedBoxer(gltf, { skin: 0xb0703f, gear: 0x1d4ed8 }); const second = new SkinnedBoxer(gltf, { skin: 0x6e4128, gear: 0xb91c1c }); this.graphs = [new BoxingGraph(first, this.mapping), new BoxingGraph(second, this.mapping)]; + this.lookIds.fill(null); this.syncInjuryPresentation(0); this.syncInjuryPresentation(1); - this.refereeShirt = refereeShirtTexture(); - const official = new SkinnedBoxer(gltf, { skin: 0xc79b76, gear: 0x3b57b8, pants: 0x14161c, bodyMap: this.refereeShirt }); + const official = new SkinnedBoxer(gltf, { skin: 0xc79b76, gear: 0x3b57b8, look: OFFICIAL_LOOKS.referee, outfit: REFEREE_OUTFIT }); this.referee = new BoxingGraph(official, this.mapping, { referee: true }); - const blueShirt = cornerShirtTexture("#2b4c9e"); - const redShirt = cornerShirtTexture("#9e2b2b"); - this.cornerShirts.push(blueShirt, redShirt); - const blueCorner = new SkinnedBoxer(gltf, { skin: 0x8a5a3b, gear: 0x1b2230, pants: 0x14161c, bodyMap: blueShirt }); - const redCorner = new SkinnedBoxer(gltf, { skin: 0xd9a77c, gear: 0x1b2230, pants: 0x14161c, bodyMap: redShirt }); + const blueCorner = new SkinnedBoxer(gltf, { skin: 0x8a5a3b, gear: 0x1b2230, look: OFFICIAL_LOOKS.blueCorner, outfit: BLUE_CORNER_OUTFIT }); + const redCorner = new SkinnedBoxer(gltf, { skin: 0xd9a77c, gear: 0x1b2230, look: OFFICIAL_LOOKS.redCorner, outfit: RED_CORNER_OUTFIT }); this.cornermen = [new BoxingGraph(blueCorner, this.mapping, { referee: true }), new BoxingGraph(redCorner, this.mapping, { referee: true })]; - const whiteShirt = cornerShirtTexture("#e3e4e8"); - this.cornerShirts.push(whiteShirt); - const blueCutman = new SkinnedBoxer(gltf, { skin: 0xb98c66, gear: 0x1b2230, pants: 0x14161c, bodyMap: whiteShirt }); - const redCutman = new SkinnedBoxer(gltf, { skin: 0x5a3a26, gear: 0x1b2230, pants: 0x14161c, bodyMap: whiteShirt }); + const blueCutman = new SkinnedBoxer(gltf, { skin: 0xb98c66, gear: 0x1b2230, look: OFFICIAL_LOOKS.blueCutman, outfit: CUTMAN_OUTFIT }); + const redCutman = new SkinnedBoxer(gltf, { skin: 0x5a3a26, gear: 0x1b2230, look: OFFICIAL_LOOKS.redCutman, outfit: CUTMAN_OUTFIT }); this.cutmen = [new BoxingGraph(blueCutman, this.mapping, { referee: true }), new BoxingGraph(redCutman, this.mapping, { referee: true })]; blueCutman.root.visible = false; redCutman.root.visible = false; @@ -678,6 +1277,10 @@ export class FightRenderer { // neither the first frame with fighters nor the first bloody hit stalls the page. const staging = new THREE.Group(); staging.add(first.root, second.root, official.root, blueCorner.root, redCorner.root, blueCutman.root, redCutman.root, ...this.trails.map((trail) => trail.mesh), ...this.effects.compileStandIns()); + // A finisher opens a head, which a shader of its own draws, and another its shadow: the first + // fighter's head is opened for the warm-up and closed again before it is drawn. + const opened = first.headInjury.open; + first.headInjury.setOpen(true); try { await compileForComposer(this.renderer, this.composer, staging, this.camera, this.scene); } catch { @@ -686,6 +1289,8 @@ export class FightRenderer { if (this.destroyed) return; this.scene.add(first.root, second.root, official.root, blueCorner.root, redCorner.root, blueCutman.root, redCutman.root); for (const trail of this.trails) this.scene.add(trail.mesh); + this.compileShadowsOf(staging); + first.headInjury.setOpen(opened); }) .catch((error: unknown) => { this.glbLoading = false; @@ -693,6 +1298,35 @@ export class FightRenderer { }); } + /** + * The key light's shadow pass draws every caster with one depth material, which compiling the scene's + * materials does not build, and which only picks its program again when the kind of mesh it draws changes + * (skinned, instanced or neither): the first severed head or glove drawn after a fighter would compile its + * variant (its side, its texture) there and then. Each stand-in is drawn into the shadow once here, asking + * for its own variant as it is drawn, and into the composer's target so the screen never shows them. + */ + private compileShadowsOf(standIns: THREE.Group): void { + const key = this.keyLight; + if (key === null || standIns.children.length === 0) return; + for (const standIn of standIns.children) { + standIn.frustumCulled = false; + standIn.onBeforeShadow = (_renderer, _scene, _camera, _shadowCamera, _geometry, depthMaterial) => { + depthMaterial.needsUpdate = true; + }; + } + this.scene.add(standIns); + key.shadow.needsUpdate = true; + this.renderer.setRenderTarget(this.composer.readBuffer); + try { + this.renderer.render(this.scene, this.camera); + } catch { + // Compiled on first use instead. + } finally { + this.renderer.setRenderTarget(null); + this.scene.remove(standIns); + } + } + get ready(): Promise { return this.graphsReady; } @@ -719,6 +1353,19 @@ export class FightRenderer { this.draw(virtualSeconds * 1000, true, render); } + /** Where the fighter's corner panel starts down the screen while it is up, so the captions keep above it. */ + setCornerPanelTop(top: number | null): void { + this.cornerPanelTop = top; + } + + /** The way the player's own camera faces along the canvas while it is in use, so the controls can turn with it. */ + viewForward(): { readonly x: number; readonly z: number } | null { + if (!this.ownViewActive) return null; + this.ownForward.x = this.fighterCam.forwardX; + this.ownForward.z = this.fighterCam.forwardZ; + return this.ownForward; + } + setPlayers(players: Readonly>, viewerId: string | null, order: readonly string[] = Object.keys(players)): void { this.players = players; this.viewerId = viewerId; @@ -736,6 +1383,7 @@ export class FightRenderer { const present = alone ? id === this.viewerId || (this.viewerId === null && index === 0) : true; graph.boxer.root.visible = present; if (!present) continue; + this.wearLook(index === 0 ? 0 : 1, id); const sign = index === 0 ? -1 : 1; const idleTick = Math.floor(time * this.simulation.tick_rate); const self = { ...blankFighter(id), x: alone ? 0 : sign * 180, y: alone ? -70 : 0, facing: alone ? 1 : -sign, facing_x: alone ? 0 : -sign * 1000, facing_y: alone ? -1000 : 0, ...(alone ? shadowBoxing(idleTick) : {}) }; @@ -747,15 +1395,34 @@ export class FightRenderer { /** A stoppage's result panel waits for the slow-motion fall; decisions show at once. */ setFinal(final: FinalMessage | null): void { + // The result already being shown, again, would start its walk to the marks or its replay over. + if (final !== null && final.match_id === this.final?.match_id) return; this.final = final; - this.finalRevealAt = this.frameSeconds + finalRevealDelay(final); + this.stoppageWinner = -1; + this.stoppageRaiseAt = Number.POSITIVE_INFINITY; + this.stoppageSpot = null; + this.raiseFramed = false; + this.ceremony = this.ceremonyFor(final); + this.finalRevealAt = this.frameSeconds + (this.ceremony === null ? finalRevealDelay(final) : CEREMONY_REVEAL_LIMIT_SECONDS); + this.ovationUntil = final === null ? 0 : this.frameSeconds + CROWD_OVATION_SECONDS; + if (final !== null) this.commentary.finish(final, this.players, this.ceremony !== null, this.frameSeconds); + if (this.ceremony === null) this.endCeremony(); this.pendingFinish = null; if (final === null || !STOPPAGE_METHODS.has(final.method)) return; - const plan = this.lastKnockdown === null ? null : planKnockoutReplay(this.history, this.lastKnockdown.knockdown, this.simulation.tick_rate); + const plan = this.lastKnockdown === null ? null : planKnockoutReplay(this.history, this.lastKnockdown.hit ?? this.lastKnockdown.knockdown, this.simulation.tick_rate); + const finisher = this.finishingInjury(final); if (plan !== null && this.graphs !== null && !this.settings().reducedMotion) { + // The blow that floored him for the count does its damage at the impact of the replay. + if (finisher !== null) this.replayInjuries[finisher.index] = { injury: finisher.injury, event: finisher.event }; this.startReplay(plan); return; } + if (finisher !== null) { + // A finisher whose punch the clock has not shown yet goes with that punch, never ahead of it. + const waiting = this.pendingContacts.find((contact) => contact.event.event_id === finisher.event.event_id); + if (waiting !== undefined) waiting.injury ??= finisher.injury; + else this.applyArcadeInjury(finisher.index, finisher.injury, finisher.event); + } // The result arrives while the screen is still a few ticks behind it, so the finish waits for the // punch that ended the bout to be shown. const resultTick = this.buffer.latest()?.result?.tick; @@ -771,6 +1438,128 @@ export class FightRenderer { this.presentFinish(pending.final); } + /** The finisher earned by a knockout whose blow was not yet known to end the bout when it landed. */ + private finishingInjury(final: FinalMessage): { index: number; injury: ArcadeInjury; event: CombatEvent } | null { + const record = this.lastKnockdown; + const settings = this.settings(); + if (record === null || record.hit === null || record.finisher === null || settings.blood !== "full" || settings.reducedMotion) return null; + if ((final.method !== "ko" && final.method !== "tko") || final.winner_id !== record.hit.actor_id) return null; + const index = this.buffer.latest()?.fighters.findIndex((fighter) => fighter.player_id === record.hit!.target_id) ?? -1; + if (index < 0 || this.arcadeInjuries[index] !== null) return null; + return { index, injury: record.finisher, event: record.hit }; + } + + /** A bout that went to the cards ends with both fighters beside the referee for the decision. */ + private ceremonyFor(final: FinalMessage | null): Ceremony | null { + const fighters = this.buffer.latest()?.fighters; + if (final === null || fighters === undefined || (final.method !== "decision" && final.method !== "draw")) return null; + if (fighters.some((fighter) => fighter.is_downed)) return null; + const seat = fighters.findIndex((fighter) => fighter.player_id === final.winner_id); + return { winnerSeat: seat === 0 || seat === 1 ? seat : null, positions: null, refereeArrived: false, arrivedAt: null, announced: false }; + } + + private endCeremony(): void { + for (const graph of this.graphs ?? []) graph.awaitVerdict(null); + this.referee?.raise(null, null); + } + + /** + * The fighters as drawn during the decision: walking to their marks, then standing square to the camera. + * The walk starts where they were drawn at the bell, `drawnFrom` their engine places. + */ + private ceremonyFighters(ceremony: Ceremony, fighters: readonly [FighterSnapshot, FighterSnapshot], dt: number, seconds: number, drawnFrom: readonly [{ readonly x: number; readonly y: number }, { readonly x: number; readonly y: number }] | null = null): readonly [FighterSnapshot, FighterSnapshot] { + if (ceremony.positions === null) { + ceremony.positions = [ + { x: fighters[0].x + (drawnFrom?.[0].x ?? 0), y: fighters[0].y + (drawnFrom?.[0].y ?? 0) }, + { x: fighters[1].x + (drawnFrom?.[1].x ?? 0), y: fighters[1].y + (drawnFrom?.[1].y ?? 0) }, + ]; + ceremony.marks = ceremony.positions[0].x <= ceremony.positions[1].x ? [0, 1] : [1, 0]; + } + const marks = ceremony.marks ?? SEAT_MARKS; + let arrived = ceremony.refereeArrived; + for (const seat of [0, 1] as const) { + const from = ceremony.positions[seat]; + const mark = marks[seat]; + const step = ceremonyStep(from.x, from.y, CEREMONY_MARKS[mark], CEREMONY_WALK_SPEED * dt); + const moved = Math.hypot(step.x - from.x, step.y - from.y); + const perTick = dt > 0 ? 1 / (dt * this.simulation.tick_rate) : 0; + const walking = !step.arrived && moved > 1e-6; + Object.assign(this.drawnFighters[seat], fighters[seat], { + x: step.x, + y: step.y, + velocity_x: (step.x - from.x) * perTick, + velocity_y: (step.y - from.y) * perTick, + facing_x: walking ? Math.round(((step.x - from.x) / moved) * 1000) : 0, + facing_y: walking ? Math.round(((step.y - from.y) / moved) * 1000) : -1000, + facing: walking && step.x < from.x ? -1 : 1, + action: null, action_id: null, action_key: null, queued_actions: 0, + defense: "none", stunned_ticks: 0, taunt_ticks: 0, clinch_ticks: 0, clinch_startup_ticks: 0, + } satisfies Partial); + from.x = step.x; + from.y = step.y; + arrived &&= step.arrived; + this.graphs?.[seat]?.awaitVerdict(mark === 0 ? 1 : -1); + } + if (arrived) ceremony.arrivedAt ??= seconds; + if (!ceremony.announced && ceremony.arrivedAt !== null && seconds - ceremony.arrivedAt >= CEREMONY_PAUSE_SECONDS) { + ceremony.announced = true; + this.commentary.verdict(this.frameSeconds); + for (const seat of [0, 1] as const) this.graphs?.[seat]?.announce(ceremony.winnerSeat === null ? "level" : ceremony.winnerSeat === seat ? "winner" : "loser"); + this.finalRevealAt = Math.min(this.finalRevealAt, this.frameSeconds + 0.35); + this.arena.excite(1); + } + if (ceremony.announced) { + // The referee holds the wrist where the last frame left it; the fighter on the left mark stands on the referee's right. + const wrist = (mark: 0 | 1): THREE.Vector3 | null => { + const seat = marks[0] === mark ? 0 : 1; + const raised = ceremony.winnerSeat === null || ceremony.winnerSeat === seat; + const bone = this.graphs?.[seat]?.boxer.rig.bones[mark === 0 ? "gloveL" : "gloveR"]; + return raised && bone !== undefined ? bone.getWorldPosition(this.ceremonyWrists[mark]) : null; + }; + this.referee?.raise(wrist(1), wrist(0)); + } + return this.drawnFighters; + } + + /** The announcement is shot from the front, in the part of the screen the result card leaves free. */ + private ceremonyFrame(seconds: number): { position: THREE.Vector3; lookAt: THREE.Vector3; framed: boolean } | null { + const ceremony = this.ceremony; + const latest = this.buffer.latest(); + if (ceremony === null || ceremony.arrivedAt === null || this.replay !== null || this.final === null || latest === null) return null; + const { width, height } = this.hudViewport; + const top = resultCardTop(this.final, width, height, latest.fighters, this.players, this.roundStats, this.viewerId); + const shot = ceremonyShot(this.camera.aspect, this.camera.fov, (height - top) / Math.max(1, height)); + const drift = this.settings().reducedMotion ? 0 : Math.sin((seconds - ceremony.arrivedAt) * 0.35) * 0.06 * shot.distance; + this.cornerPosition.set(drift, shot.height + 0.05 * shot.distance, shot.distance); + this.cornerLookAt.set(0, shot.height, 0); + return { position: this.cornerPosition, lookAt: this.cornerLookAt, framed: true }; + } + + /** + * After a stoppage, once the close-up of the beaten fighter is done, the broadcast holds the winner and + * the referee who lifts his arm, shot from the front in the part of the screen the result card leaves + * free, as the decision is. The count's framing, low over the body, put the raised glove off the top. + */ + private raiseFrame(seconds: number, dt: number, reducedMotion: boolean): { position: THREE.Vector3; lookAt: THREE.Vector3; framed: boolean } | null { + const latest = this.buffer.latest(); + if (this.stoppageWinner < 0 || this.final === null || this.replay !== null || this.ceremony !== null || latest === null || seconds < this.finishCloseUpUntil) return null; + const winner = this.stoppageWinner === 0 ? this.tmpA : this.tmpB; + // The referee's spot is chosen as he sets off; until then the one he would take first. + const spotX = this.stoppageSpot?.x ?? (winner.x > 0 ? -1 : 1) * STOPPAGE_RAISE_SPACING; + const spotZ = this.stoppageSpot?.z ?? 0; + if (!this.raiseFramed) this.raiseCentre.set(winner.x + spotX / 2, 0, winner.z + spotZ / 2); + this.raiseCentre.lerp(this.tmpCamera.set(winner.x + spotX / 2, 0, winner.z + spotZ / 2), 1 - Math.exp(-3 * dt)); + this.raiseFramed = true; + const { width, height } = this.hudViewport; + const top = resultCardTop(this.final, width, height, latest.fighters, this.players, this.roundStats, this.viewerId); + const shot = ceremonyShot(this.camera.aspect, this.baseFov, (height - top) / Math.max(1, height)); + const waved = seconds - (this.stoppageRaiseAt - STOPPAGE_RAISE_DELAY_SECONDS); + const drift = reducedMotion ? 0 : Math.sin(waved * 0.35) * 0.06 * shot.distance; + this.cornerPosition.set(THREE.MathUtils.clamp(this.raiseCentre.x, -1.4, 1.4) + drift, shot.height + 0.05 * shot.distance, this.raiseCentre.z + shot.distance); + this.cornerLookAt.set(this.raiseCentre.x, shot.height, this.raiseCentre.z); + return { position: this.cornerPosition, lookAt: this.cornerLookAt, framed: true }; + } + private presentFinish(final: FinalMessage): void { this.referee?.waveOff(); const fighters = this.buffer.latest()?.fighters; @@ -778,10 +1567,15 @@ export class FightRenderer { if (loserIndex >= 0 && this.headCacheValid[loserIndex] && !this.settings().reducedMotion) { this.finishCloseUpIndex = loserIndex; this.finishCloseUpUntil = this.frameSeconds + FINISH_CLOSE_UP_SECONDS; + this.finishCloseUpBearing = null; + this.finishCloseUpAt = this.frameSeconds; } if (final.winner_id === null) return; const index = fighters?.findIndex((fighter) => fighter.player_id === final.winner_id) ?? -1; - if (index >= 0) this.graphs?.[index]?.celebrate(); + if (index < 0) return; + this.graphs?.[index]?.celebrate(WINNER_CELEBRATION_SECONDS); + this.stoppageWinner = index; + this.stoppageRaiseAt = this.frameSeconds + STOPPAGE_RAISE_DELAY_SECONDS; } /** Between rounds the broadcast cuts to each corner in turn, the viewer's own first, once its fighter is seated. */ @@ -791,34 +1585,139 @@ export class FightRenderer { const elapsed = seconds - this.restStartedAt; const index = cornerShot(elapsed, snapshot.phase_ticks_remaining / this.simulation.tick_rate, viewer === 1 ? 1 : 0); if (index === null || !this.graphs[index].stoolVisible) return null; + // The shot waits for the cutman to get down to his work, or he walks across it. + if (this.cutmen !== null && this.cutmanProgress[index]! < CUTMAN_IN_PLACE) return null; cornerFrame(index, this.mapping.x(REST_CORNER_OFFSET), cornerShotProgress(elapsed), this.cornerPosition, this.cornerLookAt); return { position: this.cornerPosition, lookAt: this.cornerLookAt }; } + /** + * Between rounds the fighters sit in opposite corners, too far apart across a phone held upright for + * any shot from the side. The wide shot there looks down the diagonal from high over the viewer's own + * corner (the blue one for a spectator), the near fighter low in the picture and the far one above him. + * It is set by the corners the fighters rest in, never by where they stand, so their walk across to + * them cannot swing it round the ring. + */ + private restWideFrame(snapshot: EngineSnapshot | null): { position: THREE.Vector3; lookAt: THREE.Vector3; framed: boolean } | null { + if (snapshot?.phase !== "rest" || this.portraitPull <= REST_DIAGONAL_PULL) return null; + const near = snapshot.fighters.findIndex((fighter) => fighter.player_id === this.viewerId) === 1 ? 1 : 0; + const corner = this.mapping.x(REST_CORNER_OFFSET); + cornerPoint(near, corner, -REST_DIAGONAL_BACK, REST_DIAGONAL_SWING, this.cornerPosition).setY(REST_DIAGONAL_HEIGHT); + cornerPoint(near, corner, 0, 0, this.cornerLookAt); + this.cornerLookAt.lerp(cornerPoint(near === 0 ? 1 : 0, corner, 0, 0, this.tmpCamera), REST_DIAGONAL_AIM).setY(0.9); + return { position: this.cornerPosition, lookAt: this.cornerLookAt, framed: true }; + } + /** A short high three-quarter close-up on the beaten fighter's face, or on the head where it came to rest, before the result panel. */ private closeUpFrame(seconds: number): { position: THREE.Vector3; lookAt: THREE.Vector3; tight: boolean } | null { if (seconds >= this.finishCloseUpUntil || this.finishCloseUpIndex < 0 || !this.headCacheValid[this.finishCloseUpIndex]) return null; - const severed = this.arcadeInjuries[this.finishCloseUpIndex] === "decapitation" && this.effects.severedHeadPosition(this.finishCloseUpIndex, this.closeUpTarget); - const head = severed ? this.closeUpTarget : this.headCache[this.finishCloseUpIndex]!; - const reach = severed ? 0.8 : 1.05; + const injury = this.arcadeInjuries[this.finishCloseUpIndex]; + const severed = injury === "decapitation" && this.effects.severedHeadPosition(this.finishCloseUpIndex, this.closeUpTarget); + const fallen = severed ? null : this.graphs?.[this.finishCloseUpIndex]?.fallBody ?? null; + // A body still falling has not yet shown which way its face will lie: the shot waits for it to come to + // rest (the finish holds the broadcast's shot meanwhile), and then runs its full length. + if (this.finishCloseUpBearing === null && fallen !== null && !fallen.body.asleep && seconds - this.finishCloseUpAt < CLOSE_UP_SETTLE_SECONDS) { + this.finishCloseUpUntil = seconds + FINISH_CLOSE_UP_SECONDS; + return null; + } + // An eye out of its socket is shot close, on the side the face points to; one hanging under a face on + // the canvas cannot be seen from any side, and the shot is the head's own. Which it is, is decided with + // the side: an eye swinging across that line would otherwise jump the shot between the two. + const eyeOut = !severed && (injury === "eye_left" || injury === "eye_right") && this.effects.eyePosition(this.finishCloseUpIndex, this.closeUpTarget); + if (this.finishCloseUpBearing === null) this.finishCloseUpHanging = eyeOut && this.eyeInSight(this.closeUpTarget); + const hanging = eyeOut && this.finishCloseUpHanging; + const head = severed || hanging ? this.closeUpTarget : this.headCache[this.finishCloseUpIndex]!; + const reach = severed ? 0.8 : hanging ? 0.45 : 1.05; const drift = (seconds - (this.finishCloseUpUntil - FINISH_CLOSE_UP_SECONDS)) * 0.25 - 0.2; // Shoot from the ring-centre side of the fighter so the ropes stay behind the face. const toCentre = Math.atan2(-head.x, -head.z); - const angle = (Math.hypot(head.x, head.z) > 0.4 ? toCentre : 0.9) + drift; + // The side is chosen once: a head still tumbling would swing the camera round it. + if (this.finishCloseUpBearing === null) { + const skull = hanging ? this.headWorldPose(this.finishCloseUpIndex) : null; + // A hanging eye is shot from the side of the head it hangs on. + // A head on the canvas is shot from the side its face turns to, and never across its own body. + const facing = severed && this.effects.severedHeadFacing(this.finishCloseUpIndex, this.closeUpFacing) + ? this.closeUpFacing + : skull !== null + ? this.closeUpFacing.copy(head).sub(skull.position).normalize() + : fallen?.faceDirection(this.closeUpFacing) ?? null; + const winner = this.finishCloseUpIndex === 0 ? this.tmpB : this.tmpA; + const blockers = [{ x: winner.x, z: winner.z, radius: STANDING_BLOCK_RADIUS }, { x: this.refereePosition.x, z: this.refereePosition.z, radius: STANDING_BLOCK_RADIUS }]; + if (fallen !== null) { + for (const point of [1, 2, 3, 4] as const) { + fallen.bodyPoint(point, this.bodyPoint); + blockers.push({ x: this.bodyPoint.x, z: this.bodyPoint.z, radius: FALLEN_BLOCK_RADIUS }); + } + } + this.finishCloseUpBearing = closeUpAngle(head.x, head.z, facing, reach, TIGHT_SHOT_LIMIT, Math.hypot(head.x, head.z) > 0.4 ? toCentre : 0.9, blockers); + } + const angle = this.finishCloseUpBearing + drift; this.closeUpPosition.set( THREE.MathUtils.clamp(head.x + Math.sin(angle) * reach, -TIGHT_SHOT_LIMIT, TIGHT_SHOT_LIMIT), - head.y + (severed ? 0.42 : 0.75), + head.y + (severed ? 0.42 : hanging ? 0.5 : 0.75), THREE.MathUtils.clamp(head.z + Math.cos(angle) * reach, -TIGHT_SHOT_LIMIT, TIGHT_SHOT_LIMIT), ); this.replayLookAt.copy(head); return { position: this.closeUpPosition, lookAt: this.replayLookAt, tight: true }; } + /** + * Where the referee stands to lift the winner's arm, as an offset from the winner: the first of + * RAISE_SPOTS clear of the beaten fighter, of a body and a head on the canvas, and inside the ropes, or the + * least crowded of them. A spot taken by any of those would keep him stepping in and being pushed back out. + */ + private raiseSpot(winner: THREE.Vector3, loser: THREE.Vector3): { x: number; z: number } { + const toCentre = winner.x > 0 ? -1 : 1; + let best = { x: toCentre * STOPPAGE_RAISE_SPACING, z: 0 }; + let bestRoom = Number.NEGATIVE_INFINITY; + for (const [across, along] of RAISE_SPOTS) { + const x = winner.x + toCentre * across * STOPPAGE_RAISE_SPACING; + const z = winner.z + along * STOPPAGE_RAISE_SPACING; + let room = Math.min(RAISE_LIMIT - Math.abs(x), RAISE_LIMIT - Math.abs(z), Math.hypot(x - loser.x, z - loser.z) - RAISE_CLEARANCE); + for (const graph of this.graphs ?? []) { + const body = graph.fallBody; + if (body === null) continue; + for (const point of [0, 1, 2, 3, 4] as const) { + body.bodyPoint(point, this.bodyPoint); + room = Math.min(room, Math.hypot(x - this.bodyPoint.x, z - this.bodyPoint.z) - BODY_CLEARANCE - 0.05); + } + } + for (const index of [0, 1]) { + if (this.effects.severedHeadPosition(index, this.closeUpTarget)) room = Math.min(room, Math.hypot(x - this.closeUpTarget.x, z - this.closeUpTarget.z) - HEAD_CLEARANCE - 0.05); + } + if (room >= 0) return { x: x - winner.x, z: z - winner.z }; + if (room > bestRoom) { + bestRoom = room; + best = { x: x - winner.x, z: z - winner.z }; + } + } + return best; + } + + /** The winner's glove nearer the referee, in world space. */ + private nearerGlove(left: THREE.Bone, right: THREE.Bone): THREE.Vector3 { + left.getWorldPosition(this.stoppageWrist); + right.getWorldPosition(this.stoppageOtherWrist); + const reach = (glove: THREE.Vector3): number => Math.hypot(glove.x - this.refereePosition.x, glove.z - this.refereePosition.z); + return reach(this.stoppageOtherWrist) < reach(this.stoppageWrist) ? this.stoppageOtherWrist : this.stoppageWrist; + } + + /** Whether an eye hanging at `eye` is clear of the beaten fighter's skull from above, rather than under his face. */ + private eyeInSight(eye: THREE.Vector3): boolean { + const skull = this.headWorldPose(this.finishCloseUpIndex); + if (skull === null) return false; + const fromSkull = this.closeUpFacing.copy(eye).sub(skull.position); + return fromSkull.y > -0.5 * fromSkull.length(); + } + /** Replays the recorded snapshots around the knockdown from a close camera before the result panel. */ private startReplay(plan: ReplayPlan): void { const buffer = new SnapshotBuffer(plan.snapshots.length + 2, this.simulation.tick_rate); for (const snapshot of plan.snapshots) buffer.push(snapshot); this.replay = { plan, buffer, startedAt: this.frameSeconds, impactFired: false, side: null }; + // The replay cuts to other moments: the fighters are drawn there at once, not eased across. + this.drawnOffsetsLive = false; + this.commentary.replay(this.frameSeconds); this.replayFollow = 0; this.replayFollowAt = 0; for (const index of [0, 1] as const) { @@ -829,15 +1728,47 @@ export class FightRenderer { this.restoreInjury(index); } } - for (const graph of this.graphs ?? []) graph.resetTransient(false); + // The replay shows the punch that ended the bout. A contact still waiting for the live clock is not + // shown live as well: the injury it carried lands at the replay's impact instead, and what came with + // the punch (the knockdown it caused, with its thud, roar and rumble) is held for that impact. + for (const pending of this.pendingContacts.splice(0)) { + const index = pending.recipientIndex; + if (pending.injury !== null && (index === 0 || index === 1) && this.replayInjuries[index] === null && this.arcadeInjuries[index] === null) { + this.replayInjuries[index] = { injury: pending.injury, event: pending.event }; + } + if (pending.event.event_id === plan.impact.event_id) continue; + pending.injury = null; + pending.contactTick = Number.POSITIVE_INFINITY; + this.pendingContacts.push(pending); + } + const victim = plan.snapshots[0]?.fighters.findIndex((fighter) => fighter.player_id === plan.impact.target_id) ?? -1; + // The replay's punch knocks the gum shield out again, so it is back in his mouth for the lead-up. + if (victim >= 0 && plan.impact.detail.endsWith(":head")) this.effects.returnMouthpiece(victim); + // The impact is the body punch itself, so the knockdown it caused says whether he went to one knee. + const kneels = this.lastKnockdown?.knockdown.detail === BODY_KNOCKDOWN; + for (const [index, graph] of (this.graphs ?? []).entries()) { + // The physics runs the recorded fall again only when it is this knockdown's. One the render clock had + // not reached when the bout ended on the punch was never recorded: the fall recorded is an earlier + // knockdown's, somewhere else, and the replay runs a new one from the replayed blow instead. + const recorded = this.liveFallTicks[index] ?? null; + graph.resetTransient(false); + if (recorded !== null && recorded >= plan.impact.tick - 1) graph.primeReplayFall(); + if (index === victim && kneels) graph.fallToKnee(true); + } this.finalRevealAt = this.frameSeconds + plan.durationSeconds + finalRevealDelay(this.final); } private endReplay(): void { this.replay = null; + this.drawnOffsetsLive = false; this.reapplyReplayInjuries(); const live = this.buffer.latest(); - for (const [index, graph] of (this.graphs ?? []).entries()) graph.resetTransient(live?.fighters[index]?.is_downed === true); + for (const [index, graph] of (this.graphs ?? []).entries()) { + const fighter = live?.fighters[index]; + const knockedOut = fighter !== undefined && fighter.player_id === this.flashKnockout?.loserId; + graph.resetTransient(fighter?.is_downed === true || knockedOut); + if (knockedOut) graph.knockOut(); + } if (this.final !== null) this.presentFinish(this.final); } @@ -848,17 +1779,27 @@ export class FightRenderer { const { recipientIndex, puncherIndex } = contactParticipants(event, snapshot); const recipient = snapshot.fighters[recipientIndex]; if (recipient === undefined) return; - this.tmpA.set(this.mapping.x(recipient.x), 0, this.mapping.z(recipient.y)); + drawnPlace(this.graphs?.[recipientIndex], recipient, this.mapping, this.contactPoint); const spray = sprayDirection(snapshot.fighters[puncherIndex], recipient, this.mapping); - this.effects.addEvent(event, this.tmpA, this.settings().reducedMotion, spray); + this.effects.addEvent(event, this.contactPoint, this.settings().reducedMotion, spray); const puncher = puncherIndex >= 0 ? snapshot.fighters[puncherIndex] : undefined; const keyParts = puncher?.action_key?.split(":") ?? []; const punchClass = (keyParts[0] ?? null) as PunchClass | null; const hand = (keyParts[1] ?? null) as Hand | null; this.graphs?.[recipientIndex]?.react("hit", event.detail.endsWith(":body") ? "body" : "head", event.direction, punchClass, hand, event.amount); + if (record.knockdown.detail === "body" && record.knockdown.tick > event.tick) { + this.graphs?.[recipientIndex]?.windedFor((record.knockdown.tick - event.tick) / this.simulation.tick_rate + 0.15, bodySideStruck(puncher?.action_key ?? null)); + } + if (recipient.player_id === this.flashKnockout?.loserId) this.graphs?.[recipientIndex]?.knockOut(); if (puncherIndex >= 0) this.graphs?.[puncherIndex]?.landedHit(false); + // The punch that floored him knocks the gum shield out again, in slow motion. + if (event.detail.endsWith(":head") && !this.settings().reducedMotion) this.knockOutMouthpiece(recipientIndex, spray ?? event.direction, event.event_id + REPLAY_EVENT_ID_OFFSET, true); this.onContact?.(event); this.reapplyReplayInjuries(spray); + // What came with the punch and was held for this moment (see startReplay) is shown with it. + for (const pending of this.pendingContacts) { + if (pending.contactTick === Number.POSITIVE_INFINITY) pending.contactTick = this.replay?.plan.impact.tick ?? snapshot.tick; + } } private replayFrame(snapshot: EngineSnapshot, elapsed: number): { position: THREE.Vector3; lookAt: THREE.Vector3; tight: boolean } { @@ -875,8 +1816,11 @@ export class FightRenderer { this.replayFollowAt = elapsed; this.replayFollow += ((falling ? 1 : 0) - this.replayFollow) * follow; const favour = victim < 0 ? 0 : Math.max(this.portraitPull > 1.3 ? 0.7 : 0, this.replayFollow * 0.8); - const midX = (ax + bx) / 2 + ((victim === 1 ? bx : ax) - (ax + bx) / 2) * favour; - const midZ = (az + bz) / 2 + ((victim === 1 ? bz : az) - (az + bz) / 2) * favour; + // Once he is falling, the shot follows his head wherever the fall takes it. + const victimX = falling ? this.headCache[victim]!.x : victim === 1 ? bx : ax; + const victimZ = falling ? this.headCache[victim]!.z : victim === 1 ? bz : az; + const midX = (ax + bx) / 2 + (victimX - (ax + bx) / 2) * favour; + const midZ = (az + bz) / 2 + (victimZ - (az + bz) / 2) * favour; let nx = -(bz - az); let nz = bx - ax; const length = Math.hypot(nx, nz) || 1; @@ -912,6 +1856,7 @@ export class FightRenderer { setInputLatency(milliseconds: number | null): void { this.inputLatencyMs = milliseconds; + this.lagWarned = lagWarning(milliseconds, this.lagWarned); } /** The server clock starts over after a new connection or a paused bout, so the render clock relearns it. */ @@ -942,7 +1887,9 @@ export class FightRenderer { const graphs = this.graphs; if (graphs !== null) { const graph = graphs[index]!; - graph.boxer.setDecapitated(injury === "decapitation"); + if (injury === "head_burst") graph.boxer.setHeadBurst(true); + else graph.boxer.setDecapitated(injury === "decapitation"); + graph.boxer.headInjury.setEyeOut(injury === "eye_left" ? "left" : injury === "eye_right" ? "right" : null); graph.boxer.setHandDismembered("left", injury === "dismember_left"); graph.boxer.setHandDismembered("right", injury === "dismember_right"); const dislocation: ArcadeDislocation | null = injury === "jaw_dislocation" @@ -965,7 +1912,7 @@ export class FightRenderer { const pose = this.headWorldPose(index); if (pose !== null) { const graph = this.graphs?.[index]; - const baked = graph === undefined ? undefined : bakeSkinnedPart(graph.boxer.headMesh, pose.position, pose.quaternion); + const baked = graph === undefined ? undefined : bakeSeveredHead(graph.boxer, pose.position, pose.quaternion); this.effects.decapitate( index, pose.position, @@ -976,7 +1923,21 @@ export class FightRenderer { baked, ); const stumpPose = this.stumpWorldPose(index); - if (stumpPose !== null) this.effects.anchorStump(index, stumpPose.position, stumpPose.quaternion); + if (stumpPose !== null) this.effects.anchorStump(index, stumpPose.position, stumpPose.quaternion, stumpPose.rim, stumpPose.across); + applied = true; + } + } else if (injury === "head_burst") { + const pose = this.headWorldPose(index); + if (pose !== null) { + this.effects.burstHead(index, pose.position, spray ?? event.direction, event.event_id); + const jaw = this.burstStumpPose(index); + if (jaw !== null) this.effects.anchorStump(index, jaw.position, jaw.quaternion, jaw.rim, jaw.across); + applied = true; + } + } else if (injury === "eye_left" || injury === "eye_right") { + const boxer = this.graphs?.[index]?.boxer; + if (boxer !== undefined && eyeSocket(boxer, injury === "eye_left" ? "left" : "right", this.tmpPart, this.tmpStumpOffset, this.tmpPartQuaternion)) { + this.effects.gougeEye(index, this.tmpPart, this.tmpStumpOffset, spray ?? event.direction, event.event_id); applied = true; } } else if (injury === "dismember_left" || injury === "dismember_right") { @@ -984,7 +1945,7 @@ export class FightRenderer { const pose = this.handWorldPose(index, side); if (pose !== null) { const graph = this.graphs?.[index]; - const baked = graph === undefined ? undefined : bakeSkinnedPart(graph.boxer.gloveMesh(side), pose.position, pose.quaternion); + const baked = graph === undefined ? undefined : { ...bakeSkinnedPart(graph.boxer.gloveMesh(side), pose.position, pose.quaternion), gloveBlood: graph.boxer.gloveBloodLevel }; this.effects.dismemberHand( index, side, @@ -998,6 +1959,19 @@ export class FightRenderer { this.effects.anchorHandStump(index, side, pose.position, pose.quaternion); applied = true; } + } else if (injury === "ribs_left" || injury === "ribs_right") { + // The ribs cave in under the punch (held every frame in draw, see caveInRibs) and blood bursts from the + // side, from rib height while he is still up; on the canvas the wound and the pool tell it. + const graph = this.graphs?.[index]; + const fighter = this.buffer.latest()?.fighters[index]; + if (graph !== undefined && fighter !== undefined) { + if (graph.boxer.rig.bones.upperChest.getWorldPosition(this.tmpPart).y > RIBS_BURST_CHEST_HEIGHT) { + drawnPlace(graph, fighter, this.mapping, this.contactPoint); + const burst: CombatEvent = { ...event, event_id: event.event_id + RIBS_BURST_EVENT_OFFSET, kind: "counter_hit", detail: "hook:body", amount: Math.max(event.amount, BIG_SHOT), blood: 100 }; + this.effects.addEvent(burst, this.contactPoint, false, spray); + } + applied = true; + } } if (!applied) return false; this.arcadeInjuries[index] = injury; @@ -1042,7 +2016,7 @@ export class FightRenderer { graphs[index]!.boxer.root.updateMatrixWorld(true); head.getWorldPosition(this.tmpHead); head.getWorldQuaternion(this.tmpHeadQuaternion); - this.tmpStumpOffset.set(0, 0.12, 0.02).applyQuaternion(this.tmpHeadQuaternion); + this.tmpStumpOffset.copy(HEAD_PIVOT_OFFSET).applyQuaternion(this.tmpHeadQuaternion); this.tmpHead.add(this.tmpStumpOffset); return { position: this.tmpHead, quaternion: this.tmpHeadQuaternion }; } @@ -1068,14 +2042,40 @@ export class FightRenderer { return this.graphs?.[index]?.boxer.skinBaseColor.getHex() ?? 0xb0703f; } - private stumpWorldPose(index: number): { position: THREE.Vector3; quaternion: THREE.Quaternion } | null { - const graphs = this.graphs; - const anchor = graphs !== null ? graphs[index]!.boxer.bone("Neck_012") : null; - if (anchor === null || graphs === null) return null; - graphs[index]!.boxer.root.updateMatrixWorld(true); - anchor.getWorldPosition(this.tmpStump); - anchor.getWorldQuaternion(this.tmpStumpQuaternion); - return { position: this.tmpStump, quaternion: this.tmpStumpQuaternion }; + /** + * The exposed cut through the neck, measured on the skin as it is posed: its middle, facing up the + * neck, and its edge, both ends of each of its edges in turn. + */ + private stumpWorldPose(index: number): { position: THREE.Vector3; quaternion: THREE.Quaternion; rim: Float32Array; across: THREE.Vector3 } | null { + const boxer = this.graphs?.[index]?.boxer; + const head = boxer?.bone("head"); + if (boxer === undefined || head === undefined || head === null) return null; + boxer.root.updateMatrixWorld(true); + const mesh = boxer.headMesh; + const rim = neckRim(mesh.geometry); + if (rim.length === 0) return null; + if (this.stumpRim.length !== rim.length * 3) this.stumpRim = new Float32Array(rim.length * 3); + this.tmpStump.set(0, 0, 0); + for (const [at, corner] of rim.entries()) { + posedHeadVertex(boxer, corner, this.tmpStumpOffset); + this.tmpStumpOffset.toArray(this.stumpRim, at * 3); + this.tmpStump.add(this.tmpStumpOffset); + } + this.tmpStump.multiplyScalar(1 / rim.length); + head.getWorldPosition(this.tmpStumpOffset).sub(this.tmpStump).normalize(); + // The head's own sideways axis turns the cross-section with the neck: the windpipe at the throat, the spine at the nape. + this.tmpStumpAcross.set(1, 0, 0).applyQuaternion(head.getWorldQuaternion(this.tmpStumpQuaternion)); + this.tmpStumpQuaternion.setFromUnitVectors(UP, this.tmpStumpOffset); + return { position: this.tmpStump, quaternion: this.tmpStumpQuaternion, rim: this.stumpRim, across: this.tmpStumpAcross }; + } + + private burstStumpPose(index: number): { position: THREE.Vector3; quaternion: THREE.Quaternion; rim: Float32Array; across: THREE.Vector3 } | null { + const boxer = this.graphs?.[index]?.boxer; + if (boxer === undefined) return null; + const rim = measureBurstStump(boxer, this.stumpRim, this.burstStump); + if (rim === null) return null; + this.stumpRim = rim; + return { position: this.burstStump.position, quaternion: this.burstStump.quaternion, rim, across: this.burstStump.across }; } predictAction(action: SemanticAction): void { @@ -1083,7 +2083,7 @@ export class FightRenderer { if (latest === null || this.viewerId === null || this.replay !== null) return; const index = latest.fighters.findIndex((fighter) => fighter.player_id === this.viewerId); if (index < 0) return; - // The server turns a punch or an evasion down outside the fight phase and while the fighter cannot act or pay for it. + // The server turns a punch or an evasion down outside the fight phase and while the fighter cannot act. const fighter = latest.fighters[index]!; if (latest.phase !== "fight" || !canStartPunch(fighter)) return; const leadTicks = ((this.inputLatencyMs ?? 60) / 1000) * this.simulation.tick_rate + this.buffer.interpolationDelayTicks; @@ -1091,11 +2091,12 @@ export class FightRenderer { // Behind a punch the server holds it, so it is shown at once only when the fighter is free. const busy = this.graphs?.[index]?.ownPunchActive === true || attackTicksRemaining(fighter, latest.tick) > 0 || fighter.queued_actions > 0; if (!busy && action.id !== undefined && fighter.stamina >= EVASION_STAMINA) { - this.evasion.press(action.kind, action.id, this.manualClock ? this.lastManualTime : performance.now(), leadTicks, this.simulation.tick_rate); + this.evasion.press(action.kind, action.id, this.manualClock ? this.lastManualTime : performance.now(), leadTicks, this.simulation.tick_rate, EVASION_TICKS + styleTiming(fighter.style).evasionTicks); } return; } - if (action.kind !== "punch" || !canAffordPunch(fighter, action)) return; + // A fighter who cannot pay for it in full still throws it, as a slow arm punch (see predictedPunchTiming). + if (action.kind !== "punch") return; this.graphs?.[index]?.predict(action, performance.now() / 1000, this.simulation.tick_rate, leadTicks, predictedPunchTiming(fighter, action, latest.tick + 1)); } @@ -1119,13 +2120,33 @@ export class FightRenderer { this.history.push(snapshot); if (this.history.length > HISTORY_LIMIT) this.history.shift(); for (const event of accepted) { - this.roundStats.record(event); + this.roundStats.record(event, accepted); if (event.kind === "knockdown") { - const hit = accepted.find((candidate) => (candidate.kind === "hit" || candidate.kind === "counter_hit") && candidate.target_id === event.target_id) ?? null; - this.lastKnockdown = { knockdown: event, hit }; + // The punch that put him down landed on the knockdown's own tick; a snapshot can carry older ones too. + const hit = accepted.find((candidate) => isHit(candidate) && candidate.target_id === event.target_id && candidate.tick === event.tick) + ?? (event.detail === "body" ? this.recordedHit(event) : null); + // A punch the guard took can still floor a fighter with no poise left. The replay shows it as it was, + // but it earns no finisher. + this.lastKnockdown = { knockdown: event, hit, finisher: hit !== null && wasBlocked(hit, snapshot.events) ? null : knockdownFinisher(hit, snapshot) }; + const downed = snapshot.fighters.findIndex((fighter) => fighter.player_id === event.target_id); + this.graphs?.[downed]?.fallToKnee(event.detail === BODY_KNOCKDOWN); + } + if (event.kind === "result" && event.detail === "flash_ko") { + const hit = accepted.findLast((candidate) => (candidate.kind === "hit" || candidate.kind === "counter_hit") && candidate.actor_id === event.actor_id) ?? null; + if (hit !== null && hit.target_id !== null) { + this.flashKnockout = { hitEventId: hit.event_id, loserId: hit.target_id }; + // The replay shows the knockout punch and the fall as it would a knockdown. + // Its injury, if any, is done live by the punch itself, so nothing is left for the replay to apply. + this.lastKnockdown = { knockdown: { ...hit, kind: "knockdown" }, hit, finisher: null }; + } } if (event.kind === "referee_break") this.referee?.breakClinch(); + // The fighter who beat the count is ready and the referee sends them back to it. + if (event.kind === "box") this.eventCallout = { text: "BOX!", until: this.frameSeconds + BOX_CALLOUT_SECONDS }; + if (event.kind === "body_collapse" && event.target_id === this.viewerId) this.ownCollapseUntil = event.tick + event.amount; + this.movement.observe(event, this.viewerId); } + this.commentary.observe(snapshot, accepted, this.players, this.simulation.tick_rate, this.frameSeconds); for (const { event, presentationEvent, presentImpact, reactAmount } of contactPresentationPlan(accepted, snapshot)) { const targetIndex = snapshot.fighters.findIndex((fighter) => fighter.player_id === event.target_id); const actorIndex = snapshot.fighters.findIndex((fighter) => fighter.player_id === event.actor_id); @@ -1134,8 +2155,8 @@ export class FightRenderer { ?? snapshot.fighters[targetIndex] ?? snapshot.fighters[actorIndex] ?? snapshot.fighters[0]; - this.tmpA.set(this.mapping.x(recipient.x), 0, this.mapping.z(recipient.y)); - if (["hit", "counter_hit", "block", "perfect_block", "guard_break", "knockdown"].includes(event.kind)) { + drawnPlace(this.graphs?.[snapshot.fighters.indexOf(recipient)], recipient, this.mapping, this.contactPoint); + if (CONTACT_KINDS.has(event.kind)) { const puncher = puncherIndex >= 0 ? snapshot.fighters[puncherIndex]! : null; this.pendingContacts.push({ event, @@ -1148,7 +2169,7 @@ export class FightRenderer { injury: arcadeInjuryFor(event, snapshot.fighters[recipientIndex], snapshot.result, puncher ?? undefined), }); } else if (event.kind === "bleed") { - this.effects.addEvent(event, this.tmpA, this.settings().reducedMotion); + this.effects.addEvent(event, this.contactPoint, this.settings().reducedMotion); } } } @@ -1161,15 +2182,19 @@ export class FightRenderer { continue; } this.pendingContacts.splice(index, 1); - const { event, presentationEvent, presentImpact, reactAmount, recipientIndex, puncherIndex, injury } = pending; + const { event, presentationEvent, reactAmount, recipientIndex, puncherIndex, injury } = pending; + // A body shot that dropped him a moment later has already been shown landing. + const presentImpact = pending.presentImpact && !isDelayedBodyKnockdown(presentationEvent); const target = this.buffer.latest()?.fighters[recipientIndex]; + // Blood, teeth and severed parts fly the way the punch travelled. const spray = sprayDirection(this.buffer.latest()?.fighters[puncherIndex], target, this.mapping); + this.presentFightEvent(event, recipientIndex, puncherIndex); if (presentImpact && target !== undefined) { - this.tmpA.set(this.mapping.x(target.x), 0, this.mapping.z(target.y)); - this.effects.addEvent(presentationEvent, this.tmpA, this.settings().reducedMotion, spray); + drawnPlace(this.graphs?.[recipientIndex], target, this.mapping, this.contactPoint); + this.effects.addEvent(presentationEvent, this.contactPoint, this.settings().reducedMotion, spray); } const currentSettings = this.settings(); - if (presentImpact) this.arena.excite(CROWD_EXCITEMENT[event.kind] ?? 0); + if (pending.presentImpact || UNIMPACTFUL_KINDS.has(event.kind)) this.arena.excite(CROWD_EXCITEMENT[event.kind] ?? 0); if ( presentImpact && recipientIndex >= 0 @@ -1189,19 +2214,15 @@ export class FightRenderer { this.applyArcadeInjury(recipientIndex, injury, event, spray); } const graphs = this.graphs; - if ( - presentImpact - && recipientIndex >= 0 - && ["hit", "counter_hit", "knockdown"].includes(event.kind) - && presentationEvent.detail.endsWith(":head") - && event.amount >= 260 - && !currentSettings.reducedMotion - && currentSettings.blood !== "off" - ) { + if (presentImpact && recipientIndex >= 0 && !currentSettings.reducedMotion && mouthpieceFlies(event.kind, event.amount, presentationEvent.detail.endsWith(":head"))) { + this.knockOutMouthpiece(recipientIndex, spray ?? presentationEvent.direction, event.event_id, false); + } + const teeth = presentImpact && recipientIndex >= 0 ? teethFor(event.kind, event.amount, presentationEvent.detail.endsWith(":head")) : 0; + if (teeth > 0 && !currentSettings.reducedMotion && currentSettings.blood !== "off") { const pose = this.headWorldPose(recipientIndex); if (pose !== null) { - this.tmpB.set(0, -0.07, 0.1).applyQuaternion(pose.quaternion).add(pose.position); - this.effects.spawnTeeth(this.tmpB, spray ?? presentationEvent.direction, event.kind === "knockdown" ? 3 : 1 + Math.floor((event.amount - 260) / 120), event.event_id); + this.mouthPoint.set(0, -0.07, 0.1).applyQuaternion(pose.quaternion).add(pose.position); + this.effects.spawnTeeth(this.mouthPoint, spray ?? presentationEvent.direction, teeth, event.event_id); } } if ( @@ -1218,6 +2239,7 @@ export class FightRenderer { const punchClass = (keyParts[0] ?? presentationEvent.detail.split(":")[0] ?? null) as PunchClass | null; const punchHand = (keyParts[1] ?? null) as Hand | null; graphs[recipientIndex]!.react(blocked ? "block" : "hit", targetKind, presentationEvent.direction, punchClass, punchHand, reactAmount); + if (this.flashKnockout?.hitEventId === event.event_id) graphs[recipientIndex]!.knockOut(); } } if ( @@ -1227,10 +2249,48 @@ export class FightRenderer { ) { graphs?.[puncherIndex]?.landedHit(event.kind === "block" || event.kind === "perfect_block"); } - this.onContact?.(event); + // The hit that leaked through a block is heard and felt as the block it came with, not as a landed punch. + if (pending.presentImpact || !isHit(event)) this.onContact?.(event); + } + } + + /** The parry's stagger, the body shot that is putting a fighter down, and the broadcast's callouts. */ + private presentFightEvent(event: CombatEvent, recipientIndex: number, puncherIndex: number): void { + const callout = EVENT_CALLOUTS[event.kind]; + if (callout !== undefined) this.eventCallout = { text: callout.text, until: this.frameSeconds + callout.seconds }; + const graphs = this.graphs; + if (graphs === null || recipientIndex < 0) return; + if (event.kind === "parry") graphs[recipientIndex]?.stagger(); + if (event.kind === "body_collapse") { + const puncher = puncherIndex >= 0 ? this.buffer.latest()?.fighters[puncherIndex] : undefined; + graphs[recipientIndex]?.windedFor(event.amount / this.simulation.tick_rate + 0.15, bodySideStruck(puncher?.action_key ?? null)); } } + /** The punch that landed with this knockdown's action id, from the recorded snapshots. */ + private recordedHit(knockdown: CombatEvent): CombatEvent | null { + if (knockdown.action_id === null) return null; + for (let index = this.history.length - 1; index >= 0; index -= 1) { + const hit = this.history[index]!.events.find((candidate) => (candidate.kind === "hit" || candidate.kind === "counter_hit") && candidate.action_id === knockdown.action_id && candidate.target_id === knockdown.target_id); + if (hit !== undefined) return hit; + } + return null; + } + + /** The bell ends the round: the rest starts and the corners put the gum shields back in. */ + private enterRest(seconds: number): void { + this.restStartedAt = seconds; + this.effects.clearMouthpieces(); + } + + /** The gum shield flies out of the mouth along the punch, from the head as it is posed. */ + private knockOutMouthpiece(index: number, direction: number | SprayDirection, eventId: number, again: boolean): void { + const pose = this.headWorldPose(index); + if (pose === null) return; + this.mouthPoint.set(0, -0.075, 0.1).applyQuaternion(pose.quaternion).add(pose.position); + this.effects.ejectMouthpiece(index, this.mouthPoint, pose.quaternion, direction, eventId, this.gearColor(index), again); + } + /** True once the result panel is on screen, after any knockout replay and close-up. */ get resultVisible(): boolean { return this.final !== null && this.frameSeconds >= this.finalRevealAt; @@ -1261,6 +2321,8 @@ export class FightRenderer { const low = this.scaler.scale <= LOW_TIER_SCALE; this.bloomPass.enabled = !low; if (this.keyLight !== null) setKeyShadowTier(this.keyLight.shadow, low, this.scaler.scale < 0.8 ? 1024 : 2048); + this.arena.setLowTier(low); + this.effects.setLowTier(low); } private setupLights(): void { @@ -1330,7 +2392,8 @@ export class FightRenderer { this.camera.aspect = width / height; // Narrow viewports widen the lens a little and pull the camera back (see the frame step below). this.portraitPull = THREE.MathUtils.clamp(1.2 / this.camera.aspect, 1, 2.2); - this.camera.fov = 36 * Math.min(1.3, Math.sqrt(this.portraitPull)); + this.baseFov = 36 * Math.min(1.3, Math.sqrt(this.portraitPull)); + this.camera.fov = this.baseFov; this.camera.updateProjectionMatrix(); } } @@ -1343,8 +2406,10 @@ export class FightRenderer { this.setBloodLevel(current.blood); const latest = this.buffer.latest(); + const frameDt = dt; const finishing = latest?.result !== null && latest?.result !== undefined && STOPPAGE_METHODS.has(latest.result.finish_method); + this.cheer(seconds, dt); if (finishing && !this.finishSeen) { this.finishSeen = true; this.finishSlowMotion = 2.2; @@ -1357,6 +2422,11 @@ export class FightRenderer { } this.viewerHitFlash = Math.max(0, this.viewerHitFlash - dt * 3.2); let sampledTick = latest === null ? 0 : manual ? presentationTickFor(latest) : this.buffer.renderTick(time); + // The hurt vision and the muffle start with the punch that rocked him, when the render clock shows it, + // not when the snapshot carrying the stun arrives a playback delay earlier. The sound follows the + // newest server tick, which its own clocks count. + const rocked = this.updateRocked(latest === null ? null : this.shownAt(sampledTick), frameDt, current.reducedMotion); + if (rocked !== null && latest !== null) this.onRocked?.(rocked, latest.tick); let snapshot = latest === null ? null : this.applyLocalPrediction(this.buffer.sample(sampledTick), dt, time); const replay = this.replay; if (replay !== null) { @@ -1376,8 +2446,10 @@ export class FightRenderer { const actorDt = this.replay !== null ? dt * this.replay.plan.speed : dt; let separation = 1.8; let knockdown = false; + let downedAt: { x: number; z: number } | null = null; if (snapshot !== null) { - const [a, b] = snapshot.fighters; + const ceremony = this.replay === null ? this.ceremony : null; + const [a, b] = ceremony !== null ? this.ceremonyFighters(ceremony, snapshot.fighters, actorDt, seconds, this.drawnOffsets) : this.standApart(snapshot.fighters, actorDt); for (const [index, fighter] of snapshot.fighters.entries()) { if (this.arcadeInjuries[index] === null || this.replay !== null) continue; if (fighter.is_downed) this.observedInjuryDown[index] = true; @@ -1385,21 +2457,32 @@ export class FightRenderer { } const graphs = this.graphs; if (graphs !== null) { + this.wearLook(0, a.player_id); + this.wearLook(1, b.player_id); const headA = this.headCacheValid[0] ? this.headCache[0] : undefined; const headB = this.headCacheValid[1] ? this.headCache[1] : undefined; graphs[0].boxer.root.visible = true; graphs[1].boxer.root.visible = true; - graphs[0].setResting(snapshot.phase === "rest"); - graphs[1].setResting(snapshot.phase === "rest"); + const resting = cornersAtWork(snapshot, this.simulation.tick_rate); + graphs[0].setResting(resting); + graphs[1].setResting(resting); const countdown = snapshot.phase === "countdown" ? snapshot.phase_ticks_remaining : null; graphs[0].setCountdown(countdown); graphs[1].setCountdown(countdown); + // Counted out, or replayed going down, a fighter stays where the fall left him whatever his meter says. + const overOnTheCanvas = this.replay !== null || snapshot.phase === "complete"; + graphs[0].stayDown(overOnTheCanvas); + graphs[1].stayDown(overOnTheCanvas); if (snapshot.phase === "fight" && this.lastPhase === "rest" && this.replay === null) { this.roundCalloutUntil = seconds + ROUND_CALLOUT_SECONDS; this.roundCalloutRound = snapshot.round_number; } - if (snapshot.phase === "rest" && this.lastPhase !== "rest") this.restStartedAt = seconds; + if (snapshot.phase === "rest" && this.lastPhase !== "rest") this.enterRest(seconds); this.lastPhase = snapshot.phase; + this.anticipatePunches(latest, sampledTick); + // A fighter going down falls against his standing opponent, not through him. + graphs[0].setObstacle(graphs[1].boxer.root.position.x, graphs[1].boxer.root.position.z); + graphs[1].setObstacle(graphs[0].boxer.root.position.x, graphs[0].boxer.root.position.z); graphs[0].update(a, b, actorDt, seconds, current.reducedMotion, current.blood, sampledTick, headB); graphs[1].update(b, a, actorDt, seconds, current.reducedMotion, current.blood, sampledTick, headA); for (const [index, graph] of graphs.entries()) { @@ -1409,17 +2492,31 @@ export class FightRenderer { headBone.getWorldPosition(this.headCache[index]!); this.headCacheValid[index] = true; } + // Which knockdown the physics last recorded a fall for, so the replay runs only that one again. + const falling = graph.fallBody !== null; + if (falling && !this.fallingLive[index] && this.replay === null) this.liveFallTicks[index] = sampledTick; + this.fallingLive[index] = falling; } } for (let index = 0; index < this.arcadeInjuries.length; index += 1) { const injury = this.arcadeInjuries[index]; if (injury === "decapitation") { const pose = this.stumpWorldPose(index); - if (pose !== null) this.effects.anchorStump(index, pose.position, pose.quaternion); + if (pose !== null) this.effects.anchorStump(index, pose.position, pose.quaternion, pose.rim, pose.across); + } else if (injury === "head_burst") { + const jaw = this.burstStumpPose(index); + if (jaw !== null) this.effects.anchorStump(index, jaw.position, jaw.quaternion, jaw.rim, jaw.across); + } else if (injury === "eye_left" || injury === "eye_right") { + const boxer = this.graphs?.[index]?.boxer; + const skull = this.headWorldPose(index); + if (boxer !== undefined && skull !== null && eyeSocket(boxer, injury === "eye_left" ? "left" : "right", this.tmpPart, this.tmpStumpOffset, this.tmpPartQuaternion)) this.effects.anchorEye(index, this.tmpPart, this.tmpStumpOffset, skull.position, skull.quaternion); } else if (injury === "dismember_left" || injury === "dismember_right") { const side = injury === "dismember_left" ? "left" : "right"; const pose = this.handWorldPose(index, side); if (pose !== null) this.effects.anchorHandStump(index, side, pose.position, pose.quaternion); + } else if (injury === "ribs_left" || injury === "ribs_right") { + const body = this.graphs?.[index]?.boxer.bodyInjury; + if (body !== undefined) caveInRibs(body, injury === "ribs_left" ? "left" : "right"); } } const ax = this.mapping.x(a.x); @@ -1427,34 +2524,17 @@ export class FightRenderer { const bx = this.mapping.x(b.x); const bz = this.mapping.z(b.y); separation = Math.hypot(ax - bx, az - bz); - knockdown = a.is_downed || b.is_downed; - this.ring.setRopeContacts({ x: ax, z: az }, { x: bx, z: bz }); + // Over a count the broadcast frames the man on the canvas; the result has its own shots (raiseFrame). + knockdown = snapshot.result === null && (a.is_downed || b.is_downed); + const fallenA = this.graphs?.[0]?.fallBody?.centre(this.bodyPoint) ?? null; + const contactA = fallenA === null ? { x: ax, z: az } : { x: fallenA.x, z: fallenA.z }; + const fallenB = this.graphs?.[1]?.fallBody?.centre(this.bodyPoint) ?? null; + const contactB = fallenB === null ? { x: bx, z: bz } : { x: fallenB.x, z: fallenB.z }; + downedAt = !knockdown ? null : a.is_downed ? contactA : contactB; + this.ring.setRopeContacts(contactA, contactB); this.tmpA.set(ax, 0, az); this.tmpB.set(bx, 0, bz); - for (const [index, fighter] of snapshot.fighters.entries()) { - const severity = (fighter.trauma.bleeding + fighter.trauma.left_cut + fighter.trauma.right_cut) / 380; - if (severity > 0.05 && !fighter.is_downed) { - this.downedPoolAccumulators[index] = 0; - const anchor = index === 0 ? this.tmpA : this.tmpB; - this.tmpHead.set(anchor.x, this.headHeightOf(index), anchor.z); - this.effects.drip(this.tmpHead, severity, current.reducedMotion, index); - } else if (severity > 0.3 && fighter.is_downed && current.blood !== "off") { - this.effects.stopDrip(index); - this.downedPoolAccumulators[index]! += dt * 0.8; - while (this.downedPoolAccumulators[index]! >= 1) { - this.downedPoolAccumulators[index]! -= 1; - const count = this.downedPoolCounts[index]!; - this.downedPoolCounts[index] = count + 1; - const angle = count * 2.399_963 + index * Math.PI; - const radius = 0.12 + ((count * 0.618_034) % 1) * 0.24; - const anchor = index === 0 ? this.tmpA : this.tmpB; - this.effects.pool(anchor.x + Math.sin(angle) * radius, anchor.z + Math.cos(angle) * radius, 0.8); - } - } else { - this.effects.stopDrip(index); - this.downedPoolAccumulators[index] = 0; - } - } + this.bleed(snapshot.fighters, dt, current); } else { this.tmpA.set(-0.9, 0, 0); this.tmpB.set(0.9, 0, 0); @@ -1482,10 +2562,25 @@ export class FightRenderer { knockdown, this.effects.shakeAmount, current.reducedMotion, + current.camera === "close", + downedAt, ); + const viewerIndex = snapshot === null ? -1 : snapshot.fighters.findIndex((fighter) => fighter.player_id === this.viewerId); + const ownView = current.camera === "fighter" && viewerIndex >= 0 && ownViewPhase(snapshot) + ? this.fighterCam.update(dt, seconds, viewerIndex === 0 ? this.tmpA : this.tmpB, viewerIndex === 0 ? this.tmpB : this.tmpA, this.effects.shakeAmount, current.reducedMotion, this.camera.aspect) + : null; + if (ownView === null) this.fighterCam.reset(); const replaying = this.replay; - const frame: { position: THREE.Vector3; lookAt: THREE.Vector3; tight?: boolean } = this.cameraOverride ?? (replaying !== null && snapshot !== null ? this.replayFrame(snapshot, seconds - replaying.startedAt) : (this.closeUpFrame(seconds) ?? this.cornerShotFrame(seconds, snapshot, current.reducedMotion) ?? directed)); - if (this.cameraOverride === null && this.portraitPull > 1) { + const frame: { position: THREE.Vector3; lookAt: THREE.Vector3; tight?: boolean; framed?: boolean } = this.cameraOverride ?? (replaying !== null && snapshot !== null ? this.replayFrame(snapshot, seconds - replaying.startedAt) : (this.closeUpFrame(seconds) ?? this.raiseFrame(seconds, dt, current.reducedMotion) ?? this.cornerShotFrame(seconds, snapshot, current.reducedMotion) ?? this.restWideFrame(snapshot) ?? this.ceremonyFrame(seconds) ?? ownView ?? directed)); + this.ownViewActive = ownView !== null && frame === ownView; + const fov = this.ownViewActive ? this.baseFov * FIGHTER_CAM_FOV_SCALE : this.baseFov; + if (this.camera.fov !== fov) { + this.camera.fov = fov; + this.camera.updateProjectionMatrix(); + } + // The player's own camera frames itself for the screen (FighterCam) and may stand over the apron, so it + // is neither pulled back nor kept inside the ropes like the broadcast's shots. + if (this.cameraOverride === null && this.portraitPull > 1 && frame.framed !== true && !this.ownViewActive) { const tight = frame.tight === true; const pull = this.portraitPull / Math.min(1.3, Math.sqrt(this.portraitPull)); const distanceScale = tight ? Math.min(pull, 1.25) : pull; @@ -1500,6 +2595,11 @@ export class FightRenderer { this.camera.position.copy(frame.position); this.camera.lookAt(frame.lookAt); } + this.effects.setViewDistance(this.camera.position.distanceTo(frame.lookAt)); + this.arena.makeRoomForCamera(this.camera.position); + // The broadcast camera and the announcement look through the near ropes; every other shot is from inside them. + const solid = frame === directed ? nearRopeOpacityFor(Math.max(this.tmpA.z, this.tmpB.z)) : frame.framed === true ? ANNOUNCEMENT_ROPE_OPACITY : 1; + this.ring.setNearRopeOpacity(this.ring.nearRopeOpacity() + (solid - this.ring.nearRopeOpacity()) * (1 - Math.exp(-6 * dt))); if (render) { this.composer.render(); @@ -1515,9 +2615,11 @@ export class FightRenderer { const held = this.localInput() ?? { moveX: 0, moveY: 0, defense: "none" }; // Shown as far ahead of the snapshot on screen as an input pressed now takes to show up in it: the // round trip plus the interpolation delay. The player's own punch starts here before the server - // has it, and holds the feet from then on. + // has it, and holds the feet from then on. A body shot that is putting the player's own fighter down + // freezes him until he drops. const leadTicks = ((this.inputLatencyMs ?? 60) / 1000) * this.simulation.tick_rate + this.buffer.interpolationDelayTicks; - const fighting = index >= 0 && snapshot.phase === "fight" ? snapshot.fighters[index]! : null; + const collapsing = this.ownCollapseUntil > snapshot.tick; + const fighting = index >= 0 && snapshot.phase === "fight" && !collapsing ? snapshot.fighters[index]! : null; const holdFeet = this.graphs?.[index]?.ownPunchActive === true || this.evasion.holdsFeet(timeMs, this.simulation.tick_rate); const local = this.movement.update(fighting, held, holdFeet, timeMs, leadTicks, snapshot.tick, dt, this.simulation.tick_rate); // The guard is the most latency-sensitive thing the player controls: it is shown as held, and a @@ -1533,19 +2635,194 @@ export class FightRenderer { return { ...snapshot, fighters }; } + /** The newest snapshot may already carry a punch the delayed clock has not reached; a fighter on the canvas throws none. */ + private anticipatePunches(latest: EngineSnapshot | null, sampledTick: number): void { + if (this.replay !== null || latest === null || latest.phase !== "fight") return; + for (const [index, graph] of (this.graphs ?? []).entries()) { + const ahead = latest.fighters[index]; + if (ahead !== undefined && !ahead.is_downed) graph.anticipate(ahead, ahead.action_start_tick - sampledTick); + } + } + + /** Keeps the crowd on its feet for a while after the result. */ + private cheer(seconds: number, dt: number): void { + if (seconds < this.ovationUntil) this.arena.excite(dt * CROWD_OVATION_RATE); + } + + /** Gives the fighter in a seat the look of the player who holds it. */ + private wearLook(index: 0 | 1, playerId: string): void { + if (this.lookIds[index] === playerId) return; + this.lookIds[index] = playerId; + this.graphs?.[index]?.boxer.setLook(lookFor(playerId)); + } + + /** + * The fighters as drawn: apart when the engine has them closer than two bodies can stand, and off a + * fighter lying on the canvas (in the replay as live). Each is eased toward where he should be drawn, at + * no more than a walk, rather than jumped there. + */ + private standApart(fighters: readonly [FighterSnapshot, FighterSnapshot], dt: number): readonly [FighterSnapshot, FighterSnapshot] { + const [a, b] = fighters; + const tied = [a, b].some((fighter) => fighter.clinch_ticks > 0 || fighter.clinch_startup_ticks > 0 || fighter.is_downed); + const apart = tied ? null : visualSeparation(a.x, a.y, b.x, b.y, DRAWN_MINIMUM_GAP, RING_HALF_WIDTH - FIGHTER_RADIUS, RING_HALF_HEIGHT - FIGHTER_RADIUS, RING_CORNER_REACH); + const [targetA, targetB] = this.drawnTargets; + targetA.x = apart === null ? 0 : apart.ax - a.x; + targetA.y = apart === null ? 0 : apart.ay - a.y; + targetB.x = apart === null ? 0 : apart.bx - b.x; + targetB.y = apart === null ? 0 : apart.by - b.y; + this.clearOfTheFallen(fighters); + const rate = 1 - Math.exp(-DRAWN_SHIFT_RATE * dt); + for (const seat of [0, 1] as const) { + const offset = this.drawnOffsets[seat]; + const target = this.drawnTargets[seat]; + let stepX = target.x - offset.x; + let stepY = target.y - offset.y; + if (this.drawnOffsetsLive) { + stepX *= rate; + stepY *= rate; + const step = Math.hypot(stepX, stepY); + if (step > DRAWN_SHIFT_SPEED * dt) { + stepX *= (DRAWN_SHIFT_SPEED * dt) / step; + stepY *= (DRAWN_SHIFT_SPEED * dt) / step; + } + } + offset.x += stepX; + offset.y += stepY; + } + this.drawnOffsetsLive = true; + return [this.drawnAt(0, a), this.drawnAt(1, b)]; + } + + /** A fighter where he is drawn this frame: his engine place and his offset, inside the ropes and corner pads. */ + private drawnAt(seat: 0 | 1, fighter: FighterSnapshot): FighterSnapshot { + const offset = this.drawnOffsets[seat]; + if (Math.abs(offset.x) < 0.01 && Math.abs(offset.y) < 0.01) return fighter; + const inside = ringPoint(fighter.x + offset.x, fighter.y + offset.y); + return Object.assign(this.drawnFighters[seat], fighter, { x: inside.x, y: inside.y }); + } + + /** + * Whoever is still on his feet stands off the body of a fighter lying where the fall took him, never in it, + * and inside the ropes: his drawn target moves by as much. + */ + private clearOfTheFallen(fighters: readonly [FighterSnapshot, FighterSnapshot]): void { + for (const lying of [0, 1] as const) { + const body = this.graphs?.[lying]?.fallBody ?? null; + const seat = lying === 0 ? 1 : 0; + const standing = fighters[seat]; + if (body === null || standing.is_downed || (this.graphs?.[seat]?.fallBody ?? null) !== null) continue; + const target = this.drawnTargets[seat]; + const fromX = this.mapping.x(standing.x + target.x); + const fromZ = this.mapping.z(standing.y + target.y); + const clear = this.clearOf(body, fromX, fromZ); + target.x += (clear.x - fromX) / this.mapping.x(1); + target.y += (clear.z - fromZ) / this.mapping.z(1); + return; + } + } + + /** + * The place nearest (x, z) at least STANDING_BODY_CLEARANCE from every point of a body on the canvas, and + * inside the ropes: pushed off the body, then, where the ropes stop the push, slid along them. + */ + private clearOf(body: Pick, "bodyPoint">, x: number, z: number): { x: number; z: number } { + let pushed = { x, z }; + for (const point of [0, 1, 2, 3, 4] as const) { + body.bodyPoint(point, this.bodyPoint); + pushed = keepClear(pushed.x, pushed.z, this.bodyPoint.x, this.bodyPoint.z, STANDING_BODY_CLEARANCE); + } + const inside = this.insideRopes(pushed.x, pushed.z); + if (Math.hypot(inside.x - pushed.x, inside.z - pushed.z) < 1e-6) return pushed; + const clear = (at: { x: number; z: number }): boolean => ([0, 1, 2, 3, 4] as const).every((point) => { + body.bodyPoint(point, this.bodyPoint); + return Math.hypot(at.x - this.bodyPoint.x, at.z - this.bodyPoint.z) >= STANDING_BODY_CLEARANCE - 1e-6; + }); + for (let slid = ROPE_SLIDE_STEP; slid <= ROPE_SLIDE_LIMIT + 1e-9; slid += ROPE_SLIDE_STEP) { + for (const [alongX, alongZ] of [[1, 0], [-1, 0], [0, 1], [0, -1]] as const) { + const along = this.insideRopes(inside.x + alongX * slid, inside.z + alongZ * slid); + if (clear(along)) return along; + } + } + return inside; + } + + /** The nearest place inside the ropes and corner pads to (x, z), in world space. */ + private insideRopes(x: number, z: number): { x: number; z: number } { + const inside = ringPoint(x / this.mapping.x(1), z / this.mapping.z(1)); + return { x: this.mapping.x(inside.x), z: this.mapping.z(inside.y) }; + } + private headHeightOf(index: number): number { return (this.graphs?.[index]?.boxer.metrics.headRestY ?? 1.52) + 0.04; } + /** + * Blood from a fighter's wounds: dripping from his face while he is up, and pooling under him once he is + * down (see fighterDown). A finisher's open wound pools whatever the engine says of his cuts, faster and + * wider, from the wound itself. + */ + private bleed(fighters: readonly FighterSnapshot[], dt: number, current: Settings): void { + for (const [index, fighter] of fighters.entries()) { + const down = fighterDown(fighter, this.graphs?.[index], this.flashKnockout?.loserId); + const wound = this.arcadeInjuries[index] ?? null; + const open = wound !== null && OPEN_WOUNDS.has(wound); + const engine = (fighter.trauma.bleeding + fighter.trauma.left_cut + fighter.trauma.right_cut) / 380; + const severity = open ? Math.max(engine, OPEN_WOUND_SEVERITY) : engine; + if (severity > 0.05 && !down) { + this.downedPoolAccumulators[index] = 0; + this.downedPoolCounts[index] = 0; + this.effects.drip(this.dripPoint(index), severity, current.reducedMotion, index); + } else if (severity > POOL_SEVERITY && down && current.blood !== "off") { + // A pool spreads from under his head through the count, or from the wound a finisher left. + this.effects.stopDrip(index); + this.downedPoolAccumulators[index]! += dt * POOL_RATE * (open ? OPEN_WOUND_POOL_RATE : 1); + const source = this.poolSource(index, wound); + while (this.downedPoolAccumulators[index]! >= 1) { + this.downedPoolAccumulators[index]! -= 1; + const count = this.downedPoolCounts[index]!; + this.downedPoolCounts[index] = count + 1; + const spread = poolRadius(count, severity, open ? OPEN_WOUND_POOL_REACH : POOL_REACH); + const angle = count * 2.399_963 + index * Math.PI; + this.effects.pool(source.x + Math.sin(angle) * spread * 0.25, source.z + Math.cos(angle) * spread * 0.25, spread, count + index * 7); + } + } else { + this.effects.stopDrip(index); + this.downedPoolAccumulators[index] = 0; + if (!down) this.downedPoolCounts[index] = 0; + } + } + } + + /** Where blood drips from a fighter's face: under his chin as his head is drawn, standing, seated or slipping. */ + private dripPoint(index: number): THREE.Vector3 { + const head = this.headWorldPose(index); + if (head !== null) return head.position.add(this.tmpStumpOffset.copy(UNDER_THE_CHIN).applyQuaternion(head.quaternion)); + if (this.headCacheValid[index]) return this.tmpHead.copy(this.headCache[index]!); + const anchor = index === 0 ? this.tmpA : this.tmpB; + return this.tmpHead.set(anchor.x, this.headHeightOf(index), anchor.z); + } + + /** Where a downed fighter's blood pools from: a severed hand's wrist, otherwise his head or what is left of his neck. */ + private poolSource(index: number, wound: ArcadeInjury | null): { readonly x: number; readonly z: number } { + if (wound === "dismember_left" || wound === "dismember_right") { + const wrist = this.handWorldPose(index, wound === "dismember_left" ? "left" : "right"); + if (wrist !== null) return wrist.position; + } + return this.headCacheValid[index] ? this.headCache[index]! : index === 0 ? this.tmpA : this.tmpB; + } + private updateBlobShadows(): void { const anchors = [this.tmpA, this.tmpB, this.refereePosition]; for (const [index, blob] of this.blobShadows.entries()) { - // A fighter's blob sits under the rendered fighter, who walks in from his mark for the glove touch. + // A fighter's blob sits under his body on the canvas, or under the rendered fighter, who walks in from + // his mark for the glove touch. const graph = index < 2 ? this.graphs?.[index] : undefined; - const anchor = graph?.boxer.root.visible === true ? graph.currentRoot : anchors[index]!; + const fallen = graph?.fallBody?.centre(this.bodyPoint) ?? null; + const anchor = fallen ?? (graph?.boxer.root.visible === true ? graph.currentRoot : anchors[index]!); blob.position.x = anchor.x; blob.position.z = anchor.z; - const downed = index < 2 && this.buffer.latest()?.fighters[index]?.is_downed === true; + const fighter = index < 2 ? this.buffer.latest()?.fighters[index] : undefined; + const downed = fighter !== undefined && fighterDown(fighter, graph, this.flashKnockout?.loserId); blob.scale.set(downed ? 2.1 : 1.25, downed ? 0.9 : 0.85, 1); } } @@ -1553,11 +2830,24 @@ export class FightRenderer { private updateReferee(dt: number, time: number, snapshot: EngineSnapshot | null, sampledTick: number): void { const referee = this.referee; if (referee === null) return; - const downed = snapshot?.fighters.find((fighter) => fighter.is_downed) ?? null; + // The replay isolates the two fighters: the referee, who would stand in its close shot, is left out of it. + referee.boxer.root.visible = this.replay === null; + const shadow = this.blobShadows[2]; + if (shadow !== undefined) shadow.visible = this.replay === null; + // He counts over the fighter who went down from the knockdown until "Box!": one who beats the count is + // still counted to the mandatory eight, on his feet, and looked over. + const counted = snapshot?.fighters.find((fighter) => fighter.is_downed) + ?? (snapshot?.phase === "knockdown" ? snapshot.fighters.find((fighter) => fighter.player_id === this.lastKnockdown?.knockdown.target_id) : undefined) + ?? null; const clinched = snapshot?.fighters.some((fighter) => fighter.clinch_ticks > 0 || fighter.clinch_startup_ticks > 0) ?? false; - const breaking = downed === null && referee.breaking; - const focusX = downed !== null ? this.mapping.x(downed.x) : (this.tmpA.x + this.tmpB.x) / 2; - const focusZ = downed !== null ? this.mapping.z(downed.y) : (this.tmpA.z + this.tmpB.z) / 2; + const breaking = counted === null && referee.breaking; + // He counts over the fighter where he is: his body where the fall left it, or, once up (a fall the + // physics carried off is got up from where it lies), where he is drawn walking back to his place. + const countedGraph = counted === null ? undefined : this.graphs?.[snapshot?.fighters.indexOf(counted) ?? -1]; + const fallen = countedGraph?.fallBody?.centre(this.bodyPoint) ?? null; + const risen = counted !== null && !counted.is_downed ? countedGraph?.currentRoot ?? null : null; + const focusX = counted === null ? (this.tmpA.x + this.tmpB.x) / 2 : fallen?.x ?? risen?.x ?? this.mapping.x(counted.x); + const focusZ = counted === null ? (this.tmpA.z + this.tmpB.z) / 2 : fallen?.z ?? risen?.z ?? this.mapping.z(counted.y); const away = this.refereeAway.set(this.refereePosition.x - focusX, 0, this.refereePosition.z - focusZ); if (away.lengthSq() < 0.01) away.set(0, 0, -1); away.normalize(); @@ -1569,33 +2859,84 @@ export class FightRenderer { const side = acrossX * away.x + acrossZ * away.z >= 0 ? 1 : -1; if (across > 0.01) away.set((acrossX / across) * side, 0, (acrossZ / across) * side); } - const { standoff, clearance } = refereeSpacing(downed !== null, clinched, breaking); + const { standoff, clearance } = refereeSpacing(counted !== null, clinched, breaking); const targetX = THREE.MathUtils.clamp(focusX + away.x * standoff, -2.4, 2.4); const targetZ = THREE.MathUtils.clamp(focusZ + away.z * standoff, -2.4, 2.4); const previousX = this.refereePosition.x; const previousZ = this.refereePosition.z; - // A break is a brisk step in, never faster than REFEREE_BREAK_SPEED; otherwise he drifts into place. - const rate = 1 - Math.exp(-(breaking ? 8 : 1.6) * dt); - const pace = breaking ? Math.min(1, (REFEREE_BREAK_SPEED * dt) / Math.max(1e-6, Math.hypot(targetX - this.refereePosition.x, targetZ - this.refereePosition.z) * rate)) : 1; - this.refereePosition.x += (targetX - this.refereePosition.x) * rate * pace; - this.refereePosition.z += (targetZ - this.refereePosition.z) * rate * pace; - for (const fighter of [this.tmpA, this.tmpB]) { + const ceremony = this.replay === null && this.ceremony?.positions !== null ? this.ceremony : null; + // Once the fight is waved off, the referee goes to the winner's side: the side nearer the middle of the + // ring, or the first one clear of the beaten fighter and his body (see raiseSpot). + const winner = ceremony === null && this.replay === null && this.stoppageWinner >= 0 && this.frameSeconds >= this.stoppageRaiseAt + ? (this.stoppageWinner === 0 ? this.tmpA : this.tmpB) + : null; + const spot = winner === null ? null : (this.stoppageSpot ??= this.raiseSpot(winner, this.stoppageWinner === 0 ? this.tmpB : this.tmpA)); + const side = spot !== null && spot.x < 0 ? -1 : 1; + if (winner !== null && spot !== null) { + const step = ceremonyStep(this.refereePosition.x, this.refereePosition.z, { x: winner.x + spot.x, y: winner.z + spot.z }, CEREMONY_REFEREE_SPEED * dt); + this.refereePosition.x = step.x; + this.refereePosition.z = step.y; + } else if (ceremony !== null) { + // The referee walks to the mark between the fighters and turns to the camera. + const step = ceremonyStep(this.refereePosition.x, this.refereePosition.z, { x: CEREMONY_REFEREE.x, y: CEREMONY_REFEREE.z }, CEREMONY_REFEREE_SPEED * dt); + this.refereePosition.x = step.x; + this.refereePosition.z = step.y; + ceremony.refereeArrived = step.arrived; + } else { + // A break is a brisk step in, never faster than REFEREE_BREAK_SPEED; otherwise he drifts into place. + const rate = 1 - Math.exp(-(breaking ? 8 : 1.6) * dt); + const pace = breaking ? Math.min(1, (REFEREE_BREAK_SPEED * dt) / Math.max(1e-6, Math.hypot(targetX - this.refereePosition.x, targetZ - this.refereePosition.z) * rate)) : 1; + this.refereePosition.x += (targetX - this.refereePosition.x) * rate * pace; + this.refereePosition.z += (targetZ - this.refereePosition.z) * rate * pace; + } + const keepOff = winner !== null ? STOPPAGE_RAISE_SPACING * 0.85 : ceremony === null ? clearance : ceremony.refereeArrived ? 0 : CEREMONY_WALK_CLEARANCE; + for (const fighter of keepOff > 0 ? [this.tmpA, this.tmpB] : []) { const dx = this.refereePosition.x - fighter.x; const dz = this.refereePosition.z - fighter.z; const distance = Math.hypot(dx, dz); - if (distance < clearance && distance > 0.001) { - this.refereePosition.x = fighter.x + (dx / distance) * clearance; - this.refereePosition.z = fighter.z + (dz / distance) * clearance; + if (distance < keepOff && distance > 0.001) { + this.refereePosition.x = fighter.x + (dx / distance) * keepOff; + this.refereePosition.z = fighter.z + (dz / distance) * keepOff; } } + // Nor on a fighter lying where the fall took him. + for (const graph of ceremony !== null ? [] : this.graphs ?? []) { + const body = graph.fallBody; + if (body === null) continue; + for (const point of [0, 1, 2, 3, 4] as const) { + body.bodyPoint(point, this.bodyPoint); + const step = keepClear(this.refereePosition.x, this.refereePosition.z, this.bodyPoint.x, this.bodyPoint.z, BODY_CLEARANCE); + this.refereePosition.x = step.x; + this.refereePosition.z = step.z; + } + } + // Nobody stands over a head on the canvas. + for (const index of [0, 1]) { + if (!this.effects.severedHeadPosition(index, this.closeUpTarget)) continue; + const step = keepClear(this.refereePosition.x, this.refereePosition.z, this.closeUpTarget.x, this.closeUpTarget.z, HEAD_CLEARANCE); + this.refereePosition.x = step.x; + this.refereePosition.z = step.z; + } if (dt > 0) { this.refereeVelocity.set((this.refereePosition.x - previousX) / dt, 0, (this.refereePosition.z - previousZ) / dt); } - const yaw = Math.atan2(focusX - this.refereePosition.x, focusZ - this.refereePosition.z); + // Arrived is where the keep-offs above let him stand, not where his step would have put him. + const beside = winner !== null && spot !== null + && Math.hypot(this.refereePosition.x - winner.x - spot.x, this.refereePosition.z - winner.z - spot.z) <= RAISE_ARRIVED; + const walking = ceremony !== null && !ceremony.refereeArrived; + const yaw = winner !== null ? (beside ? 0 : Math.atan2(winner.x - this.refereePosition.x, winner.z - this.refereePosition.z)) + : ceremony === null ? Math.atan2(focusX - this.refereePosition.x, focusZ - this.refereePosition.z) + : walking ? Math.atan2(CEREMONY_REFEREE.x - this.refereePosition.x, CEREMONY_REFEREE.z - this.refereePosition.z) : 0; + if (winner !== null) { + // Facing the camera, the referee's left hand is the one on the +x side. + const gloves = this.graphs?.[this.stoppageWinner]?.boxer.rig.bones; + const wrist = beside && gloves !== undefined ? this.nearerGlove(gloves.gloveL, gloves.gloveR) : null; + referee.raise(side < 0 ? wrist : null, side > 0 ? wrist : null); + } const yawDelta = ((yaw - this.refereeYaw + Math.PI) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2) - Math.PI; this.refereeYaw += yawDelta * (1 - Math.exp(-(breaking ? 8 : 3) * dt)); const state = refereeSnapshot(this.refereePosition, this.refereeYaw, this.refereeVelocity, this.mapping); - referee.setRefereeCount(downed !== null, downed?.get_up_count ?? 0); + referee.setRefereeCount(counted !== null, counted?.get_up_count ?? 0); referee.aimBreak(this.tmpA, this.tmpB); referee.update(state.self, state.focus, dt, time, false, "off", sampledTick); } @@ -1611,7 +2952,7 @@ export class FightRenderer { continue; } graph.boxer.rig.bones[bone].getWorldPosition(this.trailGlove); - trail.update(this.trailGlove, dt, this.camera.position, !reducedMotion && this.replay === null); + trail.update(this.trailGlove, dt, this.camera.position, !reducedMotion && this.replay === null && !graph.isDown); } } } @@ -1620,7 +2961,7 @@ export class FightRenderer { private updateCornermen(dt: number, time: number, snapshot: EngineSnapshot | null, sampledTick: number): void { const cornermen = this.cornermen; if (cornermen === null) return; - const resting = snapshot?.phase === "rest"; + const resting = cornersAtWork(snapshot, this.simulation.tick_rate); for (const [index, graph] of cornermen.entries()) { const sign = index === 0 ? -1 : 1; const reach = resting ? CORNERMAN_WORK_DISTANCE : CORNERMAN_APRON_DISTANCE; @@ -1656,23 +2997,22 @@ export class FightRenderer { if (!wasVisible) last.copy(this.cornermanPosition); this.cornermanVelocity.copy(this.cornermanPosition).sub(last).divideScalar(Math.max(dt, 1e-3)); last.copy(this.cornermanPosition); - const travelling = next > 0.02 && next < 0.98; + const travelling = next > 0.02 && next < CUTMAN_IN_PLACE; const head = this.headCacheValid[index] ? this.headCache[index]! : null; const yaw = travelling ? Math.atan2(this.cutmanTo.x - this.cutmanFrom.x, this.cutmanTo.z - this.cutmanFrom.z) + (wanted === 1 ? 0 : Math.PI) - : next >= 0.98 + : next >= CUTMAN_IN_PLACE ? (head !== null ? Math.atan2(head.x - this.cutmanTo.x, head.z - this.cutmanTo.z) : Math.atan2(sign, -sign)) : Math.atan2(-this.cutmanFrom.x, -this.cutmanFrom.z); - const trauma = snapshot?.fighters[index]?.trauma; - const side = trauma !== undefined && trauma.right_eye + trauma.right_cut > trauma.left_eye + trauma.left_cut ? -1 : 1; - if (next >= 0.98 && head !== null) { + const work = cutmanWork(snapshot?.fighters[index]); + if (next >= CUTMAN_IN_PLACE && head !== null) { const fighterYaw = graphs[index]!.boxer.root.rotation.y; this.cutmanFacing.set(Math.sin(fighterYaw), 0, Math.cos(fighterYaw)); this.cutmanEye.copy(head).addScaledVector(this.cutmanFacing, 0.09); - this.cutmanEye.x += Math.cos(fighterYaw) * side * 0.035; - this.cutmanEye.z -= Math.sin(fighterYaw) * side * 0.035; - this.cutmanEye.y += 0.08; - graph.treat(this.cutmanEye, this.cutmanFacing, side); + this.cutmanEye.x += Math.cos(fighterYaw) * work.side * work.lateral; + this.cutmanEye.z -= Math.sin(fighterYaw) * work.side * work.lateral; + this.cutmanEye.y += work.lift; + graph.treat(this.cutmanEye, this.cutmanFacing, work.side, work.prop); } else { graph.treat(null); } @@ -1685,6 +3025,8 @@ export class FightRenderer { const resized = resizeHighDpi(this.hudCanvas); if (resized === null) return; const { context: ctx, viewport } = resized; + this.hudViewport.width = viewport.width; + this.hudViewport.height = viewport.height; ctx.clearRect(0, 0, viewport.width, viewport.height); if (snapshot === null) return; if (this.viewerHitFlash > 0.01 && !this.settings().reducedMotion) { @@ -1702,7 +3044,58 @@ export class FightRenderer { ctx.fillStyle = vignette; ctx.fillRect(0, 0, viewport.width, viewport.height); } - drawHud(ctx, viewport.width, viewport.height, snapshot, this.players, this.viewerId, this.frameSeconds >= this.finalRevealAt ? this.final : null, this.reconnectMs, this.simulation.tick_rate, this.roundStats, this.replay !== null ? "KNOCKOUT REPLAY" : null, this.inputLatencyMs, this.frameSeconds < this.roundCalloutUntil ? `ROUND ${this.roundCalloutRound}` : null, this.roundClock.ticks(snapshot)); + drawHud(ctx, viewport.width, viewport.height, snapshot, this.players, this.viewerId, this.frameSeconds >= this.finalRevealAt ? this.final : null, this.reconnectMs, this.simulation.tick_rate, this.roundStats, this.replay !== null ? "KNOCKOUT REPLAY" : null, this.lagWarned ? this.inputLatencyMs : null, this.frameSeconds < this.roundCalloutUntil ? `ROUND ${this.roundCalloutRound}` : this.eventCallout !== null && this.frameSeconds < this.eventCallout.until ? this.eventCallout.text : null, this.roundClock.ticks(snapshot), (player) => this.avatars.get(player), this.lastKnockdown?.knockdown.target_id ?? null, this.touchControls && snapshot.fighters.some((fighter) => fighter.player_id === this.viewerId)); + this.drawCaption(ctx, viewport.width, viewport.height, snapshot); + } + + /** + * Hurt vision while the viewer's own fighter is rocked; never for a spectator, in a replay or with reduced + * motion. `shown` is the snapshot on screen, so it starts with the punch that rocked him. Returns how rocked + * he is for the sound, or null for a spectator. + */ + private updateRocked(shown: EngineSnapshot | null, dt: number, reducedMotion: boolean): number | null { + const viewer = shown?.fighters.find((fighter) => fighter.player_id === this.viewerId); + const rocked = shown === null || this.replay !== null || this.final !== null ? 0 : rockedLevel(viewer, shown.phase); + const level = this.rocked.update(reducedMotion ? 0 : rocked, dt); + this.finishPass.uniforms.uRocked!.value = reducedMotion ? 0 : level; + return viewer === undefined ? null : rocked; + } + + /** The newest snapshot the render clock has reached: the fight as the screen has shown it so far. */ + private shownAt(tick: number): EngineSnapshot | null { + for (let index = this.history.length - 1; index >= 0; index -= 1) { + if (this.history[index]!.tick <= tick) return this.history[index]!; + } + return null; + } + + /** The broadcast caption: commentary, the ring announcer and the decision read-out. */ + private drawCaption(ctx: CanvasRenderingContext2D, width: number, height: number, snapshot: EngineSnapshot): void { + const final = this.final !== null && this.frameSeconds >= this.finalRevealAt ? this.final : null; + if (final !== null && !this.resultAnnounced) { + this.resultAnnounced = true; + this.commentary.resultShown(this.frameSeconds); + } + const caption = this.commentary.current(this.frameSeconds); + const settings = this.settings(); + const text = caption === null || !settings.commentary ? "" : caption.line.text; + if (text !== this.captionText) { + this.captionText = text; + this.hudCanvas.dataset.caption = text; + } + if (caption === null || !settings.commentary) return; + const viewer = snapshot.fighters.find((fighter) => fighter.player_id === this.viewerId); + // Laid out like the rest of the HUD for a 1280 x 720 screen and drawn larger on a bigger one. + const scale = hudScale(width, height); + const resultTop = final === null ? null : resultCardTop(final, width, height, snapshot.fighters, this.players, this.roundStats, this.viewerId) / scale; + const callout = this.frameSeconds < this.roundCalloutUntil || (this.eventCallout !== null && this.frameSeconds < this.eventCallout.until); + const cornerPanelTop = this.cornerPanelTop === null ? null : this.cornerPanelTop / scale; + const slot = captionSlot({ width: width / scale, height: height / scale, phase: snapshot.phase, resultTop, touch: this.touchControls && viewer !== undefined, hint: snapshot.phase === "countdown" && viewer !== undefined, viewerDown: viewer?.is_downed === true, replay: this.replay !== null, cornerPanelTop, callout, lag: this.lagWarned, paused: this.reconnectMs > 0 }); + if (slot === null) return; + ctx.save(); + ctx.scale(scale, scale); + drawCaption(ctx, caption, slot, settings.reducedMotion); + ctx.restore(); } destroy(): void { @@ -1714,6 +3107,7 @@ export class FightRenderer { this.dedupe.reset(); this.pendingContacts.length = 0; this.effects.dispose(); + this.avatars.dispose(); this.arena.dispose(); disposeRing(this.ring); if (this.graphs !== null) { @@ -1747,9 +3141,6 @@ export class FightRenderer { } this.cutmen = null; } - for (const shirt of this.cornerShirts) shirt.dispose(); - this.cornerShirts.length = 0; - this.refereeShirt?.dispose(); for (const light of this.lights) this.scene.remove(light); this.keyLight?.shadow.map?.dispose(); this.keyLight?.shadow.dispose(); diff --git a/web/hands/src/render/replay.test.ts b/web/hands/src/render/replay.test.ts index 60eaea94..15e3bfd2 100644 --- a/web/hands/src/render/replay.test.ts +++ b/web/hands/src/render/replay.test.ts @@ -12,6 +12,20 @@ const history = (from: number, to: number): EngineSnapshot[] => { const knockdown = (tick: number): CombatEvent => ({ event_id: 9, tick, kind: "knockdown", actor_id: "one", target_id: "two", amount: 1, detail: "", blood: 0, direction: 1, action_id: null }); describe("knockout replay plan", () => { + it("holds the last picture when the bout ended on the punch itself, and still needs the lead-up", () => { + const ended = history(40, 100); + const final = { ...ended.at(-1)!, phase: "complete" as const, events: [knockdown(100)], result: { match_id: "m", activity_instance_id: "i", guild_id: "g", player_one_id: "one", player_two_id: "two", winner_id: "one", finish_method: "tko" as const, round_number: 1, tick: 100, scorecards: [], player_one_knockdowns: 0, player_two_knockdowns: 3, player_one_damage: 1, player_two_damage: 1 } }; + ended[ended.length - 1] = final; + const plan = planKnockoutReplay(ended, knockdown(100)); + expect(plan).not.toBeNull(); + expect(plan!.toTick).toBe(100 + REPLAY_AFTER_TICKS); + const after = plan!.snapshots.filter((snapshot) => snapshot.tick > 100); + expect(after).toHaveLength(REPLAY_AFTER_TICKS); + for (const held of after) expect(held).toMatchObject({ phase: "complete", fighters: final.fighters, events: [] }); + expect(planKnockoutReplay(ended.slice(-5), knockdown(100))).toBeNull(); + expect(planKnockoutReplay(history(40, 100), knockdown(100))).toBeNull(); + }); + it("covers the ticks before and after the knockdown at replay speed", () => { const plan = planKnockoutReplay(history(0, 200), knockdown(100)); expect(plan).not.toBeNull(); diff --git a/web/hands/src/render/replay.ts b/web/hands/src/render/replay.ts index ada5b2af..c1e62ded 100644 --- a/web/hands/src/render/replay.ts +++ b/web/hands/src/render/replay.ts @@ -26,7 +26,13 @@ export function planKnockoutReplay( ): ReplayPlan | null { const wantedFrom = impact.tick - REPLAY_BEFORE_TICKS; const wantedTo = impact.tick + REPLAY_AFTER_TICKS; - const snapshots = history.filter((snapshot) => snapshot.tick >= wantedFrom - 1 && snapshot.tick <= wantedTo + 1); + const recorded = history.filter((snapshot) => snapshot.tick >= wantedFrom - 1 && snapshot.tick <= wantedTo + 1); + const ended = recorded.at(-1); + // A bout that ends on the punch itself (a flash knockout, a third knockdown) stops the recording at the + // impact, because the server stops; the replay holds that final picture while the fall plays out. + const snapshots = ended !== undefined && ended.result !== null && ended.tick < wantedTo + ? [...recorded, ...Array.from({ length: wantedTo - ended.tick }, (_, step) => ({ ...ended, tick: ended.tick + step + 1, events: [] }))] + : recorded; const first = snapshots[0]; const last = snapshots.at(-1); if (first === undefined || last === undefined || snapshots.length < 8) return null; diff --git a/web/hands/src/render/rig.ts b/web/hands/src/render/rig.ts index 9f5460ee..c6b10ec7 100644 --- a/web/hands/src/render/rig.ts +++ b/web/hands/src/render/rig.ts @@ -108,7 +108,7 @@ export class SolvedRig { readonly bones: Readonly>; readonly metrics: RigMetrics; private readonly restLocal = new Map(); - private readonly restLocalPosition = new Map(); + private readonly restPositions = new Map(); private readonly limb = { reached: false, joint: new THREE.Vector3() }; constructor(root: THREE.Object3D) { @@ -126,7 +126,7 @@ export class SolvedRig { root.updateMatrixWorld(true); for (const bone of found.values()) { this.restLocal.set(bone, bone.quaternion.clone()); - this.restLocalPosition.set(bone, bone.position.clone()); + this.restPositions.set(bone, bone.position.clone()); } const distance = (from: CanonicalBone, to: CanonicalBone): number => worldPosition(bones[from], scratchA).distanceTo(worldPosition(bones[to], scratchB)); @@ -152,7 +152,7 @@ export class SolvedRig { resetToRest(): void { for (const [bone, quaternion] of this.restLocal) { bone.quaternion.copy(quaternion); - const position = this.restLocalPosition.get(bone); + const position = this.restPositions.get(bone); if (position !== undefined) bone.position.copy(position); } } @@ -161,6 +161,10 @@ export class SolvedRig { return this.restLocal.get(bone) ?? bone.quaternion; } + restLocalPosition(bone: THREE.Bone): THREE.Vector3 { + return this.restPositions.get(bone) ?? bone.position; + } + position(bone: CanonicalBone, target: THREE.Vector3): THREE.Vector3 { return worldPosition(this.bones[bone], target); } diff --git a/web/hands/src/render/ring.ts b/web/hands/src/render/ring.ts index 9fb709d2..0245e411 100644 --- a/web/hands/src/render/ring.ts +++ b/web/hands/src/render/ring.ts @@ -1,5 +1,5 @@ import * as THREE from "three"; -import { CANVAS_TOP, CORNER_COLORS, PLATFORM_HALF, POST_RADIUS, RING_APRON_HALF, RING_FIGHT_HALF, ROPE_HEIGHTS } from "./world"; +import { CANVAS_TOP, CORNER_COLORS, PLATFORM_HALF, POST_RADIUS, RING_APRON_HALF, RING_FIGHT_HALF, ROPE_HEIGHTS, ROPE_LINE } from "./world"; export interface BuiltRing { readonly group: THREE.Group; @@ -9,6 +9,27 @@ export interface BuiltRing { /** Feeds the two fighters' world positions to the rope flex shader. */ readonly setRopeContacts: (a: { x: number; z: number } | null, b: { x: number; z: number } | null) => void; readonly ropeContacts: readonly [THREE.Vector4, THREE.Vector4]; + /** Fades the ropes between the broadcast camera and the ring so they do not hide the fighters. */ + readonly setNearRopeOpacity: (opacity: number) => void; + readonly nearRopeOpacity: () => number; +} + +function smoothstep(edge0: number, edge1: number, value: number): number { + const t = Math.min(1, Math.max(0, (value - edge0) / (edge1 - edge0))); + return t * t * (3 - 2 * t); +} + +/** The side of the ring that faces the broadcast camera. */ +const NEAR_SIDE = 3; +/** + * The near side's ropes and straps blend last, after the blob shadows (1) and the glove trails (4) seen + * through them, which would otherwise be cut where a faded rope crosses them. + */ +const NEAR_ROPE_RENDER_ORDER = 5; + +/** How solid the near ropes are drawn for fighters whose nearest point to the camera is `z` metres from the centre. */ +export function nearRopeOpacityFor(z: number): number { + return 1 - 0.74 * smoothstep(-0.6, 1.4, z); } export interface RopePress { @@ -16,23 +37,64 @@ export interface RopePress { readonly pressZ: number; } -const ROPE_PRESS_START = 0.6; -const ROPE_PRESS_FULL = 0.22; - -function smoothstep(edge0: number, edge1: number, value: number): number { - const t = Math.min(1, Math.max(0, (value - edge0) / (edge1 - edge0))); - return t * t * (3 - 2 * t); -} +/** The ropes run behind a fighter's back, this far from the middle of the body (his back, and his rear leg in the stance). */ +export const ROPE_BACK = 0.23; +/** + * The furthest the ropes give behind a fighter: well short of the 0.38 m between them, so they never wrap + * round him. The engine lets his middle reach 0.36 m past the rope line; the rest is made up by drawing + * him in and leaning him back on the ropes (see `ropeExcess`). + */ +export const ROPE_MAX_GIVE = 0.22; +/** Along the rope, the give is full across the back and gone this far from the fighter. */ +const ROPE_GIVE_FLAT = 0.22; +const ROPE_GIVE_REACH = 1.15; +/** The rope is tied at the post and gives nothing there. */ +const ROPE_TIE = 0.5; +/** The bottom rope is pushed by the legs: in the stance the rear one reaches almost as far back as the shoulders. */ +const ROPE_LOW_GIVE = 0.9; +/** The ropes sag this far in the middle of a side. */ +const ROPE_SAG = 0.045; +/** Where along each side the straps tie the ropes together. */ +const TIE_POSITIONS = [0.33, 0.66] as const; +/** How far the ropes have sagged at the ties, and how far past a rope's middle a strap wraps round it. */ +const TIE_SAG = ROPE_SAG * 2 * 0.33 * 0.67; +const TIE_WRAP = 0.04; -/** How hard a fighter at (x, z) leans into the x-facing and z-facing ropes (0..1 each). */ +/** + * How far, in metres, a fighter at (x, z) pushes the ropes on the x and z sides outward. The engine + * lets a fighter's middle pass the line of the ropes, so they give way to stay behind the back, as far + * as `ROPE_MAX_GIVE`. + */ export function ropePress(x: number, z: number): RopePress { return { - pressX: smoothstep(ROPE_PRESS_START, ROPE_PRESS_FULL, RING_FIGHT_HALF - Math.abs(x)), - pressZ: smoothstep(ROPE_PRESS_START, ROPE_PRESS_FULL, RING_FIGHT_HALF - Math.abs(z)), + pressX: Math.min(ROPE_MAX_GIVE, Math.max(0, Math.abs(x) + ROPE_BACK - ROPE_LINE)), + pressZ: Math.min(ROPE_MAX_GIVE, Math.max(0, Math.abs(z) + ROPE_BACK - ROPE_LINE)), }; } -const ROPE_FLEX_GLSL = ` +/** + * How far past the ropes' furthest give the engine has a fighter at (x, z), per axis and signed outward, + * in metres: he is drawn that much nearer the middle, his back on the ropes, and leans back on them. + */ +export function ropeExcess(x: number, z: number, out: { x: number; z: number }): { x: number; z: number } { + out.x = Math.sign(x) * Math.max(0, Math.abs(x) + ROPE_BACK - ROPE_LINE - ROPE_MAX_GIVE); + out.z = Math.sign(z) * Math.max(0, Math.abs(z) + ROPE_BACK - ROPE_LINE - ROPE_MAX_GIVE); + return out; +} + +/** + * The share of that push a point of the rope takes: `along` the rope from the fighter, at `position` + * along the rope and at `height`. It is the shader's own function, and a test runs this text. + */ +export const ROPE_GIVE_GLSL = ` +float ropeGive(float along, float position, float height) { + float near = 1.0 - smoothstep(${ROPE_GIVE_FLAT.toFixed(2)}, ${ROPE_GIVE_REACH.toFixed(2)}, abs(along)); + float tied = smoothstep(0.0, ${ROPE_TIE.toFixed(2)}, ${ROPE_LINE.toFixed(4)} - abs(position)); + return near * tied * (${ROPE_LOW_GIVE.toFixed(2)} + ${(1 - ROPE_LOW_GIVE).toFixed(2)} * smoothstep(0.5, 0.88, height)); +} +`; + +export const ROPE_FLEX_GLSL = ` vec3 ropeWorld = (modelMatrix * vec4(transformed, 1.0)).xyz; bool ropeSideX = abs(ropeWorld.x) > abs(ropeWorld.z); vec3 ropeOut = ropeSideX ? vec3(sign(ropeWorld.x), 0.0, 0.0) : vec3(0.0, 0.0, sign(ropeWorld.z)); @@ -43,11 +105,9 @@ for (int ropeIndex = 0; ropeIndex < 2; ropeIndex += 1) { float along = ropeSideX ? contact.y : contact.x; float press = ropeSideX ? contact.z : contact.w; float sameSide = ropeSideX ? step(0.0, ropeOut.x * contact.x) : step(0.0, ropeOut.z * contact.y); - ropeFlex += press * sameSide * (1.0 - smoothstep(0.0, 0.8, abs(ropeAlong - along))); + ropeFlex = max(ropeFlex, press * sameSide * ropeGive(ropeAlong - along, ropeAlong, ropeWorld.y)); } -float ropeAnchor = smoothstep(0.0, 0.7, ${RING_FIGHT_HALF.toFixed(2)} - abs(ropeAlong)); -float ropeHeightWeight = 0.45 + 0.55 * smoothstep(0.4, 1.0, ropeWorld.y); -transformed += ropeOut * min(1.0, ropeFlex) * 0.22 * ropeHeightWeight * ropeAnchor; +transformed += ropeOut * ropeFlex; `; function canvasTexture(size: number, draw: (ctx: CanvasRenderingContext2D, size: number) => void): THREE.CanvasTexture { @@ -97,7 +157,7 @@ function ringCanvasTexture(): THREE.CanvasTexture { ctx.textBaseline = "middle"; ctx.fillText("H A N D S", size / 2, size / 2 - size * 0.012); ctx.font = `600 ${Math.round(size * 0.024)}px Inter, system-ui, sans-serif`; - ctx.fillText("AUTHORITATIVE BOXING", size / 2, size / 2 + size * 0.052); + ctx.fillText("CHAMPIONSHIP BOXING", size / 2, size / 2 + size * 0.052); ctx.strokeStyle = "rgba(30,60,140,0.6)"; ctx.lineWidth = 6; ctx.strokeRect(size * 0.035, size * 0.035, size * 0.93, size * 0.93); @@ -157,7 +217,8 @@ export function buildRing(): BuiltRing { const steelMat = new THREE.MeshStandardMaterial({ color: "#9aa7b5", roughness: 0.35, metalness: 0.75 }); materials.push(steelMat); - const cornerColors = [CORNER_COLORS.blue, CORNER_COLORS.neutral, CORNER_COLORS.red, CORNER_COLORS.neutral]; + // Front right, back right, back left, front left: the blue corner is where seat one rests, the red where seat two does. + const cornerColors = [CORNER_COLORS.neutral, CORNER_COLORS.red, CORNER_COLORS.neutral, CORNER_COLORS.blue]; const postGeo = new THREE.CylinderGeometry(0.055, 0.055, 1.55, 12); geometries.push(postGeo); const padGeo = new THREE.BoxGeometry(0.34, 0.52, 0.13); @@ -188,18 +249,37 @@ export function buildRing(): BuiltRing { const ropeContacts: [THREE.Vector4, THREE.Vector4] = [new THREE.Vector4(0, 0, 0, 0), new THREE.Vector4(0, 0, 0, 0)]; const ropeUniforms = { uRopeContactA: { value: ropeContacts[0] }, uRopeContactB: { value: ropeContacts[1] } }; const ropeColors = [0xb91c1c, 0xe5e7eb, 0x1d4ed8]; - const ropeMats = ropeColors.map((color) => { - const material = new THREE.MeshStandardMaterial({ color, roughness: 0.42, metalness: 0.05 }); - material.onBeforeCompile = (shader) => { - shader.uniforms.uRopeContactA = ropeUniforms.uRopeContactA; - shader.uniforms.uRopeContactB = ropeUniforms.uRopeContactB; - shader.vertexShader = shader.vertexShader - .replace("#include ", "#include \nuniform vec4 uRopeContactA;\nuniform vec4 uRopeContactB;") - .replace("#include ", `#include ${ROPE_FLEX_GLSL}`); - }; + const bendRopes = (shader: THREE.WebGLProgramParametersWithUniforms): void => { + shader.uniforms.uRopeContactA = ropeUniforms.uRopeContactA; + shader.uniforms.uRopeContactB = ropeUniforms.uRopeContactB; + shader.vertexShader = shader.vertexShader + .replace("#include ", `#include \nuniform vec4 uRopeContactA;\nuniform vec4 uRopeContactB;\n${ROPE_GIVE_GLSL}`) + .replace("#include ", `#include ${ROPE_FLEX_GLSL}`); + }; + const flexMaterial = (parameters: THREE.MeshStandardMaterialParameters): THREE.MeshStandardMaterial => { + const material = new THREE.MeshStandardMaterial(parameters); + material.onBeforeCompile = bendRopes; materials.push(material); return material; - }); + }; + // The key light's shadow pass draws a mesh with three's own depth material unless it has one: the ropes' + // bends them as they give, so their shadows move with them off a fighter pinned on them. + const ropeDepth = new THREE.MeshDepthMaterial(); + ropeDepth.onBeforeCompile = bendRopes; + ropeDepth.customProgramCacheKey = () => "hands-rope-depth"; + materials.push(ropeDepth); + const ropeMaterial = (color: number): THREE.MeshStandardMaterial => flexMaterial({ color, roughness: 0.42, metalness: 0.05 }); + const ropeMats = ropeColors.map(ropeMaterial); + const nearMaterials: THREE.MeshStandardMaterial[] = ropeColors.map(ropeMaterial); + // The straps tie the ropes together, so they give with them where a fighter presses into the ropes: one + // strap at each tie, from under the bottom rope to over the top one, in enough pieces to bend with them. + const tieMat = flexMaterial({ color: 0xd8dee8, roughness: 0.6 }); + const nearTieMat = flexMaterial({ color: 0xd8dee8, roughness: 0.6 }); + nearMaterials.push(nearTieMat); + const tieLow = ROPE_HEIGHTS[0] - TIE_SAG - TIE_WRAP; + const tieHigh = ROPE_HEIGHTS[ROPE_HEIGHTS.length - 1]! - TIE_SAG + TIE_WRAP; + const tieGeo = new THREE.BoxGeometry(0.035, tieHigh - tieLow, 0.012, 1, 6, 1); + geometries.push(tieGeo); const setRopeContacts = (a: { x: number; z: number } | null, b: { x: number; z: number } | null): void => { for (const [index, contact] of [a, b].entries()) { const target = ropeContacts[index]!; @@ -216,32 +296,45 @@ export function buildRing(): BuiltRing { const to = corners[(side + 1) % 4]!; for (const [ropeIndex, height] of ROPE_HEIGHTS.entries()) { const middle = from.clone().add(to).multiplyScalar(0.5); - middle.y = height - 0.045; + middle.y = height - ROPE_SAG; const curve = new THREE.QuadraticBezierCurve3( new THREE.Vector3(from.x, height, from.z), middle, new THREE.Vector3(to.x, height, to.z), ); - const ropeGeo = new THREE.TubeGeometry(curve, 24, 0.028, 8, false); + // Enough segments for the rope to curve round a fighter's back rather than kink. + const ropeGeo = new THREE.TubeGeometry(curve, 72, 0.028, 8, false); geometries.push(ropeGeo); - const rope = new THREE.Mesh(ropeGeo, ropeMats[ropeIndex]!); + const rope = new THREE.Mesh(ropeGeo, (side === NEAR_SIDE ? nearMaterials : ropeMats)[ropeIndex]!); rope.castShadow = true; + rope.customDepthMaterial = ropeDepth; + if (side === NEAR_SIDE) rope.renderOrder = NEAR_ROPE_RENDER_ORDER; group.add(rope); - for (const t of [0.33, 0.66]) { - const point = curve.getPoint(t); - const tieGeo = new THREE.BoxGeometry(0.035, 0.82, 0.012); - geometries.push(tieGeo); - const tieMat = new THREE.MeshStandardMaterial({ color: 0xd8dee8, roughness: 0.6 }); - materials.push(tieMat); - const tie = new THREE.Mesh(tieGeo, tieMat); - tie.position.set(point.x, height - 0.36, point.z); - tie.lookAt(0, height - 0.36, 0); - group.add(tie); - } + } + for (const t of TIE_POSITIONS) { + const x = from.x + (to.x - from.x) * t; + const z = from.z + (to.z - from.z) * t; + const tie = new THREE.Mesh(tieGeo, side === NEAR_SIDE ? nearTieMat : tieMat); + tie.position.set(x, (tieLow + tieHigh) / 2, z); + tie.lookAt(0, tie.position.y, 0); + if (side === NEAR_SIDE) tie.renderOrder = NEAR_ROPE_RENDER_ORDER; + group.add(tie); } } - return { group, materials, geometries, textures, setRopeContacts, ropeContacts }; + for (const material of nearMaterials) material.transparent = true; + const setNearRopeOpacity = (opacity: number): void => { + for (const material of nearMaterials) { + material.opacity = THREE.MathUtils.clamp(opacity, 0, 1); + // Faded right out they are not drawn at all, or they would still write depth and catch the bloom. + material.visible = material.opacity > 0.03; + // Faded, they blend over what is behind them without cutting it out of anything drawn later. Depth + // writing is render state, not part of the shader, so switching it compiles nothing. + material.depthWrite = material.opacity >= 1; + } + }; + + return { group, materials, geometries, textures, setRopeContacts, ropeContacts, setNearRopeOpacity, nearRopeOpacity: () => nearMaterials[0]!.opacity }; } export function disposeRing(ring: BuiltRing): void { diff --git a/web/hands/src/render/rocked.test.ts b/web/hands/src/render/rocked.test.ts new file mode 100644 index 00000000..e0860868 --- /dev/null +++ b/web/hands/src/render/rocked.test.ts @@ -0,0 +1,37 @@ +import { ROCKED_BASE_TICKS, ROCKED_MAX_TICKS } from "../manifest"; +import { fighter } from "../test/fixtures"; +import { ROCKED_DOWN, RockedVision, rockedLevel } from "./rocked"; + +describe("hurt vision", () => { + it("follows how long the fighter is stunned and how little poise is left", () => { + expect(rockedLevel(undefined, "fight")).toBe(0); + expect(rockedLevel(fighter("one"), "fight")).toBe(0); + expect(rockedLevel({ ...fighter("one"), stunned_ticks: ROCKED_MAX_TICKS }, "fight")).toBe(1); + expect(rockedLevel({ ...fighter("one"), stunned_ticks: 90 }, "fight")).toBe(1); + expect(rockedLevel({ ...fighter("one"), stunned_ticks: ROCKED_BASE_TICKS }, "fight")).toBeCloseTo(ROCKED_BASE_TICKS / ROCKED_MAX_TICKS); + expect(rockedLevel({ ...fighter("one"), poise: 75 }, "fight")).toBeCloseTo(0.3); + expect(rockedLevel({ ...fighter("one"), poise: 75, stunned_ticks: 30 }, "fight")).toBeCloseTo(30 / ROCKED_MAX_TICKS); + }); + + it("leaves a flinch from an ordinary clean shot alone", () => { + expect(rockedLevel({ ...fighter("one"), stunned_ticks: 9 }, "fight")).toBe(0); + expect(rockedLevel({ ...fighter("one"), stunned_ticks: ROCKED_BASE_TICKS - 1 }, "fight")).toBe(0); + }); + + it("is strongest on the canvas and gone once the bout is not live", () => { + expect(rockedLevel({ ...fighter("one"), is_downed: true, poise: 0 }, "knockdown")).toBe(ROCKED_DOWN); + for (const phase of ["countdown", "rest", "foul_recovery", "complete"] as const) expect(rockedLevel({ ...fighter("one"), stunned_ticks: 60, is_downed: true }, phase)).toBe(0); + }); + + it("comes on at once and clears slowly", () => { + const vision = new RockedVision(); + let level = 0; + for (let frame = 0; frame < 18; frame += 1) level = vision.update(1, 1 / 60); + expect(level).toBeGreaterThan(0.85); + for (let frame = 0; frame < 60; frame += 1) level = vision.update(0, 1 / 60); + expect(level).toBeGreaterThan(0.35); + for (let frame = 0; frame < 600; frame += 1) level = vision.update(0, 1 / 60); + expect(level).toBe(0); + expect(vision.update(0.5, 0)).toBe(0); + }); +}); diff --git a/web/hands/src/render/rocked.ts b/web/hands/src/render/rocked.ts new file mode 100644 index 00000000..b284f94b --- /dev/null +++ b/web/hands/src/render/rocked.ts @@ -0,0 +1,35 @@ +import { ROCKED_BASE_TICKS, ROCKED_MAX_TICKS } from "../manifest"; +import type { FighterSnapshot, MatchPhase } from "../types"; + +/** Stun at which the hurt vision is at full strength: the longest a big head shot rocks a man. */ +export const ROCKED_STUN_TICKS = ROCKED_MAX_TICKS; +/** Below this poise a fighter is hanging on even when not stunned. */ +export const ROCKED_POISE = 150; +/** On the canvas during a count. */ +export const ROCKED_DOWN = 0.9; + +/** How rocked the viewer's own fighter is, 0 to 1, while the bout is live. */ +export function rockedLevel(fighter: FighterSnapshot | undefined, phase: MatchPhase): number { + if (fighter === undefined || (phase !== "fight" && phase !== "knockdown")) return 0; + if (fighter.is_downed) return ROCKED_DOWN; + // A flinch from an ordinary clean shot is over before it could blur anything. + const stunned = fighter.stunned_ticks < ROCKED_BASE_TICKS ? 0 : Math.min(1, fighter.stunned_ticks / ROCKED_STUN_TICKS); + const shaky = fighter.poise < ROCKED_POISE ? ((ROCKED_POISE - fighter.poise) / ROCKED_POISE) * 0.6 : 0; + return Math.max(stunned, shaky); +} + +/** Eases the hurt vision in at once and lets it go slowly, as a fighter clears the head. */ +export class RockedVision { + private value = 0; + + get level(): number { + return this.value; + } + + update(target: number, dt: number): number { + const rate = target > this.value ? 7 : 0.9; + this.value += (target - this.value) * (1 - Math.exp(-rate * Math.max(0, dt))); + if (target === 0 && this.value < 0.003) this.value = 0; + return this.value; + } +} diff --git a/web/hands/src/render/world.ts b/web/hands/src/render/world.ts index 2a5ed296..849873a5 100644 --- a/web/hands/src/render/world.ts +++ b/web/hands/src/render/world.ts @@ -4,8 +4,12 @@ export const RING_FIGHT_HALF = 3.05; export const RING_APRON_HALF = 3.85; export const PLATFORM_HALF = 4.05; export const POST_RADIUS = 3.42; +/** Distance from the centre of the ring to the ropes along either axis. */ +export const ROPE_LINE = POST_RADIUS * 0.72; export const ROPE_HEIGHTS = [0.5, 0.88, 1.26] as const; export const CANVAS_TOP = 0; +/** The arena floor, a metre below the canvas. */ +export const ARENA_FLOOR = -1; export interface WorldMapping { readonly x: (simX: number) => number; diff --git a/web/hands/src/settings.test.ts b/web/hands/src/settings.test.ts index f720c65a..7988cf6e 100644 --- a/web/hands/src/settings.test.ts +++ b/web/hands/src/settings.test.ts @@ -1,7 +1,14 @@ import { loadSettings, saveSettings, SettingsStore } from "./settings"; describe("nonsensitive preferences", () => { it("defaults to full blood and OS motion preference", () => expect(loadSettings()).toMatchObject({ blood: "full", haptics: true, reducedMotion: false })); - it("migrates malformed values safely and clamps volume", () => { localStorage.setItem("hands.preferences.v1", JSON.stringify({ volume: 99, haptics: "yes", reducedMotion: true, blood: "unknown", token: "must-not-propagate" })); expect(loadSettings()).toEqual({ volume: 1, haptics: true, reducedMotion: true, blood: "full" }); }); - it("persists only allowlisted preferences", () => { saveSettings({ volume: 0.4, haptics: false, reducedMotion: true, blood: "off" }); const raw = localStorage.getItem("hands.preferences.v1")!; expect(raw).toContain('"blood":"off"'); expect(raw).not.toMatch(/token|ticket|state/u); const store = new SettingsStore(); store.update({ blood: "reduced" }); expect(store.current.blood).toBe("reduced"); store.destroy(); }); - it("survives storage read and write failures", () => { const broken = { getItem: () => { throw new Error("read blocked"); }, setItem: () => { throw new Error("write blocked"); } } as unknown as Storage; expect(loadSettings(broken)).toMatchObject({ blood: "full" }); expect(() => saveSettings({ volume: 0.5, haptics: true, reducedMotion: false, blood: "off" }, broken)).not.toThrow(); const store = new SettingsStore(broken); expect(() => store.update({ blood: "reduced" })).not.toThrow(); expect(store.current.blood).toBe("reduced"); }); + it("migrates malformed values safely and clamps volume", () => { localStorage.setItem("hands.preferences.v1", JSON.stringify({ volume: 99, haptics: "yes", reducedMotion: true, blood: "unknown", token: "must-not-propagate" })); expect(loadSettings()).toEqual({ volume: 1, haptics: true, reducedMotion: true, blood: "full", commentary: true, announcer: true, camera: "broadcast" }); }); + it("persists only allowlisted preferences", () => { saveSettings({ volume: 0.4, haptics: false, reducedMotion: true, blood: "off", camera: "fighter", commentary: true, announcer: true }); const raw = localStorage.getItem("hands.preferences.v1")!; expect(raw).toContain('"blood":"off"'); expect(raw).not.toMatch(/token|ticket|state/u); const store = new SettingsStore(); store.update({ blood: "reduced" }); expect(store.current.blood).toBe("reduced"); store.destroy(); }); + it("keeps a chosen camera and rejects unknown ones", () => { + saveSettings({ volume: 0.5, haptics: true, reducedMotion: false, blood: "full", camera: "close", commentary: true, announcer: true }); + expect(loadSettings().camera).toBe("close"); + localStorage.setItem("hands.preferences.v1", JSON.stringify({ camera: "drone" })); + expect(loadSettings().camera).toBe("broadcast"); + }); + it("survives storage read and write failures", () => { const broken = { getItem: () => { throw new Error("read blocked"); }, setItem: () => { throw new Error("write blocked"); } } as unknown as Storage; expect(loadSettings(broken)).toMatchObject({ blood: "full" }); expect(() => saveSettings({ volume: 0.5, haptics: true, reducedMotion: false, blood: "off", commentary: true, announcer: true, camera: "broadcast" }, broken)).not.toThrow(); const store = new SettingsStore(broken); expect(() => store.update({ blood: "reduced" })).not.toThrow(); expect(store.current.blood).toBe("reduced"); }); + it("defaults commentary captions and the announcer voice on and remembers turning them off", () => { expect(loadSettings()).toMatchObject({ commentary: true, announcer: true }); localStorage.setItem("hands.preferences.v1", JSON.stringify({ commentary: "no", announcer: 1 })); expect(loadSettings()).toMatchObject({ commentary: true, announcer: true }); const store = new SettingsStore(); store.update({ commentary: false, announcer: false }); expect(new SettingsStore().current).toMatchObject({ commentary: false, announcer: false }); store.destroy(); }); }); diff --git a/web/hands/src/settings.ts b/web/hands/src/settings.ts index b286c404..cbabfe1a 100644 --- a/web/hands/src/settings.ts +++ b/web/hands/src/settings.ts @@ -1,19 +1,21 @@ export type BloodLevel = "full" | "reduced" | "off"; -export interface Settings { readonly volume: number; readonly haptics: boolean; readonly reducedMotion: boolean; readonly blood: BloodLevel } +export type CameraMode = "broadcast" | "close" | "fighter"; +export const CAMERA_MODES: readonly CameraMode[] = ["broadcast", "close", "fighter"]; +export interface Settings { readonly volume: number; readonly haptics: boolean; readonly reducedMotion: boolean; readonly blood: BloodLevel; readonly commentary: boolean; readonly announcer: boolean; readonly camera: CameraMode } const STORAGE_KEY = "hands.preferences.v1"; const unavailableStorage: Storage = { length: 0, clear: () => undefined, getItem: () => null, key: () => null, removeItem: () => undefined, setItem: () => undefined }; const browserStorage = (): Storage => { try { return window.localStorage; } catch { return unavailableStorage; } }; -const defaults = (): Settings => ({ volume: 0.7, haptics: true, reducedMotion: window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false, blood: "full" }); +const defaults = (): Settings => ({ volume: 0.7, haptics: true, reducedMotion: window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false, blood: "full", commentary: true, announcer: true, camera: "broadcast" }); export function loadSettings(storage: Storage = browserStorage()): Settings { const fallback = defaults(); try { const raw: unknown = JSON.parse(storage.getItem(STORAGE_KEY) ?? "null"); if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return fallback; const value = raw as Record; const volume = typeof value.volume === "number" && Number.isFinite(value.volume) ? Math.max(0, Math.min(1, value.volume)) : fallback.volume; - return { volume, haptics: typeof value.haptics === "boolean" ? value.haptics : fallback.haptics, reducedMotion: typeof value.reducedMotion === "boolean" ? value.reducedMotion : fallback.reducedMotion, blood: value.blood === "reduced" || value.blood === "off" || value.blood === "full" ? value.blood : fallback.blood }; + return { volume, haptics: typeof value.haptics === "boolean" ? value.haptics : fallback.haptics, reducedMotion: typeof value.reducedMotion === "boolean" ? value.reducedMotion : fallback.reducedMotion, blood: value.blood === "reduced" || value.blood === "off" || value.blood === "full" ? value.blood : fallback.blood, commentary: typeof value.commentary === "boolean" ? value.commentary : fallback.commentary, announcer: typeof value.announcer === "boolean" ? value.announcer : fallback.announcer, camera: CAMERA_MODES.includes(value.camera as CameraMode) ? value.camera as CameraMode : fallback.camera }; } catch { return fallback; } } export function saveSettings(settings: Settings, storage: Storage = browserStorage()): void { - try { storage.setItem(STORAGE_KEY, JSON.stringify({ volume: Math.max(0, Math.min(1, settings.volume)), haptics: settings.haptics, reducedMotion: settings.reducedMotion, blood: settings.blood })); } catch { /* Storage can be unavailable in embedded/privacy contexts. */ } + try { storage.setItem(STORAGE_KEY, JSON.stringify({ volume: Math.max(0, Math.min(1, settings.volume)), haptics: settings.haptics, reducedMotion: settings.reducedMotion, blood: settings.blood, commentary: settings.commentary, announcer: settings.announcer, camera: settings.camera })); } catch { /* Storage can be unavailable in embedded/privacy contexts. */ } } export class SettingsStore { private value: Settings; private readonly listeners = new Set<(settings: Settings) => void>(); diff --git a/web/hands/src/state.test.ts b/web/hands/src/state.test.ts index 8aa410ef..83c30491 100644 --- a/web/hands/src/state.test.ts +++ b/web/hands/src/state.test.ts @@ -1,21 +1,32 @@ +import { PROTOCOL_VERSION } from "./types"; import { initialState, reduceState } from "./state"; import { publicPlayers, snapshot } from "./test/fixtures"; describe("authoritative state reducer", () => { it("moves through wait, ready, combat, pause, resume and final without client outcomes", () => { - let state = reduceState(initialState, { type: "message", message: { version: 3, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [publicPlayers[0]], server_tick: 0, next_sequence: 7 } }); + let state = reduceState(initialState, { type: "message", message: { version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [publicPlayers[0]], server_tick: 0, next_sequence: 7 } }); expect(state).toMatchObject({ role: "fighter", playerId: "one", nextSequence: 7 }); - state = reduceState(state, { type: "message", message: { version: 3, type: "waiting", open_seats: 1 } }); expect(state.stage).toBe("waiting"); - state = reduceState(state, { type: "message", message: { version: 3, type: "ready", players: [...publicPlayers] } }); expect(Object.keys(state.players)).toHaveLength(2); - state = reduceState(state, { type: "message", message: { version: 3, type: "snapshot", payload: snapshot(20) } }); expect(state.stage).toBe("fight"); - state = reduceState(state, { type: "message", message: { version: 3, type: "paused", player_id: "two", grace_ms: 20000 } }); expect(state.stage).toBe("paused"); - state = reduceState(state, { type: "message", message: { version: 3, type: "resumed", player_id: "two" } }); expect(state.stage).toBe("fight"); - state = reduceState(state, { type: "message", message: { version: 3, type: "final", match_id: "m", winner_id: null, method: "draw", round: 12, scorecards: [], ratings: { one: { before: 1500, after: 1500 }, two: { before: 1500, after: 1500 } } } }); expect(state.stage).toBe("complete"); + state = reduceState(state, { type: "message", message: { version: PROTOCOL_VERSION, type: "waiting", open_seats: 1 } }); expect(state.stage).toBe("waiting"); + state = reduceState(state, { type: "message", message: { version: PROTOCOL_VERSION, type: "ready", players: [...publicPlayers] } }); expect(Object.keys(state.players)).toHaveLength(2); + state = reduceState(state, { type: "message", message: { version: PROTOCOL_VERSION, type: "snapshot", payload: snapshot(20) } }); expect(state.stage).toBe("fight"); + state = reduceState(state, { type: "message", message: { version: PROTOCOL_VERSION, type: "paused", player_id: "two", grace_ms: 20000 } }); expect(state.stage).toBe("paused"); + state = reduceState(state, { type: "message", message: { version: PROTOCOL_VERSION, type: "resumed", player_id: "two" } }); expect(state.stage).toBe("fight"); + state = reduceState(state, { type: "message", message: { version: PROTOCOL_VERSION, type: "final", match_id: "m", winner_id: null, method: "draw", round: 12, scorecards: [], ratings: { one: { before: 1500, after: 1500 }, two: { before: 1500, after: 1500 } } } }); expect(state.stage).toBe("complete"); + }); + it("shows the pick of styles with both fighters, then clears it when the bout is ready", () => { + let state = reduceState(initialState, { type: "message", message: { version: PROTOCOL_VERSION, type: "welcome", role: "fighter", player_id: "one", seat: 1, rating: 1500, players: [publicPlayers[0]], server_tick: 0, next_sequence: 0 } }); + state = reduceState(state, { type: "message", message: { version: PROTOCOL_VERSION, type: "waiting", open_seats: 1 } }); + const select = { version: PROTOCOL_VERSION, type: "select", deadline_ms: 9_000, players: [publicPlayers[0], { ...publicPlayers[1], style: "boxer" }], ready: ["two"] } as const; + state = reduceState(state, { type: "message", message: select }); + expect(state).toMatchObject({ stage: "select", select, playerOrder: ["one", "two"] }); + expect(state.players.two?.style).toBe("boxer"); + state = reduceState(state, { type: "message", message: { version: PROTOCOL_VERSION, type: "ready", players: [...publicPlayers] } }); + expect(state).toMatchObject({ stage: "countdown", select: null }); }); it("stores spectator role without granting fighter identity or sequence authority", () => { - const state = reduceState(initialState, { type: "message", message: { version: 3, type: "welcome", role: "spectator", player_id: "viewer", players: [...publicPlayers], server_tick: 30 } }); + const state = reduceState(initialState, { type: "message", message: { version: PROTOCOL_VERSION, type: "welcome", role: "spectator", player_id: "viewer", players: [...publicPlayers], server_tick: 30 } }); expect(state).toMatchObject({ role: "spectator", playerId: null, nextSequence: 0, serverTick: 30 }); expect(Object.keys(state.players)).toEqual(["one", "two"]); }); - it("keeps refreshed credentials out of application state", () => expect(reduceState(initialState, { type: "message", message: { version: 3, type: "ticket", reconnect_ticket: "memory-only", refresh_id: "refresh-identifier" } })).toBe(initialState)); - it("fails closed on safe server errors", () => expect(reduceState(initialState, { type: "message", message: { version: 3, type: "error", code: "invalid_input" } }).safeError).toBe("invalid_input")); + it("keeps refreshed credentials out of application state", () => expect(reduceState(initialState, { type: "message", message: { version: PROTOCOL_VERSION, type: "ticket", reconnect_ticket: "memory-only", refresh_id: "refresh-identifier" } })).toBe(initialState)); + it("fails closed on safe server errors", () => expect(reduceState(initialState, { type: "message", message: { version: PROTOCOL_VERSION, type: "error", code: "invalid_input" } }).safeError).toBe("invalid_input")); }); diff --git a/web/hands/src/state.ts b/web/hands/src/state.ts index 750f4dcd..b1e735ca 100644 --- a/web/hands/src/state.ts +++ b/web/hands/src/state.ts @@ -1,6 +1,6 @@ -import type { ConnectionRole, EngineSnapshot, FinalMessage, PublicPlayer, ServerMessage, SimulationInfo, TokenPlayer } from "./types"; +import type { ConnectionRole, EngineSnapshot, FinalMessage, PublicPlayer, SelectMessage, ServerMessage, SimulationInfo, TokenPlayer } from "./types"; -export type AppStage = "bootstrapping" | "authorizing" | "connecting" | "waiting" | "countdown" | "fight" | "knockdown" | "foul_recovery" | "rest" | "paused" | "complete" | "fatal"; +export type AppStage = "bootstrapping" | "authorizing" | "connecting" | "waiting" | "select" | "countdown" | "fight" | "knockdown" | "foul_recovery" | "rest" | "paused" | "complete" | "fatal"; export interface GameState { readonly stage: AppStage; readonly player: TokenPlayer | null; @@ -11,12 +11,14 @@ export interface GameState { readonly simulation: SimulationInfo | null; readonly snapshot: EngineSnapshot | null; readonly final: FinalMessage | null; + /** The pick of styles before the bout, while it lasts. */ + readonly select: SelectMessage | null; readonly serverTick: number; readonly nextSequence: number; readonly reconnectMs: number; readonly safeError: string | null; } -export const initialState: GameState = { stage: "bootstrapping", player: null, playerId: null, role: null, players: {}, playerOrder: [], simulation: null, snapshot: null, final: null, serverTick: 0, nextSequence: 0, reconnectMs: 0, safeError: null }; +export const initialState: GameState = { stage: "bootstrapping", player: null, playerId: null, role: null, players: {}, playerOrder: [], simulation: null, snapshot: null, final: null, select: null, serverTick: 0, nextSequence: 0, reconnectMs: 0, safeError: null }; export type StateAction = { type: "bootstrap"; simulation: SimulationInfo } | { type: "authorized"; player: TokenPlayer } | { type: "connecting" } | { type: "message"; message: ServerMessage } | { type: "reconnect-tick"; remainingMs: number } | { type: "fatal"; code: string }; const mapPlayers = (players: readonly PublicPlayer[]): Readonly> => Object.fromEntries(players.map((player) => [player.id, player])); @@ -30,8 +32,9 @@ export function reduceState(state: GameState, action: StateAction): GameState { switch (message.type) { case "welcome": return { ...state, playerId: message.role === "fighter" ? message.player_id : null, role: message.role, players: mapPlayers(message.players), playerOrder: message.players.map((player) => player.id), serverTick: message.server_tick, nextSequence: message.role === "fighter" ? message.next_sequence : 0, safeError: null }; case "ticket": return state; - case "waiting": return { ...state, stage: "waiting" }; - case "ready": return { ...state, stage: state.snapshot?.phase ?? "countdown", players: mapPlayers(message.players), playerOrder: message.players.map((player) => player.id) }; + case "waiting": return { ...state, stage: "waiting", select: null }; + case "select": return { ...state, stage: "select", select: message, players: mapPlayers(message.players), playerOrder: message.players.map((player) => player.id) }; + case "ready": return { ...state, stage: state.snapshot?.phase ?? "countdown", select: null, players: mapPlayers(message.players), playerOrder: message.players.map((player) => player.id) }; case "paused": return { ...state, stage: "paused", reconnectMs: message.grace_ms }; case "resumed": return { ...state, stage: state.snapshot?.phase ?? "countdown", reconnectMs: 0 }; case "snapshot": return { ...state, stage: message.payload.phase, snapshot: message.payload, serverTick: Math.max(state.serverTick, message.payload.tick) }; diff --git a/web/hands/src/style.css b/web/hands/src/style.css index 832d62f1..12e93bc1 100644 --- a/web/hands/src/style.css +++ b/web/hands/src/style.css @@ -1,6 +1,10 @@ -:root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#f2f6ff;background:#07090e;font-synthesis:none}*{box-sizing:border-box}html,body,#app{width:100%;height:100%;margin:0;overflow:hidden}.activity{position:relative;width:100%;height:100%;min-height:320px;background:#080b12}.fight{display:block;width:100%;height:100%;touch-action:none}.topbar{position:absolute;top:max(8px,env(safe-area-inset-top));left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:12px;padding:7px 10px;border:1px solid #ffffff24;background:#07101ad9;backdrop-filter:blur(8px);border-radius:3px;letter-spacing:.03em;white-space:nowrap}.topbar strong{color:#f1cc72;letter-spacing:.18em}.topbar span{font-size:12px;color:#aebbd0}.topbar .spectator-role{color:#f1cc72;font-weight:800}.spectator-role[hidden]{display:none}.topbar button,.panel button{color:#eaf1ff;border:1px solid #ffffff2e;background:#172334;padding:6px 10px;border-radius:3px;cursor:pointer}.overlay{position:absolute;left:50%;bottom:max(18px,env(safe-area-inset-bottom));transform:translateX(-50%);display:grid;justify-items:center;gap:9px;max-width:min(90vw,540px);text-align:center;pointer-events:none}.overlay[hidden]{display:none}.overlay[data-raised]{bottom:max(124px,calc(env(safe-area-inset-bottom) + 112px))}.status[hidden]{display:none}.status{padding:8px 13px;margin:0;color:#e8effc;background:#07101adf;border:1px solid #ffffff1f;border-radius:3px;font-weight:600}.hint{margin:0;padding:6px 12px;font-size:12px;letter-spacing:.02em;color:#aebbd0;background:#07101ab8;border:1px solid #ffffff14;border-radius:3px}.hint[hidden]{display:none}.primary{pointer-events:auto;padding:10px 18px;color:#081018;background:#f1cc72;border:0;border-radius:3px;font-weight:800;cursor:pointer}.primary:disabled{opacity:.55;cursor:wait}.panel{position:absolute;z-index:4;top:58px;right:14px;width:min(390px,calc(100vw - 28px));max-height:calc(100vh - 80px);overflow:auto;padding:16px 20px;background:#08111cf2;border:1px solid #ffffff2b;box-shadow:0 14px 45px #0009}.panel h2{margin:0 0 10px;color:#f1cc72;font-size:17px}.panel ul{margin:0;padding-left:19px;font-size:13px;line-height:1.55;color:#d7e0ee}.settings{display:grid;gap:12px}.settings[hidden],.panel[hidden]{display:none}.diagnostics h3{margin:12px 0 4px;font-size:13px;color:#f1cc72}.diagnostics pre{white-space:pre-wrap;word-break:break-word;margin:0 0 8px;padding:8px;font-size:11px;line-height:1.45;color:#c8d3e6;background:#0c1622;border:1px solid #ffffff14;border-radius:3px}.settings label{display:flex;align-items:center;justify-content:space-between;gap:14px;font-size:14px}.settings input[type=range]{width:190px}.settings select{color:#f2f6ff;background:#172334;border:1px solid #ffffff2e;padding:5px}.sr-summary{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.lab-controls{position:absolute;left:50%;bottom:max(10px,env(safe-area-inset-bottom));transform:translateX(-50%);display:flex;align-items:center;gap:10px;padding:8px 12px;background:#07101ae6;border:1px solid #ffffff24;border-radius:3px;font-size:12px;color:#d7e0ee;white-space:nowrap}.lab-controls button,.lab-controls select{color:#eaf1ff;border:1px solid #ffffff2e;background:#172334;padding:5px 9px;border-radius:3px;cursor:pointer;font-size:12px}.lab-controls input[type=number]{width:56px;background:#0c1622;color:#eaf1ff;border:1px solid #ffffff2e;padding:4px;border-radius:3px}.lab-controls label{display:flex;align-items:center;gap:4px;cursor:pointer}.lab-controls [data-lab-metrics]{color:#8fa3c8;max-width:40vw;overflow:hidden;text-overflow:ellipsis}@media(max-width:700px){.topbar{left:8px;right:8px;transform:none;justify-content:center;flex-wrap:wrap}.topbar span:not(.spectator-role){display:none}.topbar button{padding:5px 8px}.panel{top:74px}.status{font-size:13px}.overlay{bottom:10px}.overlay[data-raised]{bottom:116px}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important;animation:none!important}} +:root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#f2f6ff;background:#07090e;font-synthesis:none}*{box-sizing:border-box}html,body,#app{width:100%;height:100%;margin:0;overflow:hidden}.activity{position:relative;width:100%;height:100%;min-height:320px;background:#080b12}.fight{display:block;width:100%;height:100%;touch-action:none}.topbar{position:absolute;top:max(8px,env(safe-area-inset-top));left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:12px;padding:7px 10px;border:1px solid #ffffff24;background:#07101ad9;backdrop-filter:blur(8px);border-radius:3px;letter-spacing:.03em;white-space:nowrap}.topbar strong{color:#f1cc72;letter-spacing:.18em}.topbar span{font-size:12px;color:#aebbd0}.topbar .spectator-role{color:#f1cc72;font-weight:800}.spectator-role[hidden]{display:none}.topbar button,.panel button{color:#eaf1ff;border:1px solid #ffffff2e;background:#172334;padding:6px 10px;border-radius:3px;cursor:pointer}.overlay{position:absolute;left:50%;bottom:max(18px,env(safe-area-inset-bottom));transform:translateX(-50%);display:grid;justify-items:center;gap:9px;max-width:min(90vw,540px);text-align:center;pointer-events:none}.overlay[hidden]{display:none}.overlay[data-raised]{bottom:max(124px,calc(env(safe-area-inset-bottom) + 112px))}.overlay[data-result]{bottom:max(26px,calc(env(safe-area-inset-bottom) + 14px))}.status[hidden]{display:none}.status{padding:8px 13px;margin:0;color:#e8effc;background:#07101adf;border:1px solid #ffffff1f;border-radius:3px;font-weight:600}.hint{margin:0;padding:6px 12px;font-size:12px;letter-spacing:.02em;color:#aebbd0;background:#07101ab8;border:1px solid #ffffff14;border-radius:3px}.hint[hidden]{display:none}.primary{pointer-events:auto;padding:10px 18px;color:#081018;background:#f1cc72;border:0;border-radius:3px;font-weight:800;cursor:pointer}.primary:disabled{opacity:.55;cursor:wait}.panel{position:absolute;z-index:4;top:58px;right:14px;width:min(390px,calc(100vw - 28px));max-height:calc(100vh - 80px);overflow:auto;padding:16px 20px;background:#08111cf2;border:1px solid #ffffff2b;box-shadow:0 14px 45px #0009}.panel h2{margin:0 0 10px;color:#f1cc72;font-size:17px}.panel ul{margin:0;padding-left:19px;font-size:13px;line-height:1.55;color:#d7e0ee}.settings{display:grid;gap:12px}.settings[hidden],.panel[hidden]{display:none}.diagnostics h3{margin:12px 0 4px;font-size:13px;color:#f1cc72}.diagnostics pre{white-space:pre-wrap;word-break:break-word;margin:0 0 8px;padding:8px;font-size:11px;line-height:1.45;color:#c8d3e6;background:#0c1622;border:1px solid #ffffff14;border-radius:3px}.settings label{display:flex;align-items:center;justify-content:space-between;gap:14px;font-size:14px}.settings input[type=range]{width:190px}.settings select{color:#f2f6ff;background:#172334;border:1px solid #ffffff2e;padding:5px}.sr-summary{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.lab-controls{position:absolute;left:50%;bottom:max(10px,env(safe-area-inset-bottom));transform:translateX(-50%);display:flex;align-items:center;gap:10px;padding:8px 12px;background:#07101ae6;border:1px solid #ffffff24;border-radius:3px;font-size:12px;color:#d7e0ee;white-space:nowrap}.lab-controls button,.lab-controls select{color:#eaf1ff;border:1px solid #ffffff2e;background:#172334;padding:5px 9px;border-radius:3px;cursor:pointer;font-size:12px}.lab-controls input[type=number]{width:56px;background:#0c1622;color:#eaf1ff;border:1px solid #ffffff2e;padding:4px;border-radius:3px}.lab-controls label{display:flex;align-items:center;gap:4px;cursor:pointer}.lab-controls [data-lab-metrics]{color:#8fa3c8;max-width:40vw;overflow:hidden;text-overflow:ellipsis}@media(max-width:700px){.topbar{left:8px;right:8px;transform:none;justify-content:center;flex-wrap:wrap}.topbar span:not(.spectator-role){display:none}.topbar button{padding:5px 8px}.panel{top:74px}.status{font-size:13px}.overlay{bottom:10px}.overlay[data-raised]{bottom:116px}.overlay[data-result]{bottom:26px}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important;animation:none!important}} .model-panel{left:14px;right:auto;top:58px;max-height:calc(100vh - 80px);width:min(430px,calc(100vw - 28px))}.model-clips{max-height:200px;overflow:auto;display:grid;gap:4px}.model-clips button{text-align:left;font-size:12px;padding:5px 8px;background:#172334;color:#eaf1ff;border:1px solid #ffffff2e;border-radius:3px;cursor:pointer}.model-clips button.active{border-color:#f1cc72;color:#f1cc72}.model-row{display:flex;gap:10px;align-items:center;margin-top:8px;font-size:12px}.model-row label{display:flex;align-items:center;gap:6px}.model-inventory,.model-warnings{max-height:220px;overflow:auto;font-size:10px;line-height:1.4;color:#c8d3e6;white-space:pre-wrap} .model-credit{margin:2px 0 0;font-size:11px;line-height:1.35;color:#9cabc1}.model-credit a{color:#c7d5eb} -.touch-controls{position:absolute;inset:0;pointer-events:none;z-index:3;font-family:Inter,system-ui,sans-serif;user-select:none;-webkit-user-select:none}.touch-controls.disabled{display:none}.touch-controls.resting{opacity:.4}.touch-stick{position:absolute;left:0;top:calc(max(8px,env(safe-area-inset-top)) + 48px);bottom:0;width:46%;pointer-events:auto;touch-action:none}.touch-stick-base{position:absolute;width:132px;height:132px;margin:-66px 0 0 -66px;border-radius:50%;border:2px solid #ffffff55;background:#0a121f66;opacity:0;transition:opacity .12s}.touch-stick-base.active{opacity:1}.touch-stick-knob{position:absolute;left:50%;top:50%;width:56px;height:56px;border-radius:50%;background:#f1cc72cc;transform:translate(-50%,-50%);box-shadow:0 2px 10px #0008}.touch-pads{position:absolute;right:max(12px,env(safe-area-inset-right));bottom:max(118px,calc(env(safe-area-inset-bottom) + 118px));display:grid;gap:8px;pointer-events:auto;touch-action:none}.touch-grid{display:grid;grid-template-columns:repeat(2,92px);gap:6px}.touch-pad{position:relative;height:56px;border-radius:12px;border:1px solid #ffffff44;background:linear-gradient(90deg,#1d4ed8aa 0 50%,#b91c1caa 50% 100%);color:#fff;font-weight:800;font-size:11px;letter-spacing:.01em;display:flex;align-items:center;justify-content:space-between;padding:0 7px;cursor:pointer}.touch-pad .touch-pad-hand{font-size:10px;opacity:.8}.touch-pad .touch-pad-label{font-size:10px}.touch-pad.pressed{filter:brightness(1.5);transform:scale(.96)}.touch-mods{display:grid;grid-template-columns:repeat(2,92px);gap:6px}.touch-mod{height:32px;padding:0;border-radius:9px;border:1px solid #ffffff44;background:#0a121fcc;color:#e8effc;font-weight:800;font-size:11px;letter-spacing:.06em;cursor:pointer}.touch-mod.pressed{background:#f1cc72;color:#081018}.touch-controls.knockdown .touch-pad{background:linear-gradient(90deg,#1f8a4caa 0 50%,#1f8a4caa 50% 100%)}.touch-moves{display:grid;grid-template-columns:repeat(3,1fr);grid-template-areas:"sl wv sr" "pl cl cl";gap:6px;margin-bottom:4px}.touch-move{height:30px;padding:0;border-radius:9px;border:1px solid #7fd3c766;background:#0a1a20cc;color:#d8f3ee;font-weight:800;font-size:10px;letter-spacing:.05em;white-space:nowrap;cursor:pointer}.touch-move.pressed{background:#f1cc72;color:#081018}.touch-move[data-move=slip_left]{grid-area:sl}.touch-move[data-move=weave]{grid-area:wv}.touch-move[data-move=slip_right]{grid-area:sr}.touch-move[data-move=pull]{grid-area:pl}.touch-move[data-move=clinch]{grid-area:cl}.touch-controls.knockdown .touch-move{opacity:.35}@media(orientation:landscape){.touch-pads{grid-template-columns:auto auto;grid-template-areas:"moves mods" "moves grid";column-gap:12px}.touch-moves{grid-area:moves;align-self:end;margin-bottom:0;grid-template-columns:repeat(2,58px);grid-template-areas:"sl sr" "wv pl" "cl cl"}.touch-mods{grid-area:mods}.touch-grid{grid-area:grid}}@media(pointer:fine) and (hover:hover){.touch-controls{display:none}} +.touch-controls{position:absolute;inset:0;pointer-events:none;z-index:3;font-family:Inter,system-ui,sans-serif;user-select:none;-webkit-user-select:none}.touch-controls.disabled{display:none}.touch-controls.resting{opacity:.4}.touch-stick{position:absolute;left:0;top:calc(max(8px,env(safe-area-inset-top)) + 48px);bottom:0;width:46%;pointer-events:auto;touch-action:none}.touch-stick-base{position:absolute;width:132px;height:132px;margin:-66px 0 0 -66px;border-radius:50%;border:2px solid #ffffff55;background:#0a121f66;opacity:0;transition:opacity .12s}.touch-stick-base.active{opacity:1}.touch-stick-knob{position:absolute;left:50%;top:50%;width:56px;height:56px;border-radius:50%;background:#f1cc72cc;transform:translate(-50%,-50%);box-shadow:0 2px 10px #0008}.touch-pads{position:absolute;right:max(12px,env(safe-area-inset-right));bottom:max(118px,calc(env(safe-area-inset-bottom) + 118px));display:grid;gap:8px;pointer-events:none;touch-action:none}.touch-pad,.touch-mod,.touch-move{pointer-events:auto}.touch-grid{display:grid;grid-template-columns:repeat(2,92px);gap:6px}.touch-pad{position:relative;height:56px;border-radius:12px;border:1px solid #ffffff44;background:linear-gradient(90deg,#1d4ed8aa 0 50%,#b91c1caa 50% 100%);color:#fff;font-weight:800;font-size:11px;letter-spacing:.01em;display:flex;align-items:center;justify-content:space-between;padding:0 7px;cursor:pointer}.touch-pad .touch-pad-hand{font-size:10px;opacity:.8}.touch-pad .touch-pad-label{font-size:10px}.touch-pad.pressed{filter:brightness(1.5);transform:scale(.96)}.touch-mods{display:grid;grid-template-columns:repeat(2,92px);gap:6px}.touch-mod{height:32px;padding:0;border-radius:9px;border:1px solid #ffffff44;background:#0a121fcc;color:#e8effc;font-weight:800;font-size:11px;letter-spacing:.06em;cursor:pointer}.touch-mod.pressed{background:#f1cc72;color:#081018}.touch-controls.knockdown .touch-pad{background:linear-gradient(90deg,#1f8a4caa 0 50%,#1f8a4caa 50% 100%)}.touch-moves{display:grid;grid-template-columns:repeat(3,1fr);grid-template-areas:"sl wv sr" "pl cl cl";gap:6px;margin-bottom:4px}.touch-move{height:30px;padding:0;border-radius:9px;border:1px solid #7fd3c766;background:#0a1a20cc;color:#d8f3ee;font-weight:800;font-size:10px;letter-spacing:.05em;white-space:nowrap;cursor:pointer}.touch-move.pressed{background:#f1cc72;color:#081018}.touch-move[data-move=slip_left]{grid-area:sl}.touch-move[data-move=weave]{grid-area:wv}.touch-move[data-move=slip_right]{grid-area:sr}.touch-move[data-move=pull]{grid-area:pl}.touch-move[data-move=clinch]{grid-area:cl}.touch-controls.knockdown .touch-move,.touch-controls.knockdown .touch-mod{visibility:hidden}@media(orientation:landscape){.touch-pads{grid-template-columns:auto auto;grid-template-areas:"moves mods" "moves grid";column-gap:12px}.touch-moves{grid-area:moves;align-self:end;margin-bottom:0;grid-template-columns:repeat(2,58px);grid-template-areas:"sl sr" "wv pl" "cl cl"}.touch-mods{grid-area:mods}.touch-grid{grid-area:grid}}@media(orientation:landscape) and (max-height:350px){.touch-pads{bottom:max(112px,calc(env(safe-area-inset-bottom) + 112px));row-gap:6px}.touch-grid,.touch-mods,.touch-moves{gap:4px}.touch-pad{height:44px}.touch-mod,.touch-move{height:26px}}@media(pointer:fine) and (hover:hover){.touch-controls{display:none}} +.corner{position:absolute;z-index:3;left:50%;bottom:max(124px,calc(env(safe-area-inset-bottom) + 112px));transform:translateX(-50%);display:grid;gap:8px;width:min(660px,calc(100vw - 24px));padding:10px 12px 9px;background:#07101aeb;border:1px solid #f1cc7259;border-radius:3px;box-shadow:0 10px 30px #0009;text-align:center}.corner[hidden]{display:none}.corner-title{margin:0;color:#f1cc72;font-weight:800;font-size:12px;letter-spacing:.18em}.corner-choices{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.corner-choices button{display:grid;grid-template-columns:auto 1fr;align-items:center;column-gap:9px;row-gap:2px;padding:8px 10px;text-align:left;color:#eaf1ff;background:#172334;border:1px solid #ffffff2e;border-radius:3px;cursor:pointer;font:inherit}.corner-choices kbd{grid-row:span 2;display:grid;place-items:center;width:26px;height:26px;border-radius:3px;background:#0b1520;border:1px solid #ffffff38;color:#f1cc72;font:800 13px ui-monospace,monospace}.corner-choices strong{font-size:13px}.corner-choices span{font-size:11px;color:#aebbd0}.corner-choices button:hover:not(:disabled){border-color:#f1cc72aa}.corner-choices button:disabled{cursor:default;opacity:.42}.corner-choices button[data-picked]{opacity:1;border-color:#f1cc72;background:#272515;box-shadow:inset 0 0 0 1px #f1cc72}.corner-status{margin:0;font-size:12px;color:#d7e0ee}@media(max-width:560px){.corner{bottom:max(112px,calc(env(safe-area-inset-bottom) + 100px));padding:8px 9px}.corner-choices{grid-template-columns:1fr;gap:6px}.corner-choices button{padding:6px 9px}}@media(max-height:460px){.corner{bottom:max(116px,calc(env(safe-area-inset-bottom) + 104px));width:min(600px,calc(100vw - 24px));padding:5px 8px;gap:4px}.corner-title{font-size:10px}.corner-choices{gap:6px}.corner-choices button{padding:4px 7px;column-gap:7px}.corner-choices kbd{width:22px;height:22px;font-size:12px}.corner-choices strong{font-size:12px}.corner-choices span{font-size:10px}.corner-status{font-size:11px}} +.cpu-picker{pointer-events:auto;display:grid;justify-items:center;gap:8px;width:min(90vw,500px);padding:10px 12px;background:#07101adf;border:1px solid #ffffff1f;border-radius:3px}.cpu-picker[hidden]{display:none}.cpu-picker p{margin:0;font-size:13px;font-weight:800;letter-spacing:.04em;color:#f1cc72}.cpu-levels{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;width:100%}.cpu-levels button{display:grid;align-content:center;gap:3px;min-height:52px;padding:8px 6px;color:#eaf1ff;background:#172334;border:1px solid #ffffff2e;border-radius:3px;cursor:pointer;font:inherit;text-align:center}.cpu-levels button strong{font-size:14px;letter-spacing:.03em}.cpu-levels button span{font-size:11px;line-height:1.25;color:#aebbd0}.cpu-levels button:hover,.cpu-levels button:focus-visible{border-color:#f1cc72;outline:none}@media(max-height:480px){.cpu-picker{gap:5px;padding:6px 10px}.cpu-levels button{min-height:40px;padding:5px 6px;gap:1px}.cpu-levels button strong{font-size:13px}.cpu-levels button span{font-size:10px;line-height:1.15}} +.overlay[data-touch-hint]{left:max(12px,env(safe-area-inset-left));transform:none;max-width:min(44vw,300px,calc(100vw - 226px - env(safe-area-inset-left) - env(safe-area-inset-right)));justify-items:start;text-align:left}@media(orientation:landscape){.overlay[data-touch-hint]{max-width:min(44vw,300px,calc(100vw - 360px - env(safe-area-inset-left) - env(safe-area-inset-right)))}}.overlay[data-touch-hint] .hint{font-size:11px;line-height:1.35}.panel h3{margin:12px 0 6px;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:#f1cc72}.panel h3:first-of-type{margin-top:4px}details.diagnostics summary{cursor:pointer;font-size:13px;font-weight:700;color:#f1cc72}details.diagnostics[open] summary{margin-bottom:6px} +.style-picker{position:absolute;z-index:3;left:50%;top:50%;transform:translate(-50%,-50%);display:grid;gap:10px;width:min(980px,calc(100vw - 24px));max-height:calc(100% - 132px);overflow:auto;padding:12px 14px;background:#07101af2;border:1px solid #f1cc7266;border-radius:3px;box-shadow:0 14px 40px #000b;text-align:center}.style-picker[hidden]{display:none}.style-picker header{display:flex;justify-content:space-between;align-items:baseline;gap:12px}.style-title{margin:0;color:#f1cc72;font-weight:800;font-size:13px;letter-spacing:.18em}.style-clock{margin:0;color:#eaf1ff;font:800 15px ui-monospace,monospace}.style-cards{display:grid;grid-template-columns:repeat(5,1fr);gap:8px}.style-cards button{display:grid;grid-template-rows:auto auto 1fr auto;gap:5px;padding:10px;text-align:left;color:#eaf1ff;background:#172334;border:1px solid #ffffff2e;border-radius:3px;cursor:pointer;font:inherit}.style-cards kbd{justify-self:start;display:grid;place-items:center;width:22px;height:22px;border-radius:3px;background:#0b1520;border:1px solid #ffffff38;color:#f1cc72;font:800 12px ui-monospace,monospace}.style-cards strong{font-size:14px}.style-cards span{font-size:11px;line-height:1.35;color:#aebbd0}.style-stats{list-style:none;margin:2px 0 0;padding:0;display:grid;gap:3px}.style-stats li{display:grid;grid-template-columns:66px 1fr;align-items:center;gap:6px}.style-stats small{font-size:9px;letter-spacing:.05em;color:#8fa3c8;text-transform:uppercase}.style-stats i{display:block;height:5px;border-radius:2px;background:linear-gradient(90deg,#f1cc72,#d98a3a)}.style-stats i[data-value="1"]{width:20%}.style-stats i[data-value="2"]{width:40%}.style-stats i[data-value="3"]{width:60%}.style-stats i[data-value="4"]{width:80%}.style-stats i[data-value="5"]{width:100%}.style-cards button:hover:not(:disabled){border-color:#f1cc72aa}.style-cards button:disabled{cursor:default;opacity:.5}.style-cards button[data-chosen]{opacity:1;border-color:#f1cc72;background:#272515;box-shadow:inset 0 0 0 1px #f1cc72}.style-status{margin:0;font-size:12px;color:#d7e0ee}@media(max-width:700px){.style-picker{top:auto;bottom:max(12px,env(safe-area-inset-bottom));transform:translateX(-50%);max-height:calc(100% - 96px);padding:9px 10px}.style-cards{grid-template-columns:1fr 1fr;gap:6px}.style-cards button{grid-template-rows:auto 1fr;padding:8px 9px;gap:3px}.style-cards kbd,.style-stats{display:none}.style-cards strong{font-size:13px}.style-cards span{font-size:11px}.style-cards button:last-child{grid-column:1/-1}}@media(max-height:480px){.style-picker{top:auto;bottom:6px;transform:translateX(-50%);max-height:calc(100% - 60px);padding:6px 9px;gap:5px}.style-cards{grid-template-columns:repeat(5,1fr);gap:5px}.style-cards button{padding:5px 6px;gap:2px}.style-cards span{display:none}.style-cards button:last-child{grid-column:auto}.style-stats{display:grid;gap:1px}.style-stats li{grid-template-columns:52px 1fr}.style-stats small{font-size:8px}} diff --git a/web/hands/src/styles.test.ts b/web/hands/src/styles.test.ts new file mode 100644 index 00000000..4ff6ea3f --- /dev/null +++ b/web/hands/src/styles.test.ts @@ -0,0 +1,403 @@ +import manifest from "../../../src/intelstream/hands/combat-manifest.json"; +import { CONTROL_SECTIONS } from "./input/bindings"; +import { FIGHTER_STYLES } from "./protocol"; +import { loadStyle, saveStyle, STYLE_CARDS, StylePicker, styleTag, styleTitle } from "./styles"; +import { publicPlayers } from "./test/fixtures"; +import { PROTOCOL_VERSION, type ConnectionRole, type FighterStyle, type SelectMessage } from "./types"; + +/** A style's rule as the manifest gives it; every number it leaves out is the balanced fighter's. */ +interface ManifestStyle { + readonly impact_percent?: number; + readonly body_damage_percent?: number; + readonly poise_damage_percent?: number; + readonly reach_percent?: number; + readonly move_speed_percent?: number; + readonly stamina_cost_percent?: number; + readonly conditioning_loss_percent?: number; + readonly startup_ticks?: Readonly>; + readonly recovery_ticks?: Readonly>; +} + +const choosing = (fields: Partial = {}): SelectMessage => ({ version: PROTOCOL_VERSION, type: "select", deadline_ms: 9_500, players: [publicPlayers[0], publicPlayers[1]], ready: [], ...fields }); +const press = (code: string, init: KeyboardEventInit = {}): void => { window.dispatchEvent(new KeyboardEvent("keydown", { code, cancelable: true, ...init })); }; + +describe("style cards", () => { + it("offer every style once, each with six strengths from one to five", () => { + expect(STYLE_CARDS.map((card) => card.style)).toEqual([...FIGHTER_STYLES]); + for (const card of STYLE_CARDS) { + expect(card.stats).toHaveLength(6); + expect(card.stats.every((value) => Number.isInteger(value) && value >= 1 && value <= 5)).toBe(true); + } + expect(STYLE_CARDS.find((card) => card.style === "balanced")?.stats).toEqual([3, 3, 3, 3, 3, 3]); + }); + + it("show the strengths the manifest's numbers give each style, so a change to either is caught", () => { + // A bar is three at the balanced fighter's number and moves a step for each `step` away from it. + const bar = (measure: number, neutral: number, step: number): number => Math.max(1, Math.min(5, 3 + Math.round((measure - neutral) / step))); + const rules = manifest.styles as Readonly>; + for (const card of STYLE_CARDS) { + const rule = rules[card.style] ?? {}; + // Power: how hard the punches land, one in three to the body, and how much poise they take. + const power = ((rule.impact_percent ?? 100) * (200 + (rule.body_damage_percent ?? 100)) / 300) * (rule.poise_damage_percent ?? 100) / 100; + // Hand speed: the ticks a style takes off (or puts on) its punches' startup and recovery. + const ticks = [...Object.values(rule.startup_ticks ?? {}), ...Object.values(rule.recovery_ticks ?? {})].reduce((sum, value) => sum + value, 0); + // Stamina: what the punches cost, and how slowly the conditioning drains. + const stamina = (100 - (rule.stamina_cost_percent ?? 100)) + (100 - (rule.conditioning_loss_percent ?? 100)) / 2; + // Defence weighs the chin, head movement and parries against each other, so it is set by hand. + const [power_, speed, footwork, reach, endurance] = card.stats; + expect({ style: card.style, power: power_, speed, footwork, reach, stamina: endurance }).toEqual({ + style: card.style, + power: bar(power, 100, 6), + speed: bar(-ticks, 0, 2), + footwork: bar(rule.move_speed_percent ?? 100, 100, 4), + reach: bar(rule.reach_percent ?? 100, 100, 2), + stamina: bar(stamina, 0, 10), + }); + } + }); + + it("tag a name plate and title an introduction for every style but balanced", () => { + expect(FIGHTER_STYLES.map((style) => styleTag(style))).toEqual([null, "BOXER", "SLUGGER", "SWARMER", "COUNTER"]); + expect(FIGHTER_STYLES.map((style) => styleTitle(style))).toEqual([null, "the boxer", "the slugger", "the swarmer", "the counter-puncher"]); + expect(styleTag(undefined)).toBeNull(); + expect(styleTitle(undefined)).toBeNull(); + }); +}); + +describe("remembering the style", () => { + it("brings back last time's pick and falls back to balanced when there is none to trust", () => { + const stored = new Map(); + const storage = { getItem: (key: string) => stored.get(key) ?? null, setItem: (key: string, value: string) => { stored.set(key, value); } }; + expect(loadStyle(storage)).toBe("balanced"); + saveStyle("swarmer", storage); + expect(loadStyle(storage)).toBe("swarmer"); + stored.set("hands.style.v1", "brawler"); + expect(loadStyle(storage)).toBe("balanced"); + const blocked = { getItem: (): string | null => { throw new Error("blocked"); }, setItem: (): void => { throw new Error("blocked"); } }; + expect(loadStyle(blocked)).toBe("balanced"); + expect(() => saveStyle("boxer", blocked)).not.toThrow(); + expect(loadStyle(null)).toBe("balanced"); + }); +}); + +describe("the style picker", () => { + let parent: HTMLElement; + let sent: string[]; + let clock: number; + let accept: boolean; + const make = (preferred: FighterStyle = "balanced"): StylePicker => new StylePicker(parent, (style, ready) => { sent.push(`${style}:${ready}`); return accept; }, () => clock, preferred); + const show = (picker: StylePicker, select: SelectMessage = choosing(), viewer: string | null = "one", role: ConnectionRole = "fighter"): void => picker.update(select, viewer, role); + const card = (picker: StylePicker, style: FighterStyle): HTMLButtonElement => picker.element.querySelector(`[data-style="${style}"]`)!; + const status = (picker: StylePicker): string => picker.element.querySelector(".style-status")?.textContent ?? ""; + const clockText = (picker: StylePicker): string => picker.element.querySelector(".style-clock")?.textContent ?? ""; + + beforeEach(() => { + parent = document.createElement("div"); + document.body.append(parent); + sent = []; + clock = 1_000; + accept = true; + }); + afterEach(() => parent.remove()); + + it("stays hidden until the room is choosing, and again once it is done", () => { + const picker = make(); + expect(picker.element.hidden).toBe(true); + show(picker); + expect(picker.element.hidden).toBe(false); + expect([...picker.element.querySelectorAll("[data-style]")].map((button) => button.dataset.style)).toEqual([...FIGHTER_STYLES]); + picker.update(null, "one", "fighter"); + expect(picker.element.hidden).toBe(true); + press("Digit2"); + expect(sent).toEqual(["balanced:false"]); + picker.destroy(); + }); + + it("offers last time's style, unsettled, once each time a pick begins", () => { + const picker = make("swarmer"); + show(picker); + expect(sent).toEqual(["swarmer:false"]); + show(picker, choosing({ deadline_ms: 6_000 })); + expect(sent).toEqual(["swarmer:false"]); + picker.update(null, "one", "fighter"); + show(picker); + expect(sent).toEqual(["swarmer:false", "swarmer:false"]); + picker.destroy(); + }); + + it("begins choosing, with last time's style, when a spectator is seated before the bell", () => { + const picker = make("swarmer"); + show(picker, choosing(), "one", "spectator"); + expect(sent).toEqual([]); + show(picker, choosing({ deadline_ms: 9_000 })); + expect(sent).toEqual(["swarmer:false"]); + expect(card(picker, "swarmer").disabled).toBe(false); + picker.destroy(); + }); + + it("starts on last time's pick and moves with the arrow keys without settling", () => { + const picker = make("slugger"); + show(picker); + expect(card(picker, "slugger").getAttribute("aria-pressed")).toBe("true"); + press("ArrowRight"); + press("ArrowDown"); + press("ArrowLeft"); + press("ArrowUp"); + press("ArrowUp"); + press("ArrowUp"); + expect(sent).toEqual(["slugger:false", "swarmer:false", "counter_puncher:false", "swarmer:false", "slugger:false", "boxer:false", "balanced:false"]); + press("ArrowLeft"); + expect(sent.at(-1)).toBe("counter_puncher:false"); + expect(card(picker, "counter_puncher").getAttribute("aria-pressed")).toBe("true"); + expect(card(picker, "counter_puncher").disabled).toBe(false); + press("ArrowRight", { repeat: true }); + press("ArrowRight", { ctrlKey: true }); + expect(sent).toHaveLength(8); + picker.destroy(); + }); + + it("settles on Enter, then holds the pick, and leaves remembering it to the room's word", () => { + const picker = make(); + show(picker); + press("ArrowRight"); + press("Enter"); + expect(sent).toEqual(["balanced:false", "boxer:false", "boxer:true"]); + // Remembered once the room's ready says it is the style he boxes in (app.ts). + expect(loadStyle()).toBe("balanced"); + expect(card(picker, "slugger").disabled).toBe(true); + expect(status(picker)).toBe("Ready as Boxer. Waiting for your opponent."); + press("Digit3"); + card(picker, "slugger").click(); + expect(sent).toHaveLength(3); + // An update from before the room heard the pick does not undo it. + show(picker, choosing({ ready: [] })); + expect(card(picker, "slugger").disabled).toBe(true); + picker.destroy(); + }); + + it("lets Enter or Space on a focused card settle that card, not the highlighted one", () => { + const picker = make(); + show(picker); + const slugger = card(picker, "slugger"); + slugger.focus(); + for (const code of ["Enter", "Space"]) { + const key = new KeyboardEvent("keydown", { code, bubbles: true, cancelable: true }); + slugger.dispatchEvent(key); + // The browser's own activation of the focused button does the settling. + expect(key.defaultPrevented).toBe(false); + } + expect(sent).toEqual(["balanced:false"]); + slugger.click(); + expect(sent).toEqual(["balanced:false", "slugger:true"]); + picker.destroy(); + }); + + it("leaves keys typed into Settings or another control to that control", () => { + const picker = make(); + show(picker); + const panel = document.createElement("aside"); + panel.className = "panel"; + panel.innerHTML = ``; + parent.append(panel); + for (const [selector, code] of [["[data-volume]", "ArrowRight"], ["[data-volume]", "Digit3"], ["[data-haptics]", "Space"], ["button", "Enter"]] as const) { + const control = panel.querySelector(selector)!; + control.focus(); + const key = new KeyboardEvent("keydown", { code, bubbles: true, cancelable: true }); + control.dispatchEvent(key); + expect(key.defaultPrevented).toBe(false); + } + expect(sent).toEqual(["balanced:false"]); + picker.destroy(); + }); + + it("moves the keyboard focus with the highlight, so Tab and the arrows agree", () => { + const picker = make("boxer"); + show(picker); + const tabbable = (): string[] => [...picker.element.querySelectorAll("[data-style]")].filter((button) => button.tabIndex === 0).map((button) => button.dataset.style!); + expect(tabbable()).toEqual(["boxer"]); + card(picker, "boxer").focus(); + card(picker, "boxer").dispatchEvent(new KeyboardEvent("keydown", { code: "ArrowRight", bubbles: true, cancelable: true })); + expect(sent.at(-1)).toBe("slugger:false"); + expect(document.activeElement).toBe(card(picker, "slugger")); + expect(tabbable()).toEqual(["slugger"]); + picker.destroy(); + }); + + it("is chosen and settled with a controller, one move a press, and a button held as it opens is not a pick", () => { + const frames: FrameRequestCallback[] = []; + vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => { frames.push(callback); return frames.length; }); + vi.spyOn(window, "cancelAnimationFrame").mockImplementation(() => undefined); + const held = new Set(); + let stick = 0; + const pad = { connected: true, mapping: "standard", get buttons() { return Array.from({ length: 17 }, (_unused, index) => ({ pressed: held.has(index) })); }, get axes() { return [stick, 0, 0, 0]; } } as unknown as Gamepad; + Object.defineProperty(navigator, "getGamepads", { configurable: true, value: () => [pad] }); + try { + const picker = make(); + const frame = (): void => frames.shift()!(0); + // The bottom face button is still held from the last punch of the bout before. + held.add(0); + show(picker); + frame(); + expect(sent).toEqual(["balanced:false"]); + held.delete(0); + frame(); + // D-pad right held over two frames moves one card. + held.add(15); + frame(); + frame(); + expect(sent).toEqual(["balanced:false", "boxer:false"]); + held.delete(15); + frame(); + stick = 0.9; + frame(); + frame(); + expect(sent.at(-1)).toBe("slugger:false"); + stick = 0; + frame(); + stick = -0.9; + frame(); + expect(sent.at(-1)).toBe("boxer:false"); + expect(document.activeElement).toBe(card(picker, "boxer")); + stick = 0; + held.add(0); + frame(); + expect(sent).toEqual(["balanced:false", "boxer:false", "slugger:false", "boxer:false", "boxer:true"]); + // Once the pick is gone the controller is left alone. + picker.destroy(); + held.delete(0); + stick = 0.9; + for (const callback of frames.splice(0)) callback(0); + expect(frames).toEqual([]); + expect(sent).toHaveLength(5); + expect(CONTROL_SECTIONS.find((section) => section.title === "Controller")?.items).toContain("Controller style pick: D-pad or left stick to choose · bottom face button settles."); + } finally { + Object.defineProperty(navigator, "getGamepads", { configurable: true, value: () => [] }); + } + }); + + it("settles at once on a number key or a tap", () => { + const keyed = make(); + show(keyed); + press("Digit3"); + expect(sent).toEqual(["balanced:false", "slugger:true"]); + keyed.destroy(); + sent.length = 0; + const tapped = make(); + show(tapped); + card(tapped, "counter_puncher").click(); + expect(sent).toEqual(["balanced:false", "counter_puncher:true"]); + tapped.destroy(); + }); + + it("stops taking picks in the last moments, when one would reach the room after it closed", () => { + vi.useFakeTimers(); + const picker = make(); + show(picker); + // 300 ms of the room's 9.5 s are left on this page's clock: half a round trip from now, none. + clock += 9_200; + vi.advanceTimersByTime(250); + expect(clockText(picker)).toBe("0s"); + expect([...picker.element.querySelectorAll("[data-style]")].every((button) => button.disabled)).toBe(true); + press("Digit3"); + press("ArrowRight"); + press("Enter"); + card(picker, "slugger").click(); + expect(sent).toEqual(["balanced:false"]); + // A fresh deadline from the room opens the pick again. + show(picker, choosing({ deadline_ms: 4_000 })); + expect(card(picker, "slugger").disabled).toBe(false); + press("Digit3"); + expect(sent).toEqual(["balanced:false", "slugger:true"]); + picker.destroy(); + }); + + it("does not settle a pick that could not be sent", () => { + accept = false; + const picker = make(); + show(picker); + press("Digit4"); + expect(card(picker, "swarmer").disabled).toBe(false); + expect(loadStyle()).toBe("balanced"); + accept = true; + press("Digit4"); + expect(card(picker, "swarmer").disabled).toBe(true); + picker.destroy(); + }); + + it("counts down to the deadline the room gave", () => { + vi.useFakeTimers(); + const picker = make(); + show(picker); + expect(clockText(picker)).toBe("10s"); + clock += 4_000; + vi.advanceTimersByTime(250); + expect(clockText(picker)).toBe("6s"); + show(picker, choosing({ deadline_ms: 3_000 })); + expect(clockText(picker)).toBe("3s"); + clock += 60_000; + vi.advanceTimersByTime(250); + expect(clockText(picker)).toBe("0s"); + picker.destroy(); + }); + + it("says the other corner has settled without saying on what", () => { + const picker = make(); + show(picker); + expect(status(picker)).toBe("Tap a style, or use the arrow keys and Enter."); + // Even a style the room let slip would not be named: the bell reveals both together. + show(picker, choosing({ players: [publicPlayers[0], { ...publicPlayers[1], style: "slugger" }], ready: ["two"] })); + expect(status(picker)).toBe("Two is ready. Tap a style or press Enter to settle on yours."); + expect(status(picker)).not.toContain("Slugger"); + picker.destroy(); + }); + + it("says who the pick is waiting on when the other corner drops, and that he is back", () => { + const picker = make(); + const away = choosing({ players: [publicPlayers[0], { ...publicPlayers[1], connected: false }] }); + show(picker, away); + expect(status(picker)).toBe("Waiting for Two to reconnect. Tap a style or press Enter to settle on yours."); + card(picker, "slugger").click(); + expect(status(picker)).toBe("Ready as Slugger. Waiting for Two to reconnect."); + show(picker, choosing()); + expect(status(picker)).toBe("Ready as Slugger. Waiting for your opponent."); + show(picker, away, null, "spectator"); + expect(status(picker)).toBe("The fighters are choosing. Waiting for Two to reconnect."); + picker.destroy(); + }); + + it("shows the room's settled pick after a reconnect", () => { + const picker = make("balanced"); + show(picker, choosing({ players: [{ ...publicPlayers[0], style: "swarmer" }, publicPlayers[1]], ready: ["one"] })); + expect(card(picker, "swarmer").getAttribute("aria-pressed")).toBe("true"); + expect(card(picker, "swarmer").disabled).toBe(true); + expect(status(picker)).toBe("Ready as Swarmer. Waiting for your opponent."); + picker.destroy(); + }); + + it("lets a spectator watch the pick but not take part", () => { + const picker = make(); + show(picker, choosing({ players: [{ ...publicPlayers[0], style: "boxer" }, publicPlayers[1]], ready: ["one"] }), null, "spectator"); + expect(picker.element.hidden).toBe(false); + expect([...picker.element.querySelectorAll("[data-style]")].every((button) => button.disabled && button.getAttribute("aria-pressed") === "false")).toBe(true); + press("Digit2"); + press("Enter"); + card(picker, "boxer").click(); + expect(sent).toEqual([]); + expect(status(picker)).toBe("The fighters are choosing. One is ready."); + picker.destroy(); + }); + + it("stops listening for keys once destroyed", () => { + const added = vi.spyOn(window, "addEventListener"); + const removed = vi.spyOn(window, "removeEventListener"); + const picker = make(); + show(picker); + const listener = added.mock.calls.find(([type]) => type === "keydown")?.[1]; + picker.destroy(); + expect(listener).toBeDefined(); + expect(removed).toHaveBeenCalledWith("keydown", listener); + press("Digit1"); + expect(sent).toEqual(["balanced:false"]); + expect(parent.contains(picker.element)).toBe(false); + }); +}); diff --git a/web/hands/src/styles.ts b/web/hands/src/styles.ts new file mode 100644 index 00000000..f0b756ec --- /dev/null +++ b/web/hands/src/styles.ts @@ -0,0 +1,315 @@ +import { FIGHTER_STYLES } from "./protocol"; +import type { ConnectionRole, FighterStyle, SelectMessage } from "./types"; + +export interface StyleCard { + readonly style: FighterStyle; + readonly name: string; + /** Short enough for a name plate. */ + readonly tag: string; + readonly detail: string; + /** One to five for power, hand speed, footwork, reach, stamina and defence, in that order. */ + readonly stats: readonly [number, number, number, number, number, number]; +} + +export const STAT_NAMES = ["Power", "Hand speed", "Footwork", "Reach", "Stamina", "Defence"] as const; + +export const STYLE_CARDS: readonly StyleCard[] = [ + { style: "balanced", name: "Balanced", tag: "BALANCED", detail: "Even at everything, no holes to exploit.", stats: [3, 3, 3, 3, 3, 3] }, + { style: "boxer", name: "Boxer", tag: "BOXER", detail: "Snappy jab and long reach, lighter hands.", stats: [2, 4, 3, 4, 3, 3] }, + { style: "slugger", name: "Slugger", tag: "SLUGGER", detail: "Heavy hands, hard chin, slow feet, tires sooner.", stats: [5, 2, 2, 3, 2, 4] }, + { style: "swarmer", name: "Swarmer", tag: "SWARMER", detail: "Quick feet and head, digs to the body, never stops.", stats: [2, 4, 4, 3, 4, 3] }, + { style: "counter_puncher", name: "Counter-puncher", tag: "COUNTER", detail: "Slips and parries, makes every miss cost.", stats: [2, 3, 3, 3, 3, 5] }, +]; + +const cardOf = (style: FighterStyle): StyleCard => STYLE_CARDS.find((card) => card.style === style) ?? STYLE_CARDS[0]!; + +/** The name plate's tag for a style; the balanced fighter carries none. */ +export function styleTag(style: FighterStyle | undefined): string | null { + return style === undefined || style === "balanced" ? null : cardOf(style).tag; +} + +/** How the ring announcer says it: "the slugger". */ +export function styleTitle(style: FighterStyle | undefined): string | null { + return style === undefined || style === "balanced" ? null : `the ${cardOf(style).name.toLowerCase()}`; +} + +const STORAGE_KEY = "hands.style.v1"; + +/** The style the player picked last time, so it is ready again. Storage can be missing or blocked. */ +export function loadStyle(storage: Pick | null = safeStorage()): FighterStyle { + try { + const value = storage?.getItem(STORAGE_KEY); + return FIGHTER_STYLES.find((style) => style === value) ?? "balanced"; + } catch { + return "balanced"; + } +} + +export function saveStyle(style: FighterStyle, storage: Pick | null = safeStorage()): void { + try { + storage?.setItem(STORAGE_KEY, style); + } catch { + // Remembering the pick is a convenience; the bout goes ahead without it. + } +} + +function safeStorage(): Storage | null { + try { + return window.localStorage; + } catch { + return null; + } +} + +/** + * The pick closes on this page this long before the room's deadline: the deadline was measured when + * its message left the room, so this clock runs half a round trip behind, and a pick made in that last + * stretch would reach a room that had already started the bout with the earlier choice. + */ +const CLOSING_MS = 400; + +const STYLE_KEYS: Readonly> = { Digit1: 0, Digit2: 1, Digit3: 2, Digit4: 3, Digit5: 4, Numpad1: 0, Numpad2: 1, Numpad3: 2, Numpad4: 3, Numpad5: 4 }; + +/** How far the left stick has to go to move the choice, as a share of its travel. */ +const STICK_MOVE = 0.6; + +type PadIntent = "previous" | "next" | "settle"; + +/** The first connected controller with the standard layout, or null; the browser may refuse to say. */ +function standardPad(): Gamepad | null { + try { + return [...(navigator.getGamepads?.() ?? [])].find((item): item is Gamepad => item !== null && item.connected && item.mapping === "standard") ?? null; + } catch { + return null; + } +} + +/** What a standard controller asks of the pick: the D-pad or the left stick moves the choice, the bottom face button settles it. */ +function padIntents(pad: Gamepad | null): Set { + const intents = new Set(); + if (pad === null) return intents; + const pressed = (index: number): boolean => pad.buttons[index]?.pressed === true; + const x = pad.axes[0] ?? 0; + const y = pad.axes[1] ?? 0; + if (pressed(12) || pressed(14) || x < -STICK_MOVE || y < -STICK_MOVE) intents.add("previous"); + if (pressed(13) || pressed(15) || x > STICK_MOVE || y > STICK_MOVE) intents.add("next"); + if (pressed(0)) intents.add("settle"); + return intents; +} + +/** + * The pick of styles before the bout: five cards with their strengths, a countdown, and whether the + * other corner has settled (not on what). Arrow keys, the D-pad or the left stick move the choice, a + * number or a tap picks a card, and Enter or the controller's bottom face button settles on the one + * chosen; every change is sent so the deadline uses the latest. + */ +export class StylePicker { + readonly element: HTMLElement; + private readonly buttons = new Map(); + private readonly clock: HTMLElement; + private readonly status: HTMLElement; + private visible = false; + private choosing = false; + private highlighted: FighterStyle; + private settled = false; + private deadlineAt = 0; + private timer: number | null = null; + private padFrame = 0; + /** What the controller asked for last frame: a choice is made on the press, never while it is held. */ + private padHeld = new Set(); + private context: { readonly select: SelectMessage; readonly viewerId: string | null } | null = null; + + private readonly keydown = (event: KeyboardEvent): void => { + if (!this.visible || !this.choosing || event.repeat || event.ctrlKey || event.metaKey || event.altKey) return; + // A key pressed in Settings, or on any other control that has the focus, belongs to that control. + const target = event.target; + if (target instanceof HTMLElement && target !== document.body && !this.element.contains(target)) return; + const shortcut = STYLE_KEYS[event.code]; + if (shortcut !== undefined) { + event.preventDefault(); + this.pick(STYLE_CARDS[shortcut]!.style, true); + } else if (event.code === "ArrowRight" || event.code === "ArrowDown") { + event.preventDefault(); + this.move(this.neighbour(1)); + } else if (event.code === "ArrowLeft" || event.code === "ArrowUp") { + event.preventDefault(); + this.move(this.neighbour(-1)); + } else if ((event.code === "Enter" || event.code === "Space") && !(target instanceof HTMLButtonElement)) { + // On a focused card the browser's own click settles that card. + event.preventDefault(); + this.pick(this.highlighted, true); + } + }; + + constructor( + parent: HTMLElement, + private readonly send: (style: FighterStyle, ready: boolean) => boolean, + private readonly now: () => number = () => performance.now(), + preferred: FighterStyle = loadStyle(), + ) { + this.highlighted = preferred; + const section = document.createElement("section"); + section.className = "style-picker"; + section.dataset.stylePicker = ""; + section.hidden = true; + section.setAttribute("aria-label", "Pick how your fighter boxes"); + const head = document.createElement("header"); + const title = document.createElement("p"); + title.className = "style-title"; + title.textContent = "CHOOSE YOUR STYLE"; + this.clock = document.createElement("p"); + this.clock.className = "style-clock"; + head.append(title, this.clock); + const row = document.createElement("div"); + row.className = "style-cards"; + for (const [index, card] of STYLE_CARDS.entries()) { + const button = document.createElement("button"); + button.type = "button"; + button.dataset.style = card.style; + button.setAttribute("aria-pressed", "false"); + const key = document.createElement("kbd"); + key.textContent = String(index + 1); + const name = document.createElement("strong"); + name.textContent = card.name; + const detail = document.createElement("span"); + detail.textContent = card.detail; + const stats = document.createElement("ul"); + stats.className = "style-stats"; + for (const [statIndex, value] of card.stats.entries()) { + const item = document.createElement("li"); + item.setAttribute("aria-label", `${STAT_NAMES[statIndex]} ${value} of 5`); + const label = document.createElement("small"); + label.textContent = STAT_NAMES[statIndex]!; + const bar = document.createElement("i"); + bar.dataset.value = String(value); + item.append(label, bar); + stats.append(item); + } + button.append(key, name, detail, stats); + button.addEventListener("click", () => this.pick(card.style, true)); + row.append(button); + this.buttons.set(card.style, button); + } + this.status = document.createElement("p"); + this.status.className = "style-status"; + this.status.setAttribute("role", "status"); + section.append(head, row, this.status); + parent.append(section); + this.element = section; + window.addEventListener("keydown", this.keydown); + } + + /** Shows the pick while the room is choosing; `role` decides whether this viewer may choose. */ + update(select: SelectMessage | null, viewerId: string | null, role: ConnectionRole | null): void { + if (select === null) { + this.setVisible(false); + this.context = null; + this.settled = false; + return; + } + // A spectator seated before the bell begins choosing then. + const begun = this.context === null || (!this.choosing && role === "fighter"); + if (this.context?.select !== select) this.deadlineAt = this.now() + select.deadline_ms - CLOSING_MS; + this.context = { select, viewerId }; + this.choosing = role === "fighter" && viewerId !== null; + // A settled pick is final, here as in the room, even before the room has echoed it. + this.settled = this.choosing && (this.settled || select.ready.includes(viewerId!)); + // After a reconnect the room knows the settled pick better than this page does. + const own = select.players.find((player) => player.id === viewerId)?.style; + if (this.settled && own !== undefined) this.highlighted = own; + // Last time's style goes in as soon as the pick begins, so the deadline uses it unless changed. + if (begun && this.choosing && !this.settled) this.send(this.highlighted, false); + this.setVisible(true); + this.render(); + } + + private pick(style: FighterStyle, ready: boolean): void { + if (!this.choosing || this.settled || this.closed) return; + this.highlighted = style; + if (!this.send(style, ready)) return; + // Remembered for next time from the room's ready, which says the style he actually boxes in. + if (ready) this.settled = true; + this.render(); + } + + /** Too late for a pick to reach the room before it closes. */ + private get closed(): boolean { + return this.now() >= this.deadlineAt; + } + + /** The card beside the lit one, wrapping round the row. */ + private neighbour(step: 1 | -1): FighterStyle { + const index = STYLE_CARDS.findIndex((card) => card.style === this.highlighted); + return STYLE_CARDS[(index + step + STYLE_CARDS.length) % STYLE_CARDS.length]!.style; + } + + /** Moves the choice, and the keyboard focus with it, so Enter, Space and Tab all mean the card that is lit. */ + private move(style: FighterStyle): void { + this.pick(style, false); + if (!this.settled) this.buttons.get(this.highlighted)?.focus(); + } + + private render(): void { + const context = this.context; + if (context === null) return; + const { select, viewerId } = context; + for (const card of STYLE_CARDS) { + const button = this.buttons.get(card.style)!; + const mine = this.choosing && card.style === this.highlighted; + button.disabled = !this.choosing || this.settled || this.closed; + button.setAttribute("aria-pressed", String(mine)); + button.toggleAttribute("data-chosen", mine); + // Tab reaches only the lit card; the arrows move between them. + button.tabIndex = card.style === this.highlighted ? 0 : -1; + } + const seconds = Math.max(0, Math.ceil((this.deadlineAt - this.now()) / 1000)); + const clock = `${seconds}s`; + if (this.clock.textContent !== clock) this.clock.textContent = clock; + // Who has settled, never on what: the styles are revealed together at the bell, since showing a + // settled one would reward waiting to pick its counter. Who has dropped, while the pick goes on. + const named = select.players + .filter((player) => player.id !== viewerId && (!player.connected || select.ready.includes(player.id))) + .map((player) => (player.connected ? `${player.name} is ready.` : `Waiting for ${player.name} to reconnect.`)) + .join(" "); + const text = !this.choosing + ? named.length > 0 ? `The fighters are choosing. ${named}` : "The fighters are choosing their styles." + : this.settled + ? named.length > 0 ? `Ready as ${cardOf(this.highlighted).name}. ${named}` : `Ready as ${cardOf(this.highlighted).name}. Waiting for your opponent.` + : named.length > 0 ? `${named} Tap a style or press Enter to settle on yours.` : "Tap a style, or use the arrow keys and Enter."; + if (this.status.textContent !== text) this.status.textContent = text; + } + + private setVisible(visible: boolean): void { + if (this.visible === visible) return; + this.visible = visible; + this.element.hidden = !visible; + if (visible) { + this.timer = window.setInterval(() => this.render(), 250); + // A button or stick still held as the pick opens (the last punch of the bout before) is not a choice. + this.padHeld = padIntents(standardPad()); + this.padFrame = requestAnimationFrame(() => this.pollPad()); + } else { + if (this.timer !== null) window.clearInterval(this.timer); + this.timer = null; + cancelAnimationFrame(this.padFrame); + } + } + + private pollPad(): void { + if (!this.visible) return; + const intents = padIntents(standardPad()); + for (const intent of intents) { + if (this.padHeld.has(intent)) continue; + if (intent === "settle") this.pick(this.highlighted, true); + else this.move(this.neighbour(intent === "next" ? 1 : -1)); + } + this.padHeld = intents; + this.padFrame = requestAnimationFrame(() => this.pollPad()); + } + + destroy(): void { + this.setVisible(false); + window.removeEventListener("keydown", this.keydown); + this.element.remove(); + } +} diff --git a/web/hands/src/test/fixtures.ts b/web/hands/src/test/fixtures.ts index 698321e3..c5ae096a 100644 --- a/web/hands/src/test/fixtures.ts +++ b/web/hands/src/test/fixtures.ts @@ -1,5 +1,40 @@ -import type { EngineSnapshot, FighterSnapshot } from "../types"; -export const fighter = (id: string, x = 0): FighterSnapshot => ({ player_id: id, x, y: 0, facing: id === "one" ? 1 : -1, facing_x: id === "one" ? 1000 : -1000, facing_y: 0, velocity_x: 0, velocity_y: 0, stance: "orthodox", defense: "none", stamina: 900, maximum_stamina: 1000, conditioning: 850, guard: 650, poise: 550, trauma: { head: 0, body: 0, left_eye: 0, right_eye: 0, left_cut: 0, right_cut: 0, swelling: 0, bleeding: 0 }, knockdowns: 0, warnings: 0, deductions: 0, stunned_ticks: 0, is_downed: false, action: null, action_hand: null, action_target: null, action_power: null, action_id: null, action_key: null, action_start_tick: 0, action_startup_ticks: 0, action_active_ticks: 0, action_recovery_ticks: 0, action_contact_tick: null, queued_actions: 0, clinch_startup_ticks: 0, clinch_ticks: 0, is_foul_recovery_target: false, taunt_ticks: 0, get_up_prompt: null, get_up_meter: 0, get_up_required: 0, get_up_count: 0, get_up_window_start_tick: 0, get_up_window_end_tick: 0, last_input_sequence: -1 }); +import { PROTOCOL_VERSION, type EngineSnapshot, type FighterSnapshot } from "../types"; +export const fighter = (id: string, x = 0): FighterSnapshot => ({ player_id: id, x, y: 0, facing: id === "one" ? 1 : -1, facing_x: id === "one" ? 1000 : -1000, facing_y: 0, velocity_x: 0, velocity_y: 0, stance: "orthodox", style: "balanced", defense: "none", stamina: 900, maximum_stamina: 1000, conditioning: 850, guard: 650, poise: 550, trauma: { head: 0, body: 0, left_eye: 0, right_eye: 0, left_cut: 0, right_cut: 0, swelling: 0, bleeding: 0 }, knockdowns: 0, warnings: 0, deductions: 0, stunned_ticks: 0, is_downed: false, action: null, action_hand: null, action_target: null, action_power: null, action_id: null, action_key: null, action_start_tick: 0, action_startup_ticks: 0, action_active_ticks: 0, action_recovery_ticks: 0, action_contact_tick: null, queued_actions: 0, clinch_startup_ticks: 0, clinch_ticks: 0, is_foul_recovery_target: false, taunt_ticks: 0, corner_choice: null, get_up_prompt: null, get_up_meter: 0, get_up_required: 0, get_up_count: 0, get_up_window_start_tick: 0, get_up_window_end_tick: 0, last_input_sequence: -1 }); export const snapshot = (tick = 10): EngineSnapshot => ({ tick, phase: "fight", round_number: 1, phase_ticks_remaining: 5000, fighters: [fighter("one", -100), fighter("two", 100)], events: [], result: null, checksum: "a".repeat(64) }); -export const envelope = (tick = 10): Record => ({ version: 3, type: "snapshot", payload: snapshot(tick) }); +export const envelope = (tick = 10): Record => ({ version: PROTOCOL_VERSION, type: "snapshot", payload: snapshot(tick) }); export const publicPlayers = [{ id: "one", name: "One", avatar: null, rating: 1500, connected: true }, { id: "two", name: "Two", avatar: null, rating: 1500, connected: true }] as const; + +export interface DrawnPicture { image: unknown; x: number; y: number; width: number; height: number } + +export function mockHudContext(texts: string[], drawn?: DrawnPicture[], arcs?: Array<{ x: number; y: number; radius: number }>): CanvasRenderingContext2D { + const gradient = { addColorStop: () => {} }; + return { + save: () => {}, + restore: () => {}, + beginPath: () => {}, + closePath: () => {}, + arc: (x: number, y: number, radius: number) => arcs?.push({ x, y, radius }), + clip: () => {}, + setTransform: () => {}, + scale: () => {}, + drawImage: (image: unknown, x: number, y: number, width: number, height: number) => drawn?.push({ image, x, y, width, height }), + moveTo: () => {}, + lineTo: () => {}, + fill: () => {}, + stroke: () => {}, + fillRect: () => {}, + strokeRect: () => {}, + clearRect: () => {}, + fillText: (text: string) => texts.push(text), + strokeText: () => undefined, + measureText: (text: string) => ({ width: text.length * 7 }), + createLinearGradient: () => gradient, + createRadialGradient: () => gradient, + set fillStyle(_value: unknown) {}, + set strokeStyle(_value: unknown) {}, + set font(_value: string) {}, + set lineWidth(_value: number) {}, + set textAlign(_value: CanvasTextAlign) {}, + set textBaseline(_value: CanvasTextBaseline) {}, + } as unknown as CanvasRenderingContext2D; +} diff --git a/web/hands/src/test/gore-bout.json b/web/hands/src/test/gore-bout.json new file mode 100644 index 00000000..c43b592d --- /dev/null +++ b/web/hands/src/test/gore-bout.json @@ -0,0 +1 @@ +{"about":"Real engine bout (merged #293/#294 engine at 6333068): scripts/hands_balance.py's skilled player against a contender computer, seed 1. A cut opens 21 s into round 1, a knockdown late in round 1, and a decision after three rounds. Ticks at 30 a second from the opening bell; samples every sampleTicks: [x0, y0, x1, y1, bleeding plus both cuts of each fighter, downed bits]; events: [tick, kind (h hit, c counter, b block, p perfect block, k knockdown, l bleed), recipient, amount, blood, punch and target, direction], blocks and knockdowns already carrying their punch's blood and detail.","sampleTicks":15,"rounds":[0,4350,8400],"samples":[[-180,0,180,0,0,0,0],[-91,-39,94,27,0,0,0],[-46,-57,68,38,0,0,0],[-31,-59,65,67,0,0,0],[-9,-71,55,63,0,0,0],[-11,-71,54,67,0,0,0],[-14,-47,80,120,0,0,0],[-20,10,110,106,0,0,0],[-23,23,102,84,0,0,0],[-50,47,94,66,0,0,0],[-52,43,81,69,0,0,0],[-54,40,70,76,0,0,0],[-51,16,82,79,0,0,0],[-40,-8,82,79,0,0,0],[-41,-10,69,38,0,0,0],[-52,-8,82,16,0,0,0],[-61,15,78,-1,0,0,0],[-62,17,87,-7,0,0,0],[-62,20,86,-5,0,0,0],[-63,21,59,23,0,0,0],[-67,17,74,63,0,0,0],[-48,-12,82,78,0,0,0],[-34,-33,27,66,0,0,0],[-28,-61,15,82,0,0,0],[8,-39,-15,133,0,0,0],[-29,4,-57,176,0,0,0],[-73,46,-100,218,0,0,0],[-116,88,-143,260,0,0,0],[-149,113,-173,252,0,0,0],[-155,91,-186,217,0,0,0],[-129,83,-191,222,0,0,0],[-129,83,-233,225,0,0,0],[-141,149,-287,242,0,0,0],[-162,206,-309,298,0,0,0],[-192,258,-364,288,0,0,0],[-244,234,-416,258,0,0,0],[-286,203,-448,212,0,0,0],[-307,167,-455,162,0,0,0],[-301,163,-416,136,0,0,0],[-275,132,-432,101,0,0,0],[-311,149,-442,43,0,0,0],[-363,145,-429,12,0,0,0],[-368,153,-391,35,0,0,0],[-359,167,-367,54,0,27,0],[-356,169,-329,11,0,27,0],[-324,140,-283,-29,0,27,0],[-278,102,-237,-67,0,27,0],[-232,64,-191,-105,0,27,0],[-191,18,-216,-155,0,27,0],[-172,-42,-265,-190,0,27,0],[-224,-66,-321,-210,0,27,0],[-281,-86,-378,-230,0,27,0],[-333,-111,-366,-270,0,27,0],[-356,-120,-336,-248,0,27,0],[-373,-107,-330,-237,0,27,0],[-374,-107,-330,-234,0,58,0],[-370,-99,-330,-234,0,60,0],[-341,-74,-329,-205,0,60,0],[-316,-48,-354,-153,0,60,0],[-292,-36,-375,-166,0,60,0],[-331,-53,-363,-224,0,60,0],[-368,-102,-328,-272,0,60,0],[-367,-155,-277,-303,0,60,0],[-331,-189,-221,-323,0,60,0],[-297,-222,-162,-333,0,60,0],[-238,-226,-102,-335,0,60,0],[-178,-228,-42,-337,0,60,0],[-118,-231,18,-340,0,60,0],[-58,-238,46,-378,0,60,0],[0,-255,101,-395,0,60,0],[-2,-288,110,-358,0,62,0],[-6,-289,114,-355,0,62,0],[-10,-291,108,-341,0,62,0],[-16,-294,123,-308,0,64,0],[5,-325,169,-269,0,64,0],[39,-280,207,-231,0,64,0],[86,-237,259,-261,0,64,0],[145,-217,294,-309,0,64,0],[206,-216,346,-319,0,64,0],[256,-209,379,-304,0,64,0],[251,-239,399,-267,0,64,0],[249,-245,361,-234,0,64,0],[232,-281,388,-228,0,64,0],[263,-257,416,-176,0,64,0],[296,-225,422,-131,0,64,0],[296,-218,398,-96,0,64,0],[265,-204,350,-96,0,64,0],[244,-203,334,-109,0,66,0],[238,-179,388,-98,0,66,0],[260,-126,412,-43,0,66,0],[303,-127,384,-19,0,66,0],[314,-127,371,31,0,68,0],[358,-89,344,85,0,68,0],[381,-31,298,123,0,68,0],[381,32,258,156,0,68,0],[324,54,257,215,0,68,0],[277,95,272,269,0,68,0],[239,142,237,309,0,68,0],[227,151,207,299,0,68,0],[208,135,189,268,0,68,0],[215,128,184,267,0,102,0],[219,145,137,301,0,102,0],[216,210,78,318,0,102,0],[189,265,19,311,0,102,0],[142,306,-32,280,0,102,0],[97,270,-78,270,0,102,0],[43,244,-120,313,0,102,0],[-21,240,-141,368,0,102,0],[-78,255,-190,390,0,102,0],[-136,270,-249,402,0,102,0],[-149,315,-300,390,0,102,0],[-170,348,-345,352,0,102,0],[-210,326,-382,312,0,102,0],[-245,293,-417,265,0,102,0],[-278,258,-447,213,0,102,0],[-305,220,-462,158,0,102,0],[-317,185,-462,100,0,102,0],[-326,146,-462,41,0,102,0],[-336,102,-462,-19,0,102,0],[-343,53,-457,-78,0,102,0],[-383,38,-432,-102,0,102,0],[-386,41,-404,-82,0,102,0],[-389,46,-391,-66,0,102,0],[-390,48,-371,-87,0,108,0],[-401,32,-324,-125,0,108,0],[-353,4,-270,-151,0,108,0],[-300,-23,-215,-176,0,108,0],[-245,-48,-161,-201,0,108,0],[-218,-57,-149,-187,0,113,0],[-217,-56,-144,-188,0,113,0],[-217,-56,-132,-193,0,113,0],[-210,-94,-75,-210,0,113,0],[-186,-153,-31,-235,0,113,0],[-175,-183,-58,-252,0,113,0],[-188,-185,-71,-258,0,113,0],[-189,-186,-77,-237,0,143,0],[-191,-184,-55,-207,0,149,0],[-217,-164,-97,-166,0,149,0],[-247,-135,-115,-199,0,149,0],[-243,-133,-92,-256,0,149,0],[-219,-200,-74,-344,0,149,0],[-197,-272,-15,-382,0,149,0],[-152,-330,64,-351,0,149,0],[-85,-363,119,-290,0,149,0],[-11,-366,142,-212,0,149,0],[7,-296,142,-129,0,149,0],[15,-222,130,-54,0,149,0],[14,-189,91,-67,0,156,0],[9,-169,82,-9,0,156,0],[-2,-109,69,51,0,156,0],[40,-69,49,107,0,156,0],[70,-15,8,150,0,156,0],[75,48,-47,174,0,156,0],[67,94,-76,165,0,156,0],[67,102,-40,125,0,162,0],[84,80,-72,110,0,162,0],[68,71,-131,75,0,162,0],[4,32,-183,25,0,162,0],[-16,10,-170,17,0,162,0],[-16,10,-140,-8,0,197,0],[-14,11,-143,-47,0,204,0],[-25,33,-151,-66,0,204,0],[-71,29,-177,-110,0,204,0],[-127,12,-235,-126,0,204,0],[-185,-5,-293,-142,0,204,0],[-201,-61,-352,-150,0,204,0],[-234,-112,-409,-134,0,204,0],[-274,-140,-431,-89,0,204,0],[-282,-147,-373,-78,0,204,0],[-278,-178,-371,-54,0,204,0],[-274,-141,-377,0,0,204,0],[-274,-82,-375,60,0,204,0],[-274,-24,-372,120,0,204,0],[-272,34,-369,180,0,204,0],[-268,55,-323,179,0,211,0],[-285,62,-304,235,0,211,0],[-317,114,-266,281,0,211,0],[-326,174,-213,308,0,211,0],[-310,209,-191,293,0,211,0],[-310,209,-192,290,0,219,0],[-310,209,-147,286,0,219,0],[-262,201,-70,265,0,219,0],[-189,183,14,234,0,219,0],[-118,159,93,195,0,219,0],[-50,129,165,147,0,219,0],[17,157,179,105,0,219,0],[33,167,140,99,0,219,0],[26,192,106,100,0,219,0],[24,204,105,100,0,227,0],[44,217,157,87,0,227,0],[60,163,216,81,0,227,0],[97,114,273,100,0,227,0],[148,79,323,90,0,227,0],[193,49,348,57,0,227,0],[195,44,305,30,0,227,0],[178,69,327,6,0,227,0],[192,83,293,2,0,234,0],[198,94,298,-32,0,234,0],[226,102,262,-26,0,234,0],[229,107,238,-5,0,234,0],[207,114,276,-33,0,234,0],[198,105,276,24,0,234,0],[173,107,298,28,0,234,0],[159,88,282,56,0,234,0],[156,87,275,65,0,234,0],[156,87,268,73,0,234,0],[153,88,283,96,0,234,0],[143,96,279,98,0,234,0],[143,97,293,135,0,272,0],[150,122,282,170,0,272,0],[148,119,259,177,0,272,0],[148,118,238,188,0,310,0],[125,126,245,222,0,318,2],[58,158,245,222,0,318,2],[-10,191,245,222,0,318,2],[-77,224,245,222,0,318,2],[-145,257,245,222,0,318,2],[-212,290,245,222,0,318,2],[-280,323,245,222,0,318,2],[-348,357,245,222,0,318,2],[-362,364,245,222,0,318,2],[-362,364,245,222,0,318,2],[-362,364,245,222,0,318,2],[-362,364,245,222,0,318,0],[-362,364,245,222,0,318,0],[-362,364,245,222,0,318,0],[-362,364,245,222,0,318,0],[-362,364,245,222,0,318,0],[-362,364,245,222,0,318,0],[-362,364,245,222,0,318,0],[-362,364,245,222,0,318,0],[-362,364,245,222,0,318,0],[-348,351,236,238,0,318,0],[-290,304,189,271,0,318,0],[-226,265,113,305,0,318,0],[-166,305,33,330,0,318,0],[-148,324,-36,358,0,327,0],[-150,325,-25,370,0,327,0],[-150,325,-21,368,0,327,0],[-152,326,-29,359,0,362,0],[-154,325,-18,369,0,362,0],[-161,316,-39,336,0,371,0],[-196,288,-66,293,0,371,0],[-192,261,-19,257,0,371,0],[-145,225,29,220,0,371,0],[-91,244,71,178,0,371,0],[-31,249,93,123,0,371,0],[29,230,125,84,0,371,0],[84,209,182,64,0,371,0],[140,188,238,44,0,371,0],[151,131,297,33,0,371,0],[180,78,338,47,0,371,0],[183,72,306,66,0,380,0],[181,71,296,78,0,419,0],[179,68,321,118,0,429,0],[191,89,352,171,0,429,0],[222,144,383,215,0,429,0],[259,188,432,189,0,429,0],[296,209,453,134,0,429,0],[330,190,462,75,0,429,0],[340,141,462,16,0,429,0],[280,97,443,87,0,429,0],[219,54,423,159,0,427,0],[158,10,402,232,0,427,0],[97,-34,382,304,0,425,0],[36,-77,365,365,0,425,0],[-25,-121,365,365,0,423,0],[-86,-165,365,365,0,423,0],[-147,-209,365,365,0,421,0],[-208,-252,365,365,0,421,0],[-269,-296,365,365,0,419,0],[-330,-340,365,365,0,419,0],[-365,-365,365,365,0,417,0],[-365,-365,365,365,0,417,0],[-365,-365,365,365,0,415,0],[-365,-365,365,365,0,415,0],[-365,-365,365,365,0,413,0],[-365,-365,365,365,0,413,0],[-365,-365,365,365,0,411,0],[-365,-365,365,365,0,411,0],[-365,-365,365,365,0,409,0],[-365,-365,365,365,0,409,0],[-365,-365,365,365,0,407,0],[-365,-365,365,365,0,407,0],[-365,-365,365,365,0,405,0],[-365,-365,365,365,0,405,0],[-365,-365,365,365,0,403,0],[-365,-365,365,365,0,403,0],[-365,-365,365,365,0,401,0],[-365,-365,365,365,0,401,0],[-365,-365,365,365,0,399,0],[-311,-318,314,321,0,399,0],[-246,-274,249,281,0,399,0],[-161,-243,174,243,0,399,0],[-82,-201,105,196,0,399,0],[-84,-114,46,139,0,399,0],[-82,-32,34,69,0,399,0],[-88,-27,16,31,0,399,0],[-113,-22,23,40,0,441,0],[-103,-1,48,94,0,441,0],[-42,4,101,104,0,441,0],[19,2,161,103,0,441,0],[78,0,206,93,0,441,0],[105,-20,206,92,0,441,0],[106,-21,243,97,0,483,0],[136,39,302,94,0,483,0],[182,84,356,66,0,483,0],[240,106,391,19,0,483,0],[304,102,403,-40,0,483,0],[358,73,390,-98,0,483,0],[336,22,361,-150,0,483,0],[307,-31,333,-203,0,483,0],[279,-84,305,-256,0,483,0],[251,-137,277,-309,0,483,0],[223,-189,248,-362,0,483,0],[254,-235,219,-384,0,483,0],[262,-236,208,-339,0,483,0],[301,-221,208,-316,0,483,0],[295,-226,153,-318,0,495,0],[260,-260,93,-309,0,495,0],[206,-243,37,-287,0,495,0],[150,-221,-18,-265,0,495,0],[95,-199,-74,-242,0,495,0],[39,-176,-129,-220,0,495,0],[-17,-161,-151,-253,0,495,0],[-16,-193,-128,-276,0,495,0],[-11,-202,-121,-322,0,506,0],[-10,-255,-132,-377,0,506,0],[-28,-313,-191,-376,0,506,0],[-54,-353,-195,-355,0,506,0],[-44,-347,-176,-331,0,506,0],[-8,-314,-140,-302,0,506,0],[2,-306,-139,-262,0,517,0],[-10,-266,-174,-214,0,517,0],[-43,-218,-209,-165,0,517,0],[-61,-224,-189,-161,0,529,0],[-84,-238,-204,-116,0,529,0],[-142,-220,-203,-56,0,529,0],[-188,-177,-177,-3,0,529,0],[-213,-122,-141,25,0,529,0],[-205,-119,-130,-10,0,529,0],[-193,-156,-139,-27,0,529,0],[-164,-195,-166,-68,0,529,0],[-156,-205,-176,-88,0,529,0],[-154,-208,-185,-91,0,529,0],[-154,-211,-189,-90,0,529,0],[-155,-214,-190,-95,0,529,0],[-155,-215,-171,-97,0,571,0],[-158,-218,-136,-49,0,583,0],[-178,-165,-131,3,0,583,0],[-140,-119,-160,55,0,583,0],[-118,-59,-207,91,0,583,0],[-122,3,-265,104,0,583,0],[-152,59,-324,94,0,583,0],[-202,98,-372,60,0,583,0],[-241,57,-409,13,0,583,0],[-269,13,-409,-30,0,583,0],[-261,12,-364,-45,0,583,0],[-230,-11,-381,-80,0,583,0],[-253,-60,-414,-124,0,583,0],[-301,-53,-403,-156,0,583,0],[-302,-43,-360,-147,0,583,0],[-319,9,-328,-117,0,583,0],[-336,31,-305,-75,0,583,0],[-363,40,-271,-94,0,583,0],[-316,27,-213,-112,0,583,0],[-285,21,-192,-94,0,596,0],[-256,19,-134,-105,0,596,0],[-196,10,-74,-114,0,596,0],[-136,1,-14,-124,0,596,0],[-115,-55,45,-126,0,596,0],[-74,-104,100,-103,0,596,0],[-18,-132,141,-60,0,596,0],[42,-139,181,-55,0,596,0],[52,-145,156,-104,0,608,0],[25,-125,191,-121,0,608,0],[72,-96,233,-163,0,608,0],[135,-91,255,-218,0,608,0],[195,-113,253,-278,0,608,0],[215,-122,270,-256,0,608,0],[201,-144,298,-261,0,608,0],[186,-152,277,-215,0,608,0],[170,-152,281,-208,0,608,0],[170,-152,280,-196,0,608,0],[166,-152,313,-244,0,620,0],[224,-159,325,-302,0,620,0],[280,-189,313,-360,0,620,0],[307,-219,278,-345,0,633,0],[300,-220,229,-368,0,633,0],[271,-249,183,-398,0,633,0],[216,-271,127,-419,0,633,0],[162,-290,69,-434,0,633,0],[137,-294,42,-400,0,633,0],[134,-275,34,-360,0,633,0],[139,-247,46,-323,0,646,0],[142,-247,46,-323,0,646,0],[142,-247,-9,-320,0,690,0],[109,-289,-66,-299,0,690,0],[54,-320,-109,-259,0,690,0],[-5,-326,-133,-204,0,690,0],[-20,-270,-143,-145,0,690,0],[-29,-212,-151,-86,0,690,0],[-39,-152,-180,-47,0,690,0],[-68,-97,-238,-56,0,690,0],[-117,-58,-289,-87,0,690,0],[-158,-95,-329,-132,0,690,0],[-197,-139,-368,-177,0,690,0],[-241,-180,-416,-167,0,690,0],[-287,-199,-441,-114,0,690,0],[-335,-186,-452,-55,0,690,0],[-341,-138,-435,-18,0,690,0],[-338,-134,-390,-31,0,690,0],[-326,-141,-382,-34,0,733,0],[-322,-143,-359,2,0,733,0],[-318,-129,-376,50,0,733,0],[-362,-72,-349,103,0,733,0],[-384,-15,-303,141,0,733,0],[-383,47,-246,158,0,733,0],[-356,104,-187,152,0,733,0],[-308,146,-136,122,0,733,0],[-267,111,-94,79,0,733,0],[-226,68,-53,35,0,733,0],[-185,26,-13,-10,0,733,0],[-144,-18,27,-55,0,733,0],[-115,-54,45,-63,0,733,0],[-112,-52,-3,-38,0,747,0],[-132,-34,21,-8,0,747,0],[-132,-28,-30,17,0,747,0],[-163,-19,-27,17,0,747,0],[-157,7,17,-10,0,747,0],[-113,-29,61,-51,0,747,0],[-73,-67,91,-87,0,747,0],[-68,-77,69,-112,0,747,0],[-95,-86,20,-124,0,747,0],[-107,-89,2,-127,0,761,0],[-123,-65,11,-114,0,761,0],[-130,-43,-9,-76,0,775,0],[-139,-33,4,-56,0,775,0],[-125,-8,21,-21,0,775,0],[-124,-6,-15,9,0,775,0],[-138,-31,8,42,0,775,0],[-117,-40,-27,54,0,775,0],[-118,-55,-47,50,0,775,0],[-96,-70,5,63,0,775,0],[-83,-71,-15,23,0,775,0],[-106,-79,-23,40,0,775,0],[-122,-56,-47,70,0,775,0],[-123,-56,-90,49,0,775,0],[-145,-70,-55,66,0,775,0],[-154,-67,-58,27,0,775,0],[-156,-101,-64,8,0,775,0],[-156,-103,-65,-8,0,775,0],[-155,-115,-63,-11,0,819,0],[-155,-117,-52,-50,0,819,0],[-157,-120,-50,-80,0,834,0],[-181,-106,-15,-96,0,834,0],[-137,-79,29,-136,0,834,0],[-76,-71,55,-190,0,834,0],[-15,-89,57,-250,0,834,0],[32,-128,63,-300,0,834,0],[19,-186,112,-335,0,834,0],[25,-248,170,-347,0,834,0],[57,-302,228,-336,0,834,0],[105,-339,277,-303,0,834,0],[166,-354,309,-253,0,834,0],[188,-300,333,-203,0,834,0],[218,-248,388,-205,0,834,0],[256,-203,424,-158,0,834,0],[269,-179,392,-128,0,834,0],[259,-177,368,-104,0,834,0],[254,-211,336,-103,0,834,0],[260,-224,322,-105,0,834,0],[266,-245,310,-115,0,834,0],[273,-254,310,-115,0,834,0],[272,-256,281,-135,0,834,0],[271,-258,281,-136,0,850,0],[270,-258,276,-135,0,850,0],[268,-261,276,-135,0,850,0],[268,-261,270,-143,0,850,0],[267,-263,265,-149,0,850,0],[267,-263,262,-146,0,850,0],[261,-274,262,-146,0,850,0],[257,-274,238,-117,0,850,0],[257,-256,216,-122,0,850,0],[259,-256,185,-126,0,865,0],[232,-254,144,-108,0,865,0],[191,-240,102,-91,0,865,0],[150,-223,61,-74,0,865,0],[121,-214,47,-108,0,865,0],[117,-219,11,-108,0,880,0],[117,-190,-33,-105,0,880,0],[94,-151,-77,-116,0,880,0],[59,-121,-110,-138,0,880,0],[49,-111,-70,-148,0,880,0],[54,-104,-67,-148,0,880,0],[54,-104,-79,-165,0,880,0],[80,-116,-39,-168,0,880,0],[118,-131,-10,-156,0,880,0],[152,-156,27,-140,0,880,0],[178,-191,64,-135,0,880,0],[219,-186,104,-132,0,880,0],[262,-175,148,-127,0,880,0],[306,-169,200,-115,0,880,0],[350,-159,230,-141,0,880,0],[388,-136,272,-160,0,880,0],[416,-104,304,-166,0,880,0],[440,-80,341,-162,0,880,0],[462,-57,376,-148,0,880,0],[462,-49,384,-96,0,880,0],[462,-49,385,-92,0,880,0],[462,-49,385,-92,0,880,0],[462,-28,386,-88,0,880,0],[462,-20,385,-88,0,880,0],[462,-20,384,-88,0,880,0],[462,21,384,-88,0,880,0],[462,18,372,-75,0,880,0],[462,25,375,-55,0,880,0],[462,34,374,-50,0,880,0],[462,39,375,-23,0,880,0],[462,42,376,-18,0,880,0],[462,82,376,-18,0,880,0],[462,124,374,45,0,880,0],[462,168,371,78,0,880,0],[462,211,374,127,0,880,0],[462,254,371,182,0,880,0],[453,280,371,205,0,880,0],[438,295,349,197,0,880,0],[423,310,350,228,0,880,0],[408,325,352,256,0,880,0],[393,340,352,258,0,880,0],[378,355,334,269,0,880,0],[325,354,351,282,0,315,0],[269,315,363,336,0,313,0],[218,260,365,365,0,313,0],[167,205,365,365,0,311,0],[116,150,365,365,0,311,0],[64,96,365,365,0,309,0],[13,41,365,365,0,309,0],[-38,-14,365,365,0,307,0],[-89,-69,365,365,0,307,0],[-140,-124,365,365,0,305,0],[-191,-179,365,365,0,305,0],[-243,-234,365,365,0,303,0],[-294,-289,365,365,0,303,0],[-345,-344,365,365,0,301,0],[-365,-365,365,365,0,301,0],[-365,-365,365,365,0,299,0],[-365,-365,365,365,0,299,0],[-365,-365,365,365,0,297,0],[-365,-365,365,365,0,297,0],[-365,-365,365,365,0,295,0],[-365,-365,365,365,0,295,0],[-365,-365,365,365,0,293,0],[-365,-365,365,365,0,293,0],[-365,-365,365,365,0,291,0],[-365,-365,365,365,0,291,0],[-365,-365,365,365,0,289,0],[-365,-365,365,365,0,289,0],[-365,-365,365,365,0,287,0],[-365,-365,365,365,0,287,0],[-365,-365,365,365,0,285,0],[-322,-324,324,329,0,285,0],[-269,-286,272,295,0,285,0],[-198,-263,209,265,0,285,0],[-129,-233,162,221,0,285,0],[-119,-161,148,153,0,285,0],[-101,-88,124,87,0,285,0],[-72,-21,67,68,0,285,0],[-71,-12,7,60,0,293,0],[-85,-14,-2,58,0,293,0],[-85,-14,-12,55,0,293,0],[-86,-17,-21,52,0,293,0],[-86,-19,-22,55,0,293,0],[-86,-22,-24,68,0,293,0],[-87,-26,-30,76,0,332,0],[-88,-26,-39,120,0,332,0],[-106,0,-54,163,0,332,0],[-122,41,-68,206,0,332,0],[-136,83,-83,249,0,332,0],[-151,125,-88,288,0,332,0],[-116,150,-49,311,0,332,0],[-77,173,-11,334,0,332,0],[-39,196,28,358,0,332,0],[1,220,66,381,0,332,0],[35,235,90,369,0,332,0],[36,235,107,358,0,332,0],[14,235,96,318,0,332,0],[6,233,93,307,0,332,0],[6,233,128,310,0,332,0],[8,241,154,298,0,332,0],[11,241,143,274,0,332,0],[-6,215,119,248,0,332,0],[-22,192,91,220,0,332,0],[-34,183,68,197,0,332,0],[-69,154,41,172,0,332,0],[-71,152,8,165,0,332,0],[-101,164,2,169,0,332,0],[-101,164,-24,182,0,332,0],[-114,168,-25,182,0,332,0],[-115,169,-15,193,0,373,0],[-117,165,10,230,0,373,0],[-93,151,25,274,0,373,0],[-52,161,26,318,0,373,0],[-13,188,12,361,0,373,0],[17,225,-3,397,0,373,0],[-10,257,-36,425,0,373,0],[-39,260,-16,392,0,373,0],[-73,243,5,365,0,373,0],[-85,268,48,374,0,373,0],[-71,309,93,370,0,373,0],[-29,300,136,357,0,373,0],[13,287,156,361,0,373,0],[23,260,116,351,0,373,0],[24,217,81,329,0,373,0],[36,174,55,297,0,373,0],[9,141,31,265,0,373,0],[-24,111,8,232,0,373,0],[-61,88,8,194,0,373,0],[-105,78,-6,155,0,373,0],[-149,82,-32,123,0,373,0],[-176,49,-65,104,0,373,0],[-193,7,-105,95,0,373,0],[-196,-37,-140,73,0,373,0],[-184,-81,-165,42,0,373,0],[-159,-118,-180,3,0,373,0],[-175,-157,-193,-31,0,373,0],[-197,-194,-209,-68,0,373,0],[-224,-230,-202,-107,0,373,0],[-259,-258,-200,-160,0,373,0],[-268,-300,-206,-219,0,373,0],[-270,-344,-227,-243,0,373,0],[-259,-387,-249,-266,0,373,0],[-232,-423,-261,-304,0,373,0],[-196,-449,-262,-345,0,373,0],[-154,-462,-226,-363,0,373,0],[-111,-462,-193,-372,0,373,0],[-68,-462,-153,-375,0,373,0],[-48,-462,-114,-374,0,373,0],[-43,-462,-113,-374,0,373,0],[-43,-462,-113,-362,0,414,0],[-39,-460,-108,-317,0,414,0],[-25,-427,-99,-272,0,414,0],[-16,-385,-90,-228,0,414,0],[-7,-341,-82,-184,0,414,0],[2,-297,-76,-141,0,414,0],[-33,-279,-100,-145,0,414,0],[-50,-305,-101,-186,0,414,0],[-67,-345,-78,-220,0,414,0],[-94,-380,-65,-259,0,414,0],[-132,-404,-64,-297,0,414,0],[-175,-414,-77,-336,0,414,0],[-220,-412,-104,-369,0,414,0],[-262,-398,-140,-367,0,414,0],[-303,-380,-180,-355,0,414,0],[-344,-360,-220,-342,0,414,0],[-382,-338,-256,-326,0,414,0],[-419,-314,-294,-307,0,414,0],[-434,-299,-317,-289,0,414,0],[-449,-284,-339,-265,0,414,0],[-462,-269,-362,-244,0,414,0],[-462,-260,-360,-239,0,414,0],[-462,-260,-393,-227,0,414,0],[-462,-258,-393,-226,0,414,0],[-462,-252,-390,-226,0,414,0],[-462,-247,-388,-215,0,414,0],[-462,-251,-391,-221,0,414,0],[-462,-247,-390,-222,0,414,0],[-462,-240,-388,-222,0,414,0],[-462,-232,-368,-196,0,414,0],[-462,-239,-383,-193,0,414,0],[-462,-250,-390,-212,0,414,0],[-462,-256,-395,-212,0,414,0],[-455,-278,-395,-212,0,414,0],[-440,-293,-395,-230,0,414,0],[-425,-308,-395,-230,0,414,0],[-410,-323,-380,-250,0,414,0],[-395,-338,-378,-254,0,414,0],[-376,-357,-378,-254,0,414,0],[-358,-375,-357,-277,0,414,0],[-343,-390,-347,-293,0,414,0],[-328,-405,-347,-293,0,414,0],[-308,-423,-334,-297,0,414,0],[-287,-444,-302,-327,0,414,0],[-271,-462,-272,-354,0,414,0],[-256,-462,-262,-359,0,414,0],[-237,-462,-260,-355,0,414,0],[-217,-462,-248,-353,0,414,0],[-193,-462,-248,-353,0,414,0],[-168,-462,-237,-349,0,414,0],[-142,-462,-203,-365,0,414,0],[-115,-462,-175,-377,0,414,0],[-87,-462,-174,-377,0,414,0],[-58,-462,-149,-373,0,414,0],[-29,-462,-109,-374,0,414,0],[-1,-462,-68,-376,0,414,0],[2,-462,-39,-383,0,414,0],[1,-462,-29,-386,0,414,0],[-6,-462,-26,-387,0,414,0],[-18,-462,-15,-386,0,414,0],[-36,-462,-15,-386,0,414,0],[-31,-462,-15,-386,0,414,0],[-20,-462,-9,-386,0,414,0],[-7,-462,0,-386,0,414,0],[6,-462,4,-386,0,414,0],[25,-462,6,-387,0,414,0],[16,-462,21,-360,0,414,0],[-3,-462,29,-349,0,414,0],[-26,-462,21,-346,0,414,0],[-50,-462,-8,-366,0,414,0],[-75,-462,-23,-372,0,414,0],[-102,-462,-23,-360,0,414,0],[-130,-462,-59,-373,0,414,0],[-158,-462,-89,-377,0,414,0],[-186,-462,-134,-373,0,414,0],[-212,-462,-162,-372,0,414,0],[-238,-462,-176,-352,0,414,0],[-256,-456,-182,-326,0,414,0],[-257,-462,-214,-352,0,414,0],[-253,-462,-212,-370,0,414,0],[-249,-462,-195,-356,0,414,0],[-237,-462,-194,-305,0,414,0],[-217,-455,-216,-311,0,414,0],[-198,-462,-228,-351,0,414,0],[-174,-462,-207,-378,0,414,0],[-150,-462,-196,-378,0,414,0],[-123,-462,-196,-378,0,414,0],[-95,-462,-200,-363,0,414,0],[-86,-447,-203,-337,0,414,0],[-87,-433,-198,-313,0,414,0],[-97,-424,-198,-296,0,414,0],[-96,-403,-225,-305,0,414,0],[-95,-381,-243,-314,0,414,0],[-100,-361,-258,-327,0,414,0],[-110,-341,-271,-338,0,414,0],[-120,-323,-282,-318,0,414,0],[-133,-337,-293,-314,0,414,0],[-148,-351,-299,-294,0,414,0],[-163,-357,-301,-273,0,414,0],[-171,-349,-301,-253,0,414,0],[-179,-340,-298,-228,0,414,0],[-173,-324,-258,-225,0,414,0],[-145,-332,-218,-241,0,414,0],[-117,-342,-181,-253,0,414,0],[-90,-354,-166,-281,0,414,0],[-60,-356,-161,-311,0,414,0],[-41,-378,-146,-336,0,414,0],[-23,-402,-130,-362,0,414,0],[-5,-425,-102,-372,0,414,0],[7,-453,-73,-370,0,414,0],[11,-462,-45,-374,0,414,0],[37,-462,-44,-374,0,414,0],[62,-460,-50,-356,0,414,0],[68,-441,-51,-332,0,414,0],[72,-420,-43,-304,0,414,0],[76,-398,-33,-277,0,414,0],[57,-397,-29,-261,0,414,0],[36,-397,-53,-260,0,414,0],[13,-397,-80,-271,0,414,0],[7,-417,-71,-328,0,414,0],[13,-446,-64,-365,0,414,0],[41,-454,-40,-375,0,414,0],[70,-462,-2,-374,0,414,0],[98,-462,35,-373,0,414,0],[125,-462,63,-367,0,414,0],[151,-462,119,-378,0,414,0],[176,-462,124,-378,0,414,0],[202,-462,157,-373,0,414,0],[218,-462,183,-379,0,414,0],[213,-462,183,-379,0,414,0],[207,-462,183,-379,0,414,0],[199,-462,165,-368,0,414,0],[207,-462,139,-353,0,414,0],[225,-455,102,-343,0,414,0],[219,-427,72,-353,0,414,0],[213,-405,56,-368,0,414,0],[206,-386,48,-368,0,414,0],[198,-367,40,-355,0,414,0],[190,-349,31,-343,0,414,0],[182,-337,22,-331,0,414,0],[171,-349,15,-319,0,414,0],[165,-333,7,-305,0,414,0],[157,-313,-2,-280,0,414,0],[149,-293,6,-258,0,414,0],[169,-281,59,-245,0,414,0],[198,-275,91,-231,0,414,0],[215,-298,117,-228,0,414,0],[238,-307,131,-250,0,414,0],[268,-303,146,-268,0,414,0],[295,-291,173,-264,0,414,0],[322,-279,197,-255,0,414,0],[348,-265,224,-246,0,414,0],[375,-250,250,-236,0,414,0],[400,-235,276,-226,0,414,0],[420,-219,288,-214,0,414,0],[441,-201,313,-201,0,414,0],[462,-183,338,-187,0,414,0],[462,-167,361,-172,0,414,0],[462,-148,361,-172,0,414,0],[462,-126,348,-165,0,414,0],[462,-103,361,-147,0,414,0],[462,-77,368,-130,0,414,0]],"events":[[31,"p",0,0,0,"jh",-1],[31,"c",1,43,10,"jh",1],[52,"b",0,26,0,"jh",-1],[58,"c",1,43,10,"jh",1],[64,"c",0,70,17,"sb",-1],[74,"c",1,79,19,"sh",1],[113,"h",1,31,7,"jh",1],[114,"p",0,0,0,"jh",-1],[134,"b",0,26,1,"jh",-1],[145,"b",0,39,2,"sh",-1],[145,"h",1,56,14,"sh",1],[160,"h",0,27,6,"jb",-1],[161,"h",1,31,7,"jh",1],[186,"b",0,57,3,"sh",-1],[197,"c",1,70,17,"sh",1],[213,"h",1,30,7,"jh",1],[238,"b",1,26,0,"jh",1],[241,"b",0,26,1,"jh",-1],[254,"p",1,0,1,"sh",1],[262,"b",0,39,4,"sh",-1],[273,"c",1,69,17,"sh",1],[289,"h",1,30,7,"jh",1],[328,"b",1,26,1,"jh",1],[420,"b",1,26,0,"jh",-1],[453,"h",1,54,13,"sh",-1],[559,"b",1,26,0,"jh",-1],[619,"b",1,26,0,"jh",-1],[630,"b",0,26,1,"jh",1],[641,"h",1,53,13,"sh",-1],[660,"l",1,0,25,"",0],[690,"l",1,0,25,"",0],[720,"l",1,0,25,"",0],[750,"l",1,0,25,"",0],[780,"l",1,0,25,"",0],[792,"b",1,26,0,"jh",1],[802,"b",0,26,1,"jh",-1],[810,"l",1,0,25,"",0],[813,"h",1,58,14,"sh",1],[832,"h",1,30,7,"jh",1],[840,"l",1,0,25,"",0],[868,"b",1,26,0,"jh",-1],[870,"l",1,0,25,"",0],[900,"l",1,0,25,"",0],[930,"l",1,0,25,"",0],[960,"l",1,0,25,"",0],[990,"l",1,0,25,"",0],[1020,"l",1,0,25,"",0],[1050,"h",1,29,7,"jh",1],[1050,"l",1,0,25,"",0],[1053,"b",0,26,1,"jh",-1],[1076,"p",0,0,3,"sb",-1],[1080,"l",1,0,25,"",0],[1084,"c",1,37,10,"jh",1],[1110,"l",1,0,25,"",0],[1140,"l",1,0,25,"",0],[1170,"l",1,0,25,"",0],[1200,"l",1,0,25,"",0],[1205,"b",1,26,1,"jh",1],[1230,"l",1,0,25,"",0],[1260,"l",1,0,25,"",0],[1266,"b",1,26,1,"jh",1],[1290,"l",1,0,25,"",0],[1300,"h",1,29,8,"jh",1],[1320,"l",1,1,25,"",0],[1350,"l",1,1,25,"",0],[1351,"c",1,37,10,"jh",1],[1380,"l",1,1,25,"",0],[1410,"l",1,1,25,"",0],[1440,"l",1,1,25,"",0],[1463,"b",1,26,1,"jh",1],[1470,"l",1,1,25,"",0],[1489,"b",0,39,2,"sh",1],[1498,"c",1,65,17,"sh",-1],[1500,"l",1,1,25,"",0],[1530,"l",1,1,25,"",0],[1560,"l",1,1,25,"",0],[1590,"l",1,1,25,"",0],[1620,"l",1,1,25,"",0],[1650,"l",1,1,25,"",0],[1680,"l",1,1,25,"",0],[1710,"l",1,1,25,"",0],[1740,"l",1,1,25,"",0],[1770,"l",1,1,25,"",0],[1800,"l",1,1,25,"",0],[1804,"b",1,26,1,"jh",-1],[1812,"b",0,26,1,"jh",1],[1824,"p",1,0,2,"sh",-1],[1828,"b",0,26,1,"jh",1],[1830,"l",1,1,25,"",0],[1841,"c",1,36,11,"jh",-1],[1860,"l",1,1,25,"",0],[1890,"l",1,1,25,"",0],[1919,"h",1,28,9,"jh",1],[1920,"l",1,1,25,"",0],[1921,"p",0,0,1,"sh",-1],[1950,"l",1,1,25,"",0],[1980,"l",1,1,25,"",0],[1994,"b",1,26,2,"jh",1],[2005,"b",0,26,1,"jh",-1],[2010,"l",1,1,25,"",0],[2016,"b",0,39,3,"sh",-1],[2017,"h",1,55,15,"sh",1],[2034,"h",1,28,10,"jh",1],[2040,"l",1,2,25,"",0],[2070,"l",1,2,25,"",0],[2100,"l",1,2,25,"",0],[2130,"l",1,2,25,"",0],[2160,"l",1,2,25,"",0],[2190,"l",1,2,25,"",0],[2203,"c",1,36,12,"jh",1],[2220,"l",1,2,25,"",0],[2250,"l",1,2,25,"",0],[2280,"l",1,2,25,"",0],[2302,"h",1,28,10,"jh",-1],[2310,"l",1,3,25,"",0],[2340,"l",1,3,25,"",0],[2370,"l",1,3,25,"",0],[2373,"c",1,62,19,"sh",-1],[2391,"h",1,27,10,"jh",-1],[2400,"l",1,3,25,"",0],[2430,"l",1,3,25,"",0],[2460,"l",1,3,25,"",0],[2490,"l",1,3,25,"",0],[2513,"b",1,26,5,"jh",-1],[2520,"l",1,3,25,"",0],[2550,"l",1,3,25,"",0],[2580,"l",1,3,25,"",0],[2606,"h",1,27,11,"jh",-1],[2610,"l",1,4,25,"",0],[2640,"l",1,4,25,"",0],[2670,"l",1,4,25,"",0],[2673,"c",1,35,13,"jh",1],[2677,"b",0,39,2,"sh",-1],[2691,"p",1,0,6,"sh",1],[2700,"l",1,4,25,"",0],[2730,"l",1,4,25,"",0],[2760,"l",1,4,25,"",0],[2786,"b",1,26,6,"jh",1],[2790,"l",1,4,25,"",0],[2810,"p",0,0,2,"sh",-1],[2812,"c",1,35,14,"jh",1],[2820,"l",1,5,25,"",0],[2850,"l",1,5,25,"",0],[2880,"l",1,5,25,"",0],[2901,"b",1,26,7,"jh",1],[2910,"l",1,5,25,"",0],[2938,"h",1,27,12,"jh",1],[2940,"l",1,5,25,"",0],[2970,"l",1,5,25,"",0],[2974,"b",1,26,7,"jh",1],[3000,"l",1,5,25,"",0],[3011,"b",1,26,7,"jh",1],[3030,"l",1,5,25,"",0],[3047,"b",1,26,7,"jh",1],[3053,"b",0,26,1,"jh",-1],[3060,"l",1,5,25,"",0],[3065,"p",1,0,7,"sh",1],[3069,"b",0,26,2,"jh",-1],[3084,"b",1,26,7,"jh",1],[3090,"l",1,5,25,"",0],[3097,"b",0,39,2,"sh",-1],[3106,"c",1,67,23,"sh",1],[3120,"l",1,6,25,"",0],[3137,"p",1,0,7,"jh",1],[3147,"b",0,39,4,"sh",-1],[3150,"l",1,6,25,"",0],[3156,"c",1,66,24,"sh",1],[3175,"h",1,26,14,"jh",1],[3175,"k",1,1,14,"jh",1],[3480,"l",1,7,25,"",0],[3510,"l",1,7,25,"",0],[3537,"h",1,26,15,"jh",1],[3540,"l",1,7,25,"",0],[3544,"b",0,26,2,"jh",-1],[3567,"b",0,57,5,"sh",-1],[3570,"l",1,7,25,"",0],[3582,"c",1,59,24,"sh",1],[3600,"l",1,8,25,"",0],[3601,"h",1,26,17,"jh",1],[3630,"l",1,8,25,"",0],[3660,"l",1,8,25,"",0],[3690,"l",1,8,25,"",0],[3720,"l",1,8,25,"",0],[3750,"l",1,8,25,"",0],[3772,"h",1,26,17,"jh",1],[3778,"b",0,39,3,"sh",-1],[3780,"l",1,9,25,"",0],[3790,"c",1,65,28,"sh",1],[3809,"h",1,26,19,"jh",1],[3810,"l",1,10,25,"",0],[3840,"l",1,10,25,"",0],[3870,"l",1,10,25,"",0],[3900,"l",1,10,25,"",0],[4380,"l",1,7,25,"",0],[4410,"l",1,7,25,"",0],[4440,"l",1,7,25,"",0],[4445,"b",1,26,10,"jh",1],[4468,"c",1,69,27,"sh",1],[4470,"l",1,8,25,"",0],[4500,"l",1,8,25,"",0],[4530,"l",1,8,25,"",0],[4550,"c",1,69,28,"sh",1],[4560,"l",1,9,25,"",0],[4590,"l",1,9,25,"",0],[4620,"l",1,9,25,"",0],[4650,"l",1,9,25,"",0],[4680,"l",1,9,25,"",0],[4710,"l",1,9,25,"",0],[4730,"b",1,26,12,"jh",-1],[4740,"l",1,9,25,"",0],[4756,"c",1,38,21,"jh",-1],[4770,"l",1,10,25,"",0],[4800,"l",1,10,25,"",0],[4830,"l",1,10,25,"",0],[4860,"l",1,10,25,"",0],[4877,"h",1,30,20,"jh",-1],[4890,"l",1,11,25,"",0],[4920,"l",1,11,25,"",0],[4937,"b",1,26,14,"jh",-1],[4950,"l",1,11,25,"",0],[4972,"h",1,30,21,"jh",-1],[4980,"l",1,11,25,"",0],[5010,"l",1,11,25,"",0],[5020,"h",1,30,22,"jh",-1],[5040,"l",1,12,25,"",0],[5070,"l",1,12,25,"",0],[5093,"b",1,26,16,"jh",1],[5100,"l",1,12,25,"",0],[5130,"l",1,12,25,"",0],[5134,"b",1,26,16,"jh",1],[5141,"b",0,26,1,"jh",1],[5153,"p",1,0,16,"hh",-1],[5160,"b",0,39,3,"sh",1],[5160,"l",1,12,25,"",0],[5176,"p",1,0,16,"sh",-1],[5183,"b",0,39,3,"sh",1],[5190,"l",1,12,25,"",0],[5196,"c",1,67,33,"sh",-1],[5213,"h",1,29,25,"jh",-1],[5220,"l",1,14,25,"",0],[5250,"l",1,14,25,"",0],[5280,"l",1,14,25,"",0],[5310,"l",1,14,25,"",0],[5340,"l",1,14,25,"",0],[5343,"b",1,26,19,"jh",-1],[5370,"l",1,14,25,"",0],[5400,"l",1,14,25,"",0],[5403,"b",1,26,19,"jh",-1],[5430,"l",1,14,25,"",0],[5440,"b",1,26,19,"jh",1],[5460,"l",1,14,25,"",0],[5487,"c",1,37,28,"jh",1],[5490,"l",1,15,25,"",0],[5520,"l",1,15,25,"",0],[5550,"l",1,15,25,"",0],[5580,"l",1,15,25,"",0],[5602,"h",1,29,27,"jh",1],[5610,"l",1,16,25,"",0],[5640,"l",1,16,25,"",0],[5670,"l",1,16,25,"",0],[5680,"b",1,26,21,"jh",1],[5700,"l",1,16,25,"",0],[5710,"b",1,26,21,"jh",1],[5719,"b",0,26,1,"jh",-1],[5730,"l",1,16,25,"",0],[5732,"p",1,0,21,"hh",1],[5739,"b",0,26,1,"jh",-1],[5752,"h",1,29,28,"jh",1],[5760,"l",1,17,25,"",0],[5790,"l",1,17,25,"",0],[5805,"h",1,29,29,"jh",-1],[5820,"l",1,18,25,"",0],[5850,"l",1,18,25,"",0],[5876,"b",1,26,23,"jh",-1],[5880,"l",1,18,25,"",0],[5908,"h",1,28,31,"jh",-1],[5910,"l",1,19,25,"",0],[5915,"h",0,38,9,"sb",1],[5926,"c",1,69,42,"sh",-1],[5940,"l",1,20,25,"",0],[5970,"l",1,20,25,"",0],[6000,"l",1,20,25,"",0],[6030,"l",1,20,25,"",0],[6060,"l",1,20,25,"",0],[6090,"l",1,20,25,"",0],[6120,"l",1,20,25,"",0],[6150,"l",1,20,25,"",0],[6155,"b",1,26,26,"jh",-1],[6169,"h",0,44,11,"sb",1],[6177,"c",1,65,42,"sh",-1],[6180,"l",1,21,25,"",0],[6210,"l",1,21,25,"",0],[6240,"l",1,21,25,"",0],[6270,"l",1,21,25,"",0],[6300,"l",1,21,25,"",0],[6330,"l",1,21,25,"",0],[6360,"l",1,21,25,"",0],[6368,"h",1,26,34,"jh",1],[6390,"l",1,22,25,"",0],[6397,"b",1,26,29,"jh",1],[6420,"l",1,22,25,"",0],[6450,"l",1,22,25,"",0],[6474,"b",1,26,29,"jh",1],[6480,"l",1,22,25,"",0],[6503,"h",1,26,35,"jh",1],[6510,"l",1,23,25,"",0],[6539,"h",1,26,36,"jh",1],[6540,"l",1,24,25,"",0],[6570,"l",1,24,25,"",0],[6575,"b",1,26,31,"jh",1],[6600,"l",1,24,25,"",0],[6614,"b",1,26,31,"jh",1],[6630,"l",1,24,25,"",0],[6655,"b",1,26,31,"jh",1],[6660,"l",1,24,25,"",0],[6690,"l",1,24,25,"",0],[6694,"b",1,26,31,"jh",1],[6720,"l",1,24,25,"",0],[6750,"l",1,24,25,"",0],[6755,"b",1,26,31,"jh",1],[6766,"b",0,57,4,"sh",-1],[6778,"c",1,64,48,"sh",1],[6780,"l",1,25,25,"",0],[6798,"h",1,25,39,"jh",1],[6810,"l",1,27,25,"",0],[6840,"l",1,27,25,"",0],[6870,"l",1,27,25,"",0],[6900,"l",1,27,25,"",0],[6930,"l",1,27,25,"",0],[6960,"l",1,27,25,"",0],[6990,"l",1,27,25,"",0],[7020,"b",1,26,34,"jh",1],[7020,"l",1,27,25,"",0],[7050,"l",1,27,25,"",0],[7080,"l",1,27,25,"",0],[7093,"p",1,0,34,"sh",1],[7110,"l",1,27,25,"",0],[7113,"c",1,32,43,"jh",1],[7117,"b",0,57,4,"sh",-1],[7133,"p",1,0,36,"sh",1],[7140,"l",1,28,25,"",0],[7143,"b",0,39,4,"sh",-1],[7157,"p",1,0,36,"sh",1],[7165,"b",0,39,4,"sh",-1],[7170,"l",1,28,25,"",0],[7200,"b",0,57,7,"sh",1],[7200,"l",1,28,25,"",0],[7213,"p",1,0,36,"sh",-1],[7230,"l",1,28,25,"",0],[7248,"h",1,24,42,"jh",-1],[7260,"l",1,29,25,"",0],[7290,"l",1,29,25,"",0],[7320,"l",1,29,25,"",0],[7321,"h",1,24,44,"jh",-1],[7350,"l",1,30,25,"",0],[7380,"l",1,30,25,"",0],[7390,"b",1,26,39,"jh",-1],[7403,"b",0,39,4,"sh",1],[7410,"l",1,30,25,"",0],[7412,"p",1,0,39,"sh",-1],[7440,"l",1,30,25,"",0],[7470,"l",1,30,25,"",0],[7500,"l",1,30,25,"",0],[7530,"l",1,30,25,"",0],[7560,"l",1,30,25,"",0],[7590,"l",1,30,25,"",0],[7620,"l",1,30,25,"",0],[7624,"b",0,26,1,"jh",1],[7637,"b",0,49,4,"hh",1],[7638,"p",1,0,39,"hh",-1],[7650,"l",1,30,25,"",0],[7663,"b",0,49,4,"hh",1],[7677,"p",1,0,39,"hh",-1],[7680,"l",1,30,25,"",0],[7685,"b",0,26,3,"jh",1],[7698,"p",0,0,1,"sb",1],[7710,"l",1,30,25,"",0],[7740,"b",0,39,4,"sh",1],[7740,"l",1,30,25,"",0],[7770,"l",1,30,25,"",0],[7775,"b",0,26,2,"jh",1],[7787,"b",0,39,5,"sh",1],[7800,"l",1,30,25,"",0],[7830,"l",1,30,25,"",0],[7860,"l",1,30,25,"",0],[7873,"b",0,26,2,"jh",1],[7890,"l",1,30,25,"",0],[7918,"h",0,22,5,"jb",1],[7920,"l",1,30,25,"",0],[7934,"p",0,0,1,"sb",1],[7950,"l",1,30,25,"",0],[8430,"l",1,6,25,"",0],[8460,"l",1,6,25,"",0],[8490,"l",1,6,25,"",0],[8512,"h",1,27,14,"jh",1],[8520,"l",1,6,25,"",0],[8524,"b",0,26,1,"jh",-1],[8535,"p",1,0,9,"hh",1],[8547,"c",0,48,12,"sb",-1],[8550,"l",1,6,25,"",0],[8561,"p",1,0,9,"hh",1],[8570,"c",0,28,7,"jb",-1],[8580,"l",1,6,25,"",0],[8584,"b",1,26,9,"jh",1],[8593,"b",0,26,2,"jh",-1],[8606,"c",1,67,25,"hh",1],[8610,"l",1,7,25,"",0],[8640,"l",1,7,25,"",0],[8670,"l",1,7,25,"",0],[8700,"l",1,7,25,"",0],[8730,"l",1,7,25,"",0],[8760,"l",1,7,25,"",0],[8764,"b",1,26,10,"jh",1],[8789,"b",1,26,10,"jh",1],[8790,"l",1,7,25,"",0],[8799,"b",0,26,1,"jh",-1],[8812,"b",1,49,12,"hh",1],[8820,"l",1,7,25,"",0],[8842,"b",1,26,11,"jh",1],[8850,"l",1,7,25,"",0],[8879,"b",1,26,11,"jh",1],[8880,"l",1,7,25,"",0],[8910,"l",1,7,25,"",0],[8915,"b",1,26,11,"jh",1],[8933,"b",0,26,1,"jh",-1],[8940,"l",1,7,25,"",0],[8946,"b",1,49,13,"hh",1],[8965,"p",0,0,1,"hb",-1],[8970,"l",1,7,25,"",0],[8979,"c",1,70,26,"hh",1],[9000,"l",1,7,25,"",0],[9030,"l",1,7,25,"",0],[9060,"l",1,7,25,"",0],[9090,"l",1,7,25,"",0],[9120,"l",1,7,25,"",0],[9150,"l",1,7,25,"",0],[9180,"l",1,7,25,"",0],[9210,"l",1,7,25,"",0],[9240,"l",1,7,25,"",0],[9270,"l",1,7,25,"",0],[9300,"l",1,7,25,"",0],[9330,"l",1,7,25,"",0],[9360,"l",1,7,25,"",0],[9390,"l",1,7,25,"",0],[9420,"l",1,7,25,"",0],[9450,"l",1,7,25,"",0],[9480,"l",1,7,25,"",0],[9510,"l",1,7,25,"",0],[9540,"l",1,7,25,"",0],[9565,"b",0,57,3,"sh",1],[9570,"l",1,7,25,"",0],[9579,"c",1,69,27,"hh",-1],[9600,"l",1,8,25,"",0],[9630,"l",1,8,25,"",0],[9660,"l",1,8,25,"",0],[9690,"l",1,8,25,"",0],[9720,"l",1,8,25,"",0],[9750,"l",1,8,25,"",0],[9780,"l",1,8,25,"",0],[9810,"l",1,8,25,"",0],[9840,"l",1,8,25,"",0],[9870,"l",1,8,25,"",0],[9890,"b",0,26,1,"jh",-1],[9900,"l",1,8,25,"",0],[9903,"b",1,49,13,"hh",1],[9920,"b",0,26,2,"jh",-1],[9930,"l",1,8,25,"",0],[9932,"b",0,39,3,"sh",-1],[9954,"h",0,21,5,"jb",-1],[9960,"l",1,8,25,"",0],[9981,"h",0,21,5,"jb",-1],[9990,"l",1,8,25,"",0],[9994,"b",0,39,3,"sh",-1],[10020,"l",1,8,25,"",0],[10050,"b",0,26,1,"jh",-1],[10050,"l",1,8,25,"",0],[10070,"p",0,0,1,"sb",-1],[10080,"l",1,8,25,"",0],[10101,"p",0,0,1,"sb",-1],[10110,"l",1,8,25,"",0],[10132,"h",0,21,5,"jb",-1],[10140,"l",1,8,25,"",0],[10145,"b",0,39,3,"sh",-1],[10170,"l",1,8,25,"",0],[10181,"b",0,26,2,"jh",-1],[10194,"b",0,39,4,"sh",-1],[10200,"l",1,8,25,"",0],[10230,"l",1,8,25,"",0],[10259,"p",0,0,1,"sb",-1],[10260,"l",1,8,25,"",0],[10290,"l",1,8,25,"",0],[10292,"b",0,57,5,"sh",1],[10320,"l",1,8,25,"",0],[10350,"l",1,8,25,"",0],[10354,"b",0,39,4,"sh",1],[10380,"l",1,8,25,"",0],[10410,"l",1,8,25,"",0],[10427,"h",0,20,5,"jb",1],[10440,"l",1,8,25,"",0],[10446,"h",0,20,5,"jb",1],[10470,"l",1,8,25,"",0],[10471,"b",0,39,4,"sh",1],[10488,"b",0,49,5,"hh",1],[10500,"l",1,8,25,"",0],[10530,"l",1,8,25,"",0],[10541,"b",0,49,4,"hh",-1],[10560,"l",1,8,25,"",0],[10576,"b",0,26,2,"jh",1],[10590,"l",1,8,25,"",0],[10620,"l",1,8,25,"",0],[10634,"b",0,26,2,"jh",-1],[10650,"l",1,8,25,"",0],[10680,"l",1,8,25,"",0],[10710,"l",1,8,25,"",0],[10740,"l",1,8,25,"",0],[10769,"b",0,26,2,"jh",-1],[10770,"l",1,8,25,"",0],[10800,"l",1,8,25,"",0],[10830,"l",1,8,25,"",0],[10855,"h",0,20,5,"jb",1],[10860,"l",1,8,25,"",0],[10870,"p",0,0,1,"sb",1],[10890,"l",1,8,25,"",0],[10920,"l",1,8,25,"",0],[10950,"l",1,8,25,"",0],[10980,"l",1,8,25,"",0],[11010,"l",1,8,25,"",0],[11040,"l",1,8,25,"",0],[11070,"l",1,8,25,"",0],[11100,"l",1,8,25,"",0],[11130,"l",1,8,25,"",0],[11160,"l",1,8,25,"",0],[11190,"l",1,8,25,"",0],[11220,"l",1,8,25,"",0],[11250,"l",1,8,25,"",0],[11280,"l",1,8,25,"",0],[11310,"l",1,8,25,"",0],[11340,"l",1,8,25,"",0],[11370,"l",1,8,25,"",0],[11400,"l",1,8,25,"",0],[11430,"l",1,8,25,"",0],[11460,"l",1,8,25,"",0],[11465,"b",0,26,1,"jh",1],[11490,"l",1,8,25,"",0],[11510,"h",0,20,5,"jb",1],[11520,"l",1,8,25,"",0],[11524,"b",0,39,3,"sh",1],[11550,"l",1,8,25,"",0],[11580,"l",1,8,25,"",0],[11610,"l",1,8,25,"",0],[11640,"l",1,8,25,"",0],[11670,"l",1,8,25,"",0],[11700,"l",1,8,25,"",0],[11730,"l",1,8,25,"",0],[11760,"l",1,8,25,"",0],[11790,"l",1,8,25,"",0],[11820,"l",1,8,25,"",0],[11850,"l",1,8,25,"",0],[11880,"l",1,8,25,"",0],[11910,"l",1,8,25,"",0],[11940,"l",1,8,25,"",0],[11943,"p",0,0,1,"sb",1],[11970,"l",1,8,25,"",0],[12000,"l",1,8,25,"",0]]} diff --git a/web/hands/src/test/movement-traces.json b/web/hands/src/test/movement-traces.json new file mode 100644 index 00000000..20f19745 --- /dev/null +++ b/web/hands/src/test/movement-traces.json @@ -0,0 +1 @@ +{"a jab thrown holding back":[{"events":[{"action_id":"one@1","actor_id":"one","amount":0,"blood":0,"detail":"left:jab:head","direction":0,"event_id":1,"kind":"punch_start","target_id":null,"tick":1}],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":997,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":1,"maximum_stamina":999,"player_id":"one","poise":600,"queued_actions":0,"stamina":959,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-150,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":1},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":997,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":2,"maximum_stamina":999,"player_id":"one","poise":600,"queued_actions":0,"stamina":960,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-150,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":2},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":997,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":3,"maximum_stamina":999,"player_id":"one","poise":600,"queued_actions":0,"stamina":961,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-150,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":3},{"events":[{"action_id":"one@1","actor_id":"one","amount":18,"blood":0,"detail":"","direction":0,"event_id":2,"kind":"whiff","target_id":"two","tick":4}],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":4,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":944,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-150,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":4},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":5,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":945,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-150,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":5},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":6,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":946,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-150,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":6},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":7,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":947,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-150,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":7},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":8,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":948,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-150,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":8},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":9,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":949,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-150,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":9},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":10,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":950,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-150,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":10},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":11,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":951,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-150,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":11},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":12,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":952,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-150,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":12},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":13,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":955,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-4,"velocity_y":0,"warnings":0,"x":-153,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":13},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":14,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":958,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-5,"velocity_y":0,"warnings":0,"x":-158,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":14},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":15,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":961,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-6,"velocity_y":0,"warnings":0,"x":-164,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":15},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":16,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":964,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-171,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":16},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":17,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":967,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-178,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":17},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":18,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":970,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-185,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":18},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":19,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":973,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-192,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":19},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":20,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":976,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-199,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":20},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":21,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":979,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-206,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":21},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":22,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":982,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-213,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":22},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":23,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":985,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-220,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":23},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":24,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":988,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-226,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":24},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":25,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":991,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-233,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":25},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":26,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":994,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-240,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":26},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":27,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":997,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-247,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":27},{"events":[],"fighter":{"action":"jab","action_active_ticks":2,"action_contact_tick":4,"action_hand":"left","action_id":"one@1","action_key":"jab:left:head:normal","action_power":"normal","action_recovery_ticks":7,"action_start_tick":1,"action_startup_ticks":3,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":28,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-254,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":28},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":29,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-261,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":29},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":30,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-268,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":30},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":31,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-275,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":31},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":32,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-282,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":32},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":33,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-289,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":33},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":34,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-296,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":34},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":35,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-303,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":35},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":36,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-310,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":36},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":37,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-317,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":37},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":38,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-324,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":38},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":39,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-331,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":39},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":40,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-338,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":40},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":41,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-345,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":41},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":42,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-352,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":42},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":43,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-359,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":43},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":44,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-366,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":44},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":995,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":45,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-373,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":45}],"flinched mid-hook while holding back":[{"events":[{"action_id":"two@1","actor_id":"two","amount":0,"blood":0,"detail":"left:jab:head","direction":0,"event_id":1,"kind":"punch_start","target_id":null,"tick":1},{"action_id":"one@1","actor_id":"one","amount":0,"blood":0,"detail":"left:hook:head","direction":0,"event_id":2,"kind":"punch_start","target_id":null,"tick":1}],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":1,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":923,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-50,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":1},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":2,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":924,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-50,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":2},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":3,"maximum_stamina":998,"player_id":"one","poise":600,"queued_actions":0,"stamina":925,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-50,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":3},{"events":[{"action_id":"two@1","actor_id":"two","amount":43,"blood":10,"detail":"jab:head","direction":-1,"event_id":3,"kind":"counter_hit","target_id":"one","tick":4}],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":4,"maximum_stamina":998,"player_id":"one","poise":556,"queued_actions":0,"stamina":926,"stance":"orthodox","stunned_ticks":7,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-50,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":4},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":5,"maximum_stamina":998,"player_id":"one","poise":556,"queued_actions":0,"stamina":927,"stance":"orthodox","stunned_ticks":6,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-3,"velocity_y":0,"warnings":0,"x":-52,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":5},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":6,"maximum_stamina":998,"player_id":"one","poise":556,"queued_actions":0,"stamina":928,"stance":"orthodox","stunned_ticks":5,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-4,"velocity_y":0,"warnings":0,"x":-56,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":6},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":7,"maximum_stamina":998,"player_id":"one","poise":556,"queued_actions":0,"stamina":929,"stance":"orthodox","stunned_ticks":4,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-4,"velocity_y":0,"warnings":0,"x":-60,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":7},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":8,"maximum_stamina":998,"player_id":"one","poise":556,"queued_actions":0,"stamina":930,"stance":"orthodox","stunned_ticks":3,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-5,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":8},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":9,"maximum_stamina":998,"player_id":"one","poise":556,"queued_actions":0,"stamina":931,"stance":"orthodox","stunned_ticks":2,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-5,"velocity_y":0,"warnings":0,"x":-70,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":9},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":10,"maximum_stamina":998,"player_id":"one","poise":556,"queued_actions":0,"stamina":932,"stance":"orthodox","stunned_ticks":1,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-5,"velocity_y":0,"warnings":0,"x":-75,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":10},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":11,"maximum_stamina":998,"player_id":"one","poise":556,"queued_actions":0,"stamina":935,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-6,"velocity_y":0,"warnings":0,"x":-81,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":11},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":12,"maximum_stamina":998,"player_id":"one","poise":557,"queued_actions":0,"stamina":938,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-6,"velocity_y":0,"warnings":0,"x":-87,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":12},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":13,"maximum_stamina":998,"player_id":"one","poise":557,"queued_actions":0,"stamina":941,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-94,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":13},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":14,"maximum_stamina":998,"player_id":"one","poise":558,"queued_actions":0,"stamina":944,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-101,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":14},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":15,"maximum_stamina":998,"player_id":"one","poise":558,"queued_actions":0,"stamina":947,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-108,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":15},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":16,"maximum_stamina":998,"player_id":"one","poise":559,"queued_actions":0,"stamina":950,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-115,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":16},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":17,"maximum_stamina":998,"player_id":"one","poise":559,"queued_actions":0,"stamina":953,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-122,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":17},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":18,"maximum_stamina":998,"player_id":"one","poise":560,"queued_actions":0,"stamina":956,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-129,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":18},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":19,"maximum_stamina":998,"player_id":"one","poise":560,"queued_actions":0,"stamina":959,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-135,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":19},{"events":[],"fighter":{"action":"hook","action_active_ticks":3,"action_contact_tick":null,"action_hand":"left","action_id":"one@1","action_key":"hook:left:head:normal","action_power":"normal","action_recovery_ticks":12,"action_start_tick":1,"action_startup_ticks":7,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":20,"maximum_stamina":998,"player_id":"one","poise":561,"queued_actions":0,"stamina":962,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-142,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":20},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":21,"maximum_stamina":998,"player_id":"one","poise":561,"queued_actions":0,"stamina":965,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-149,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":21},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":22,"maximum_stamina":998,"player_id":"one","poise":562,"queued_actions":0,"stamina":968,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-156,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":22},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":23,"maximum_stamina":998,"player_id":"one","poise":562,"queued_actions":0,"stamina":971,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-163,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":23},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":24,"maximum_stamina":998,"player_id":"one","poise":563,"queued_actions":0,"stamina":974,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-170,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":24},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":25,"maximum_stamina":998,"player_id":"one","poise":563,"queued_actions":0,"stamina":977,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-177,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":25},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":26,"maximum_stamina":998,"player_id":"one","poise":564,"queued_actions":0,"stamina":980,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-184,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":26},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":27,"maximum_stamina":998,"player_id":"one","poise":564,"queued_actions":0,"stamina":983,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-191,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":27},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":28,"maximum_stamina":998,"player_id":"one","poise":565,"queued_actions":0,"stamina":986,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-198,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":28},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":29,"maximum_stamina":998,"player_id":"one","poise":565,"queued_actions":0,"stamina":989,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-205,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":29},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":30,"maximum_stamina":998,"player_id":"one","poise":566,"queued_actions":0,"stamina":992,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-212,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":30},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":31,"maximum_stamina":998,"player_id":"one","poise":566,"queued_actions":0,"stamina":995,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-219,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":31},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":32,"maximum_stamina":998,"player_id":"one","poise":567,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-226,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":32},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":33,"maximum_stamina":998,"player_id":"one","poise":567,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-233,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":33},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":34,"maximum_stamina":998,"player_id":"one","poise":568,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-240,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":34},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":35,"maximum_stamina":998,"player_id":"one","poise":568,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-247,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":35},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":36,"maximum_stamina":998,"player_id":"one","poise":569,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-254,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":36},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":37,"maximum_stamina":998,"player_id":"one","poise":569,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-261,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":37},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":38,"maximum_stamina":998,"player_id":"one","poise":570,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-268,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":38},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":39,"maximum_stamina":998,"player_id":"one","poise":570,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-275,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":39},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":40,"maximum_stamina":998,"player_id":"one","poise":571,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-282,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":40},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":41,"maximum_stamina":998,"player_id":"one","poise":571,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-289,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":41},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":42,"maximum_stamina":998,"player_id":"one","poise":572,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-296,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":42},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":43,"maximum_stamina":998,"player_id":"one","poise":572,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-303,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":43},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":44,"maximum_stamina":998,"player_id":"one","poise":573,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-310,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":44},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":994,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":36,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":45,"maximum_stamina":998,"player_id":"one","poise":573,"queued_actions":0,"stamina":998,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":43,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":30,"swelling":7},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-317,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":45}],"parried while holding back":[{"events":[{"action_id":"one@1","actor_id":"one","amount":0,"blood":0,"detail":"right:straight:head","direction":0,"event_id":1,"kind":"punch_start","target_id":null,"tick":1}],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":null,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":1,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":901,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":1},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":null,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":2,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":902,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":2},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":null,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":3,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":903,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":3},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":null,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":4,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":904,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":4},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":null,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":5,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":905,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":5},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":null,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":6,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":906,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":6},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":null,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":7,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":907,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":7},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":null,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":8,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":908,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":8},{"events":[{"action_id":"one@1","actor_id":"two","amount":0,"blood":0,"detail":"","direction":0,"event_id":2,"kind":"perfect_block","target_id":"one","tick":9},{"action_id":"one@1","actor_id":"two","amount":20,"blood":0,"detail":"","direction":0,"event_id":3,"kind":"parry","target_id":"one","tick":9},{"action_id":"one@1","actor_id":"one","amount":10,"blood":2,"detail":"straight:head","direction":1,"event_id":4,"kind":"hit","target_id":"two","tick":9}],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":9,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":9,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":909,"stance":"orthodox","stunned_ticks":20,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":9},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":9,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":10,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":910,"stance":"orthodox","stunned_ticks":19,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":10},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":9,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":11,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":911,"stance":"orthodox","stunned_ticks":18,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":11},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":9,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":12,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":912,"stance":"orthodox","stunned_ticks":17,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":12},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":9,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":13,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":913,"stance":"orthodox","stunned_ticks":16,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":13},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":9,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":14,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":914,"stance":"orthodox","stunned_ticks":15,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":14},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":9,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":15,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":915,"stance":"orthodox","stunned_ticks":14,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":15},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":9,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":16,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":916,"stance":"orthodox","stunned_ticks":13,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":16},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":9,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":17,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":917,"stance":"orthodox","stunned_ticks":12,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":17},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":9,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":18,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":918,"stance":"orthodox","stunned_ticks":11,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":18},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":9,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":19,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":919,"stance":"orthodox","stunned_ticks":10,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":19},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":9,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":20,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":920,"stance":"orthodox","stunned_ticks":9,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":20},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":9,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":21,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":921,"stance":"orthodox","stunned_ticks":8,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":21},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":9,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":22,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":922,"stance":"orthodox","stunned_ticks":7,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":22},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":9,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":23,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":923,"stance":"orthodox","stunned_ticks":6,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":23},{"events":[],"fighter":{"action":"straight","action_active_ticks":3,"action_contact_tick":9,"action_hand":"right","action_id":"one@1","action_key":"straight:right:head:power","action_power":"power","action_recovery_ticks":13,"action_start_tick":1,"action_startup_ticks":8,"action_target":"head","clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":24,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":924,"stance":"orthodox","stunned_ticks":5,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":24},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":25,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":925,"stance":"orthodox","stunned_ticks":4,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":25},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":26,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":926,"stance":"orthodox","stunned_ticks":3,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":26},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":27,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":927,"stance":"orthodox","stunned_ticks":2,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":27},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":28,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":928,"stance":"orthodox","stunned_ticks":1,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":0,"velocity_y":0,"warnings":0,"x":-65,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":28},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":29,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":931,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-4,"velocity_y":0,"warnings":0,"x":-68,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":29},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":30,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":934,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-5,"velocity_y":0,"warnings":0,"x":-73,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":30},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":31,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":937,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-6,"velocity_y":0,"warnings":0,"x":-79,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":31},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":32,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":940,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-86,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":32},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":33,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":943,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-93,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":33},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":34,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":946,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-100,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":34},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":35,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":949,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-107,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":35},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":36,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":952,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-114,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":36},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":37,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":955,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-121,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":37},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":38,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":958,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-128,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":38},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":39,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":961,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-135,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":39},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":40,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":964,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-141,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":40},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":41,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":967,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-148,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":41},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":42,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":970,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-155,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":42},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":43,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":973,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-162,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":43},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":44,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":976,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-169,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":44},{"events":[],"fighter":{"action":null,"action_active_ticks":0,"action_contact_tick":null,"action_hand":null,"action_id":null,"action_key":null,"action_power":null,"action_recovery_ticks":0,"action_start_tick":0,"action_startup_ticks":0,"action_target":null,"clinch_startup_ticks":0,"clinch_ticks":0,"conditioning":992,"corner_choice":null,"deductions":0,"defense":"none","facing":1,"facing_x":1000,"facing_y":0,"get_up_count":0,"get_up_meter":0,"get_up_prompt":null,"get_up_required":34,"get_up_window_end_tick":0,"get_up_window_start_tick":0,"guard":700,"is_downed":false,"is_foul_recovery_target":false,"knockdowns":0,"last_input_sequence":45,"maximum_stamina":997,"player_id":"one","poise":600,"queued_actions":0,"stamina":979,"stance":"orthodox","stunned_ticks":0,"style":"balanced","taunt_ticks":0,"trauma":{"bleeding":0,"body":0,"head":0,"left_cut":0,"left_eye":0,"right_cut":0,"right_eye":0,"swelling":0},"velocity_x":-7,"velocity_y":0,"warnings":0,"x":-176,"y":0},"held":{"defense":"none","moveX":-1000,"moveY":0},"tick":45}]} diff --git a/web/hands/src/test/style-timing-table.json b/web/hands/src/test/style-timing-table.json new file mode 100644 index 00000000..275aa05b --- /dev/null +++ b/web/hands/src/test/style-timing-table.json @@ -0,0 +1 @@ +["boxer,jhnl,0,0,7,13,42", "boxer,jhnl,0,900,7,13,42", "boxer,jhnl,260,0,5,10,42", "boxer,jhnl,260,900,7,13,42", "boxer,jhnl,553,0,4,8,42", "boxer,jhnl,553,900,7,13,42", "boxer,jhnl,820,0,3,6,42", "boxer,jhnl,820,900,5,9,42", "boxer,jhnl,1000,0,3,6,42", "boxer,jhnl,1000,900,4,8,42", "boxer,jhnr,0,0,8,13,42", "boxer,jhnr,0,900,8,13,42", "boxer,jhnr,260,0,6,10,42", "boxer,jhnr,260,900,8,13,42", "boxer,jhnr,553,0,5,8,42", "boxer,jhnr,553,900,8,13,42", "boxer,jhnr,820,0,4,6,42", "boxer,jhnr,820,900,6,9,42", "boxer,jhnr,1000,0,4,6,42", "boxer,jhnr,1000,900,5,8,42", "boxer,jhpl,0,0,11,19,65", "boxer,jhpl,0,900,11,19,65", "boxer,jhpl,260,0,9,15,65", "boxer,jhpl,260,900,11,19,65", "boxer,jhpl,553,0,7,12,65", "boxer,jhpl,553,900,11,19,65", "boxer,jhpl,820,0,5,10,65", "boxer,jhpl,820,900,8,14,65", "boxer,jhpl,1000,0,5,9,65", "boxer,jhpl,1000,900,7,12,65", "boxer,jhpr,0,0,12,19,65", "boxer,jhpr,0,900,12,19,65", "boxer,jhpr,260,0,10,15,65", "boxer,jhpr,260,900,12,19,65", "boxer,jhpr,553,0,8,12,65", "boxer,jhpr,553,900,12,19,65", "boxer,jhpr,820,0,6,10,65", "boxer,jhpr,820,900,9,14,65", "boxer,jhpr,1000,0,6,9,65", "boxer,jhpr,1000,900,8,12,65", "boxer,jbnl,0,0,9,13,46", "boxer,jbnl,0,900,9,13,46", "boxer,jbnl,260,0,7,10,46", "boxer,jbnl,260,900,9,13,46", "boxer,jbnl,553,0,5,8,46", "boxer,jbnl,553,900,9,13,46", "boxer,jbnl,820,0,4,6,46", "boxer,jbnl,820,900,6,9,46", "boxer,jbnl,1000,0,4,6,46", "boxer,jbnl,1000,900,5,8,46", "boxer,jbnr,0,0,10,13,46", "boxer,jbnr,0,900,10,13,46", "boxer,jbnr,260,0,8,10,46", "boxer,jbnr,260,900,10,13,46", "boxer,jbnr,553,0,6,8,46", "boxer,jbnr,553,900,10,13,46", "boxer,jbnr,820,0,5,6,46", "boxer,jbnr,820,900,7,9,46", "boxer,jbnr,1000,0,5,6,46", "boxer,jbnr,1000,900,6,8,46", "boxer,jbpl,0,0,13,19,71", "boxer,jbpl,0,900,13,19,71", "boxer,jbpl,260,0,10,15,71", "boxer,jbpl,260,900,13,19,71", "boxer,jbpl,553,0,8,12,71", "boxer,jbpl,553,900,13,19,71", "boxer,jbpl,820,0,6,10,71", "boxer,jbpl,820,900,9,14,71", "boxer,jbpl,1000,0,6,9,71", "boxer,jbpl,1000,900,8,12,71", "boxer,jbpr,0,0,14,19,71", "boxer,jbpr,0,900,14,19,71", "boxer,jbpr,260,0,11,15,71", "boxer,jbpr,260,900,14,19,71", "boxer,jbpr,553,0,9,12,71", "boxer,jbpr,553,900,14,19,71", "boxer,jbpr,820,0,7,10,71", "boxer,jbpr,820,900,10,14,71", "boxer,jbpr,1000,0,7,9,71", "boxer,jbpr,1000,900,9,12,71", "boxer,shnl,0,0,12,20,65", "boxer,shnl,0,900,12,20,65", "boxer,shnl,260,0,10,16,65", "boxer,shnl,260,900,12,20,65", "boxer,shnl,553,0,8,13,65", "boxer,shnl,553,900,12,20,65", "boxer,shnl,820,0,6,11,65", "boxer,shnl,820,900,9,15,65", "boxer,shnl,1000,0,6,10,65", "boxer,shnl,1000,900,8,13,65", "boxer,shnr,0,0,12,20,65", "boxer,shnr,0,900,12,20,65", "boxer,shnr,260,0,10,16,65", "boxer,shnr,260,900,12,20,65", "boxer,shnr,553,0,8,13,65", "boxer,shnr,553,900,12,20,65", "boxer,shnr,820,0,6,11,65", "boxer,shnr,820,900,9,15,65", "boxer,shnr,1000,0,6,10,65", "boxer,shnr,1000,900,8,13,65", "boxer,shpl,0,0,16,27,100", "boxer,shpl,0,900,16,27,100", "boxer,shpl,260,0,13,22,100", "boxer,shpl,260,900,16,27,100", "boxer,shpl,553,0,10,17,100", "boxer,shpl,553,900,16,26,100", "boxer,shpl,820,0,8,14,100", "boxer,shpl,820,900,12,20,100", "boxer,shpl,1000,0,8,13,100", "boxer,shpl,1000,900,10,17,100", "boxer,shpr,0,0,16,27,100", "boxer,shpr,0,900,16,27,100", "boxer,shpr,260,0,13,22,100", "boxer,shpr,260,900,16,27,100", "boxer,shpr,553,0,10,17,100", "boxer,shpr,553,900,16,26,100", "boxer,shpr,820,0,8,14,100", "boxer,shpr,820,900,12,20,100", "boxer,shpr,1000,0,8,13,100", "boxer,shpr,1000,900,10,17,100", "boxer,sbnl,0,0,14,20,69", "boxer,sbnl,0,900,14,20,69", "boxer,sbnl,260,0,11,16,69", "boxer,sbnl,260,900,14,20,69", "boxer,sbnl,553,0,9,13,69", "boxer,sbnl,553,900,14,20,69", "boxer,sbnl,820,0,7,11,69", "boxer,sbnl,820,900,10,15,69", "boxer,sbnl,1000,0,7,10,69", "boxer,sbnl,1000,900,9,13,69", "boxer,sbnr,0,0,14,20,69", "boxer,sbnr,0,900,14,20,69", "boxer,sbnr,260,0,11,16,69", "boxer,sbnr,260,900,14,20,69", "boxer,sbnr,553,0,9,13,69", "boxer,sbnr,553,900,14,20,69", "boxer,sbnr,820,0,7,11,69", "boxer,sbnr,820,900,10,15,69", "boxer,sbnr,1000,0,7,10,69", "boxer,sbnr,1000,900,9,13,69", "boxer,sbpl,0,0,18,27,106", "boxer,sbpl,0,900,18,27,106", "boxer,sbpl,260,0,15,22,106", "boxer,sbpl,260,900,18,27,106", "boxer,sbpl,553,0,12,17,106", "boxer,sbpl,553,900,18,26,106", "boxer,sbpl,820,0,10,14,106", "boxer,sbpl,820,900,13,20,106", "boxer,sbpl,1000,0,9,13,106", "boxer,sbpl,1000,900,12,17,106", "boxer,sbpr,0,0,18,27,106", "boxer,sbpr,0,900,18,27,106", "boxer,sbpr,260,0,15,22,106", "boxer,sbpr,260,900,18,27,106", "boxer,sbpr,553,0,12,17,106", "boxer,sbpr,553,900,18,26,106", "boxer,sbpr,820,0,10,14,106", "boxer,sbpr,820,900,13,20,106", "boxer,sbpr,1000,0,9,13,106", "boxer,sbpr,1000,900,12,17,106", "boxer,hhnl,0,0,14,25,78", "boxer,hhnl,0,900,14,25,78", "boxer,hhnl,260,0,11,20,78", "boxer,hhnl,260,900,14,25,78", "boxer,hhnl,553,0,9,16,78", "boxer,hhnl,553,900,14,24,78", "boxer,hhnl,820,0,7,13,78", "boxer,hhnl,820,900,10,18,78", "boxer,hhnl,1000,0,7,12,78", "boxer,hhnl,1000,900,9,16,78", "boxer,hhnr,0,0,14,25,78", "boxer,hhnr,0,900,14,25,78", "boxer,hhnr,260,0,11,20,78", "boxer,hhnr,260,900,14,25,78", "boxer,hhnr,553,0,9,16,78", "boxer,hhnr,553,900,14,24,78", "boxer,hhnr,820,0,7,13,78", "boxer,hhnr,820,900,10,18,78", "boxer,hhnr,1000,0,7,12,78", "boxer,hhnr,1000,900,9,16,78", "boxer,hhpl,0,0,18,31,120", "boxer,hhpl,0,900,18,31,120", "boxer,hhpl,260,0,15,25,120", "boxer,hhpl,260,900,18,31,120", "boxer,hhpl,553,0,12,20,120", "boxer,hhpl,553,900,18,30,120", "boxer,hhpl,820,0,10,16,120", "boxer,hhpl,820,900,13,23,120", "boxer,hhpl,1000,0,9,15,120", "boxer,hhpl,1000,900,12,20,120", "boxer,hhpr,0,0,18,31,120", "boxer,hhpr,0,900,18,31,120", "boxer,hhpr,260,0,15,25,120", "boxer,hhpr,260,900,18,31,120", "boxer,hhpr,553,0,12,20,120", "boxer,hhpr,553,900,18,30,120", "boxer,hhpr,820,0,10,16,120", "boxer,hhpr,820,900,13,23,120", "boxer,hhpr,1000,0,9,15,120", "boxer,hhpr,1000,900,12,20,120", "boxer,hbnl,0,0,16,25,82", "boxer,hbnl,0,900,16,25,82", "boxer,hbnl,260,0,13,20,82", "boxer,hbnl,260,900,16,25,82", "boxer,hbnl,553,0,10,16,82", "boxer,hbnl,553,900,16,24,82", "boxer,hbnl,820,0,8,13,82", "boxer,hbnl,820,900,12,18,82", "boxer,hbnl,1000,0,8,12,82", "boxer,hbnl,1000,900,10,16,82", "boxer,hbnr,0,0,16,25,82", "boxer,hbnr,0,900,16,25,82", "boxer,hbnr,260,0,13,20,82", "boxer,hbnr,260,900,16,25,82", "boxer,hbnr,553,0,10,16,82", "boxer,hbnr,553,900,16,24,82", "boxer,hbnr,820,0,8,13,82", "boxer,hbnr,820,900,12,18,82", "boxer,hbnr,1000,0,8,12,82", "boxer,hbnr,1000,900,10,16,82", "boxer,hbpl,0,0,20,31,127", "boxer,hbpl,0,900,20,31,127", "boxer,hbpl,260,0,16,25,127", "boxer,hbpl,260,900,20,31,127", "boxer,hbpl,553,0,13,20,127", "boxer,hbpl,553,900,20,30,127", "boxer,hbpl,820,0,11,16,127", "boxer,hbpl,820,900,15,23,127", "boxer,hbpl,1000,0,10,15,127", "boxer,hbpl,1000,900,13,20,127", "boxer,hbpr,0,0,20,31,127", "boxer,hbpr,0,900,20,31,127", "boxer,hbpr,260,0,16,25,127", "boxer,hbpr,260,900,20,31,127", "boxer,hbpr,553,0,13,20,127", "boxer,hbpr,553,900,20,30,127", "boxer,hbpr,820,0,11,16,127", "boxer,hbpr,820,900,15,23,127", "boxer,hbpr,1000,0,10,15,127", "boxer,hbpr,1000,900,13,20,127", "boxer,uhnl,0,0,16,27,88", "boxer,uhnl,0,900,16,27,88", "boxer,uhnl,260,0,13,22,88", "boxer,uhnl,260,900,16,27,88", "boxer,uhnl,553,0,10,17,88", "boxer,uhnl,553,900,16,26,88", "boxer,uhnl,820,0,8,14,88", "boxer,uhnl,820,900,12,20,88", "boxer,uhnl,1000,0,8,13,88", "boxer,uhnl,1000,900,10,17,88", "boxer,uhnr,0,0,16,27,88", "boxer,uhnr,0,900,16,27,88", "boxer,uhnr,260,0,13,22,88", "boxer,uhnr,260,900,16,27,88", "boxer,uhnr,553,0,10,17,88", "boxer,uhnr,553,900,16,26,88", "boxer,uhnr,820,0,8,14,88", "boxer,uhnr,820,900,12,20,88", "boxer,uhnr,1000,0,8,13,88", "boxer,uhnr,1000,900,10,17,88", "boxer,uhpl,0,0,20,33,136", "boxer,uhpl,0,900,20,33,136", "boxer,uhpl,260,0,16,27,136", "boxer,uhpl,260,900,20,33,136", "boxer,uhpl,553,0,13,21,136", "boxer,uhpl,553,900,20,32,136", "boxer,uhpl,820,0,11,17,136", "boxer,uhpl,820,900,15,24,136", "boxer,uhpl,1000,0,10,16,136", "boxer,uhpl,1000,900,13,21,136", "boxer,uhpr,0,0,20,33,136", "boxer,uhpr,0,900,20,33,136", "boxer,uhpr,260,0,16,27,136", "boxer,uhpr,260,900,20,33,136", "boxer,uhpr,553,0,13,21,136", "boxer,uhpr,553,900,20,32,136", "boxer,uhpr,820,0,11,17,136", "boxer,uhpr,820,900,15,24,136", "boxer,uhpr,1000,0,10,16,136", "boxer,uhpr,1000,900,13,21,136", "boxer,ubnl,0,0,18,27,92", "boxer,ubnl,0,900,18,27,92", "boxer,ubnl,260,0,15,22,92", "boxer,ubnl,260,900,18,27,92", "boxer,ubnl,553,0,12,17,92", "boxer,ubnl,553,900,18,26,92", "boxer,ubnl,820,0,10,14,92", "boxer,ubnl,820,900,13,20,92", "boxer,ubnl,1000,0,9,13,92", "boxer,ubnl,1000,900,12,17,92", "boxer,ubnr,0,0,18,27,92", "boxer,ubnr,0,900,18,27,92", "boxer,ubnr,260,0,15,22,92", "boxer,ubnr,260,900,18,27,92", "boxer,ubnr,553,0,12,17,92", "boxer,ubnr,553,900,18,26,92", "boxer,ubnr,820,0,10,14,92", "boxer,ubnr,820,900,13,20,92", "boxer,ubnr,1000,0,9,13,92", "boxer,ubnr,1000,900,12,17,92", "boxer,ubpl,0,0,22,33,142", "boxer,ubpl,0,900,22,33,142", "boxer,ubpl,260,0,18,27,142", "boxer,ubpl,260,900,22,33,142", "boxer,ubpl,553,0,14,21,142", "boxer,ubpl,553,900,22,32,142", "boxer,ubpl,820,0,12,17,142", "boxer,ubpl,820,900,16,24,142", "boxer,ubpl,1000,0,11,16,142", "boxer,ubpl,1000,900,14,21,142", "boxer,ubpr,0,0,22,33,142", "boxer,ubpr,0,900,22,33,142", "boxer,ubpr,260,0,18,27,142", "boxer,ubpr,260,900,22,33,142", "boxer,ubpr,553,0,14,21,142", "boxer,ubpr,553,900,22,32,142", "boxer,ubpr,820,0,12,17,142", "boxer,ubpr,820,900,16,24,142", "boxer,ubpr,1000,0,11,16,142", "boxer,ubpr,1000,900,14,21,142", "slugger,jhnl,0,0,7,14,48", "slugger,jhnl,0,900,7,14,48", "slugger,jhnl,260,0,5,11,48", "slugger,jhnl,260,900,7,14,48", "slugger,jhnl,553,0,4,9,48", "slugger,jhnl,553,900,7,14,48", "slugger,jhnl,820,0,3,7,48", "slugger,jhnl,820,900,5,10,48", "slugger,jhnl,1000,0,3,7,48", "slugger,jhnl,1000,900,4,9,48", "slugger,jhnr,0,0,8,14,48", "slugger,jhnr,0,900,8,14,48", "slugger,jhnr,260,0,6,11,48", "slugger,jhnr,260,900,8,14,48", "slugger,jhnr,553,0,5,9,48", "slugger,jhnr,553,900,8,14,48", "slugger,jhnr,820,0,4,7,48", "slugger,jhnr,820,900,6,10,48", "slugger,jhnr,1000,0,4,7,48", "slugger,jhnr,1000,900,5,9,48", "slugger,jhpl,0,0,11,20,74", "slugger,jhpl,0,900,11,20,74", "slugger,jhpl,260,0,9,16,74", "slugger,jhpl,260,900,11,20,74", "slugger,jhpl,553,0,7,13,74", "slugger,jhpl,553,900,11,20,74", "slugger,jhpl,820,0,5,11,74", "slugger,jhpl,820,900,8,15,74", "slugger,jhpl,1000,0,5,10,74", "slugger,jhpl,1000,900,7,13,74", "slugger,jhpr,0,0,12,20,74", "slugger,jhpr,0,900,12,20,74", "slugger,jhpr,260,0,10,16,74", "slugger,jhpr,260,900,12,20,74", "slugger,jhpr,553,0,8,13,74", "slugger,jhpr,553,900,12,20,74", "slugger,jhpr,820,0,6,11,74", "slugger,jhpr,820,900,9,15,74", "slugger,jhpr,1000,0,6,10,74", "slugger,jhpr,1000,900,8,13,74", "slugger,jbnl,0,0,9,14,52", "slugger,jbnl,0,900,9,14,52", "slugger,jbnl,260,0,7,11,52", "slugger,jbnl,260,900,9,14,52", "slugger,jbnl,553,0,5,9,52", "slugger,jbnl,553,900,9,14,52", "slugger,jbnl,820,0,4,7,52", "slugger,jbnl,820,900,6,10,52", "slugger,jbnl,1000,0,4,7,52", "slugger,jbnl,1000,900,5,9,52", "slugger,jbnr,0,0,10,14,52", "slugger,jbnr,0,900,10,14,52", "slugger,jbnr,260,0,8,11,52", "slugger,jbnr,260,900,10,14,52", "slugger,jbnr,553,0,6,9,52", "slugger,jbnr,553,900,10,14,52", "slugger,jbnr,820,0,5,7,52", "slugger,jbnr,820,900,7,10,52", "slugger,jbnr,1000,0,5,7,52", "slugger,jbnr,1000,900,6,9,52", "slugger,jbpl,0,0,13,20,81", "slugger,jbpl,0,900,13,20,81", "slugger,jbpl,260,0,10,16,81", "slugger,jbpl,260,900,13,20,81", "slugger,jbpl,553,0,8,13,81", "slugger,jbpl,553,900,13,20,81", "slugger,jbpl,820,0,6,11,81", "slugger,jbpl,820,900,9,15,81", "slugger,jbpl,1000,0,6,10,81", "slugger,jbpl,1000,900,8,13,81", "slugger,jbpr,0,0,14,20,81", "slugger,jbpr,0,900,14,20,81", "slugger,jbpr,260,0,11,16,81", "slugger,jbpr,260,900,14,20,81", "slugger,jbpr,553,0,9,13,81", "slugger,jbpr,553,900,14,20,81", "slugger,jbpr,820,0,7,11,81", "slugger,jbpr,820,900,10,15,81", "slugger,jbpr,1000,0,7,10,81", "slugger,jbpr,1000,900,9,13,81", "slugger,shnl,0,0,12,20,74", "slugger,shnl,0,900,12,20,74", "slugger,shnl,260,0,10,16,74", "slugger,shnl,260,900,12,20,74", "slugger,shnl,553,0,8,13,74", "slugger,shnl,553,900,12,20,74", "slugger,shnl,820,0,6,11,74", "slugger,shnl,820,900,9,15,74", "slugger,shnl,1000,0,6,10,74", "slugger,shnl,1000,900,8,13,74", "slugger,shnr,0,0,12,20,74", "slugger,shnr,0,900,12,20,74", "slugger,shnr,260,0,10,16,74", "slugger,shnr,260,900,12,20,74", "slugger,shnr,553,0,8,13,74", "slugger,shnr,553,900,12,20,74", "slugger,shnr,820,0,6,11,74", "slugger,shnr,820,900,9,15,74", "slugger,shnr,1000,0,6,10,74", "slugger,shnr,1000,900,8,13,74", "slugger,shpl,0,0,16,27,115", "slugger,shpl,0,900,16,27,115", "slugger,shpl,260,0,13,22,115", "slugger,shpl,260,900,16,27,115", "slugger,shpl,553,0,10,17,115", "slugger,shpl,553,900,16,26,115", "slugger,shpl,820,0,8,14,115", "slugger,shpl,820,900,12,20,115", "slugger,shpl,1000,0,8,13,115", "slugger,shpl,1000,900,10,17,115", "slugger,shpr,0,0,16,27,115", "slugger,shpr,0,900,16,27,115", "slugger,shpr,260,0,13,22,115", "slugger,shpr,260,900,16,27,115", "slugger,shpr,553,0,10,17,115", "slugger,shpr,553,900,16,26,115", "slugger,shpr,820,0,8,14,115", "slugger,shpr,820,900,12,20,115", "slugger,shpr,1000,0,8,13,115", "slugger,shpr,1000,900,10,17,115", "slugger,sbnl,0,0,14,20,79", "slugger,sbnl,0,900,14,20,79", "slugger,sbnl,260,0,11,16,79", "slugger,sbnl,260,900,14,20,79", "slugger,sbnl,553,0,9,13,79", "slugger,sbnl,553,900,14,20,79", "slugger,sbnl,820,0,7,11,79", "slugger,sbnl,820,900,10,15,79", "slugger,sbnl,1000,0,7,10,79", "slugger,sbnl,1000,900,9,13,79", "slugger,sbnr,0,0,14,20,79", "slugger,sbnr,0,900,14,20,79", "slugger,sbnr,260,0,11,16,79", "slugger,sbnr,260,900,14,20,79", "slugger,sbnr,553,0,9,13,79", "slugger,sbnr,553,900,14,20,79", "slugger,sbnr,820,0,7,11,79", "slugger,sbnr,820,900,10,15,79", "slugger,sbnr,1000,0,7,10,79", "slugger,sbnr,1000,900,9,13,79", "slugger,sbpl,0,0,18,27,121", "slugger,sbpl,0,900,18,27,121", "slugger,sbpl,260,0,15,22,121", "slugger,sbpl,260,900,18,27,121", "slugger,sbpl,553,0,12,17,121", "slugger,sbpl,553,900,18,26,121", "slugger,sbpl,820,0,10,14,121", "slugger,sbpl,820,900,13,20,121", "slugger,sbpl,1000,0,9,13,121", "slugger,sbpl,1000,900,12,17,121", "slugger,sbpr,0,0,18,27,121", "slugger,sbpr,0,900,18,27,121", "slugger,sbpr,260,0,15,22,121", "slugger,sbpr,260,900,18,27,121", "slugger,sbpr,553,0,12,17,121", "slugger,sbpr,553,900,18,26,121", "slugger,sbpr,820,0,10,14,121", "slugger,sbpr,820,900,13,20,121", "slugger,sbpr,1000,0,9,13,121", "slugger,sbpr,1000,900,12,17,121", "slugger,hhnl,0,0,14,26,89", "slugger,hhnl,0,900,14,26,89", "slugger,hhnl,260,0,11,21,89", "slugger,hhnl,260,900,14,26,89", "slugger,hhnl,553,0,9,17,89", "slugger,hhnl,553,900,14,25,89", "slugger,hhnl,820,0,7,14,89", "slugger,hhnl,820,900,10,19,89", "slugger,hhnl,1000,0,7,13,89", "slugger,hhnl,1000,900,9,17,89", "slugger,hhnr,0,0,14,26,89", "slugger,hhnr,0,900,14,26,89", "slugger,hhnr,260,0,11,21,89", "slugger,hhnr,260,900,14,26,89", "slugger,hhnr,553,0,9,17,89", "slugger,hhnr,553,900,14,25,89", "slugger,hhnr,820,0,7,14,89", "slugger,hhnr,820,900,10,19,89", "slugger,hhnr,1000,0,7,13,89", "slugger,hhnr,1000,900,9,17,89", "slugger,hhpl,0,0,18,32,138", "slugger,hhpl,0,900,18,32,138", "slugger,hhpl,260,0,15,26,138", "slugger,hhpl,260,900,18,32,138", "slugger,hhpl,553,0,12,21,138", "slugger,hhpl,553,900,18,31,138", "slugger,hhpl,820,0,10,17,138", "slugger,hhpl,820,900,13,24,138", "slugger,hhpl,1000,0,9,16,138", "slugger,hhpl,1000,900,12,21,138", "slugger,hhpr,0,0,18,32,138", "slugger,hhpr,0,900,18,32,138", "slugger,hhpr,260,0,15,26,138", "slugger,hhpr,260,900,18,32,138", "slugger,hhpr,553,0,12,21,138", "slugger,hhpr,553,900,18,31,138", "slugger,hhpr,820,0,10,17,138", "slugger,hhpr,820,900,13,24,138", "slugger,hhpr,1000,0,9,16,138", "slugger,hhpr,1000,900,12,21,138", "slugger,hbnl,0,0,16,26,94", "slugger,hbnl,0,900,16,26,94", "slugger,hbnl,260,0,13,21,94", "slugger,hbnl,260,900,16,26,94", "slugger,hbnl,553,0,10,17,94", "slugger,hbnl,553,900,16,25,94", "slugger,hbnl,820,0,8,14,94", "slugger,hbnl,820,900,12,19,94", "slugger,hbnl,1000,0,8,13,94", "slugger,hbnl,1000,900,10,17,94", "slugger,hbnr,0,0,16,26,94", "slugger,hbnr,0,900,16,26,94", "slugger,hbnr,260,0,13,21,94", "slugger,hbnr,260,900,16,26,94", "slugger,hbnr,553,0,10,17,94", "slugger,hbnr,553,900,16,25,94", "slugger,hbnr,820,0,8,14,94", "slugger,hbnr,820,900,12,19,94", "slugger,hbnr,1000,0,8,13,94", "slugger,hbnr,1000,900,10,17,94", "slugger,hbpl,0,0,20,32,146", "slugger,hbpl,0,900,20,32,146", "slugger,hbpl,260,0,16,26,146", "slugger,hbpl,260,900,20,32,146", "slugger,hbpl,553,0,13,21,146", "slugger,hbpl,553,900,20,31,146", "slugger,hbpl,820,0,11,17,146", "slugger,hbpl,820,900,15,24,146", "slugger,hbpl,1000,0,10,16,146", "slugger,hbpl,1000,900,13,21,146", "slugger,hbpr,0,0,20,32,146", "slugger,hbpr,0,900,20,32,146", "slugger,hbpr,260,0,16,26,146", "slugger,hbpr,260,900,20,32,146", "slugger,hbpr,553,0,13,21,146", "slugger,hbpr,553,900,20,31,146", "slugger,hbpr,820,0,11,17,146", "slugger,hbpr,820,900,15,24,146", "slugger,hbpr,1000,0,10,16,146", "slugger,hbpr,1000,900,13,21,146", "slugger,uhnl,0,0,16,28,101", "slugger,uhnl,0,900,16,28,101", "slugger,uhnl,260,0,13,23,101", "slugger,uhnl,260,900,16,28,101", "slugger,uhnl,553,0,10,18,101", "slugger,uhnl,553,900,16,27,101", "slugger,uhnl,820,0,8,15,101", "slugger,uhnl,820,900,12,21,101", "slugger,uhnl,1000,0,8,14,101", "slugger,uhnl,1000,900,10,18,101", "slugger,uhnr,0,0,16,28,101", "slugger,uhnr,0,900,16,28,101", "slugger,uhnr,260,0,13,23,101", "slugger,uhnr,260,900,16,28,101", "slugger,uhnr,553,0,10,18,101", "slugger,uhnr,553,900,16,27,101", "slugger,uhnr,820,0,8,15,101", "slugger,uhnr,820,900,12,21,101", "slugger,uhnr,1000,0,8,14,101", "slugger,uhnr,1000,900,10,18,101", "slugger,uhpl,0,0,20,34,156", "slugger,uhpl,0,900,20,34,156", "slugger,uhpl,260,0,16,28,156", "slugger,uhpl,260,900,20,34,156", "slugger,uhpl,553,0,13,22,156", "slugger,uhpl,553,900,20,33,156", "slugger,uhpl,820,0,11,18,156", "slugger,uhpl,820,900,15,25,156", "slugger,uhpl,1000,0,10,17,156", "slugger,uhpl,1000,900,13,22,156", "slugger,uhpr,0,0,20,34,156", "slugger,uhpr,0,900,20,34,156", "slugger,uhpr,260,0,16,28,156", "slugger,uhpr,260,900,20,34,156", "slugger,uhpr,553,0,13,22,156", "slugger,uhpr,553,900,20,33,156", "slugger,uhpr,820,0,11,18,156", "slugger,uhpr,820,900,15,25,156", "slugger,uhpr,1000,0,10,17,156", "slugger,uhpr,1000,900,13,22,156", "slugger,ubnl,0,0,18,28,105", "slugger,ubnl,0,900,18,28,105", "slugger,ubnl,260,0,15,23,105", "slugger,ubnl,260,900,18,28,105", "slugger,ubnl,553,0,12,18,105", "slugger,ubnl,553,900,18,27,105", "slugger,ubnl,820,0,10,15,105", "slugger,ubnl,820,900,13,21,105", "slugger,ubnl,1000,0,9,14,105", "slugger,ubnl,1000,900,12,18,105", "slugger,ubnr,0,0,18,28,105", "slugger,ubnr,0,900,18,28,105", "slugger,ubnr,260,0,15,23,105", "slugger,ubnr,260,900,18,28,105", "slugger,ubnr,553,0,12,18,105", "slugger,ubnr,553,900,18,27,105", "slugger,ubnr,820,0,10,15,105", "slugger,ubnr,820,900,13,21,105", "slugger,ubnr,1000,0,9,14,105", "slugger,ubnr,1000,900,12,18,105", "slugger,ubpl,0,0,22,34,163", "slugger,ubpl,0,900,22,34,163", "slugger,ubpl,260,0,18,28,163", "slugger,ubpl,260,900,22,34,163", "slugger,ubpl,553,0,14,22,163", "slugger,ubpl,553,900,22,33,163", "slugger,ubpl,820,0,12,18,163", "slugger,ubpl,820,900,16,25,163", "slugger,ubpl,1000,0,11,17,163", "slugger,ubpl,1000,900,14,22,163", "slugger,ubpr,0,0,22,34,163", "slugger,ubpr,0,900,22,34,163", "slugger,ubpr,260,0,18,28,163", "slugger,ubpr,260,900,22,34,163", "slugger,ubpr,553,0,14,22,163", "slugger,ubpr,553,900,22,33,163", "slugger,ubpr,820,0,12,18,163", "slugger,ubpr,820,900,16,25,163", "slugger,ubpr,1000,0,11,17,163", "slugger,ubpr,1000,900,14,22,163", "swarmer,jhnl,0,0,7,14,42", "swarmer,jhnl,0,900,7,14,42", "swarmer,jhnl,260,0,5,11,42", "swarmer,jhnl,260,900,7,14,42", "swarmer,jhnl,553,0,4,9,42", "swarmer,jhnl,553,900,6,13,42", "swarmer,jhnl,820,0,3,7,42", "swarmer,jhnl,820,900,5,10,42", "swarmer,jhnl,1000,0,3,7,42", "swarmer,jhnl,1000,900,4,9,42", "swarmer,jhnr,0,0,8,14,42", "swarmer,jhnr,0,900,8,14,42", "swarmer,jhnr,260,0,6,11,42", "swarmer,jhnr,260,900,8,14,42", "swarmer,jhnr,553,0,5,9,42", "swarmer,jhnr,553,900,7,13,42", "swarmer,jhnr,820,0,4,7,42", "swarmer,jhnr,820,900,6,10,42", "swarmer,jhnr,1000,0,4,7,42", "swarmer,jhnr,1000,900,5,9,42", "swarmer,jhpl,0,0,11,20,65", "swarmer,jhpl,0,900,11,20,65", "swarmer,jhpl,260,0,9,16,65", "swarmer,jhpl,260,900,11,20,65", "swarmer,jhpl,553,0,7,13,65", "swarmer,jhpl,553,900,11,20,65", "swarmer,jhpl,820,0,5,11,65", "swarmer,jhpl,820,900,8,15,65", "swarmer,jhpl,1000,0,5,10,65", "swarmer,jhpl,1000,900,7,13,65", "swarmer,jhpr,0,0,12,20,65", "swarmer,jhpr,0,900,12,20,65", "swarmer,jhpr,260,0,10,16,65", "swarmer,jhpr,260,900,12,20,65", "swarmer,jhpr,553,0,8,13,65", "swarmer,jhpr,553,900,12,20,65", "swarmer,jhpr,820,0,6,11,65", "swarmer,jhpr,820,900,9,15,65", "swarmer,jhpr,1000,0,6,10,65", "swarmer,jhpr,1000,900,8,13,65", "swarmer,jbnl,0,0,9,14,46", "swarmer,jbnl,0,900,9,14,46", "swarmer,jbnl,260,0,7,11,46", "swarmer,jbnl,260,900,9,14,46", "swarmer,jbnl,553,0,5,9,46", "swarmer,jbnl,553,900,8,13,46", "swarmer,jbnl,820,0,4,7,46", "swarmer,jbnl,820,900,6,10,46", "swarmer,jbnl,1000,0,4,7,46", "swarmer,jbnl,1000,900,5,9,46", "swarmer,jbnr,0,0,10,14,46", "swarmer,jbnr,0,900,10,14,46", "swarmer,jbnr,260,0,8,11,46", "swarmer,jbnr,260,900,10,14,46", "swarmer,jbnr,553,0,6,9,46", "swarmer,jbnr,553,900,9,13,46", "swarmer,jbnr,820,0,5,7,46", "swarmer,jbnr,820,900,7,10,46", "swarmer,jbnr,1000,0,5,7,46", "swarmer,jbnr,1000,900,6,9,46", "swarmer,jbpl,0,0,13,20,71", "swarmer,jbpl,0,900,13,20,71", "swarmer,jbpl,260,0,10,16,71", "swarmer,jbpl,260,900,13,20,71", "swarmer,jbpl,553,0,8,13,71", "swarmer,jbpl,553,900,13,20,71", "swarmer,jbpl,820,0,6,11,71", "swarmer,jbpl,820,900,9,15,71", "swarmer,jbpl,1000,0,6,10,71", "swarmer,jbpl,1000,900,8,13,71", "swarmer,jbpr,0,0,14,20,71", "swarmer,jbpr,0,900,14,20,71", "swarmer,jbpr,260,0,11,16,71", "swarmer,jbpr,260,900,14,20,71", "swarmer,jbpr,553,0,9,13,71", "swarmer,jbpr,553,900,14,20,71", "swarmer,jbpr,820,0,7,11,71", "swarmer,jbpr,820,900,10,15,71", "swarmer,jbpr,1000,0,7,10,71", "swarmer,jbpr,1000,900,9,13,71", "swarmer,shnl,0,0,12,20,65", "swarmer,shnl,0,900,12,20,65", "swarmer,shnl,260,0,10,16,65", "swarmer,shnl,260,900,12,20,65", "swarmer,shnl,553,0,8,13,65", "swarmer,shnl,553,900,12,20,65", "swarmer,shnl,820,0,6,11,65", "swarmer,shnl,820,900,9,15,65", "swarmer,shnl,1000,0,6,10,65", "swarmer,shnl,1000,900,8,13,65", "swarmer,shnr,0,0,12,20,65", "swarmer,shnr,0,900,12,20,65", "swarmer,shnr,260,0,10,16,65", "swarmer,shnr,260,900,12,20,65", "swarmer,shnr,553,0,8,13,65", "swarmer,shnr,553,900,12,20,65", "swarmer,shnr,820,0,6,11,65", "swarmer,shnr,820,900,9,15,65", "swarmer,shnr,1000,0,6,10,65", "swarmer,shnr,1000,900,8,13,65", "swarmer,shpl,0,0,16,27,100", "swarmer,shpl,0,900,16,27,100", "swarmer,shpl,260,0,13,22,100", "swarmer,shpl,260,900,16,27,100", "swarmer,shpl,553,0,10,17,100", "swarmer,shpl,553,900,16,26,100", "swarmer,shpl,820,0,8,14,100", "swarmer,shpl,820,900,12,20,100", "swarmer,shpl,1000,0,8,13,100", "swarmer,shpl,1000,900,10,17,100", "swarmer,shpr,0,0,16,27,100", "swarmer,shpr,0,900,16,27,100", "swarmer,shpr,260,0,13,22,100", "swarmer,shpr,260,900,16,27,100", "swarmer,shpr,553,0,10,17,100", "swarmer,shpr,553,900,16,26,100", "swarmer,shpr,820,0,8,14,100", "swarmer,shpr,820,900,12,20,100", "swarmer,shpr,1000,0,8,13,100", "swarmer,shpr,1000,900,10,17,100", "swarmer,sbnl,0,0,14,20,69", "swarmer,sbnl,0,900,14,20,69", "swarmer,sbnl,260,0,11,16,69", "swarmer,sbnl,260,900,14,20,69", "swarmer,sbnl,553,0,9,13,69", "swarmer,sbnl,553,900,14,20,69", "swarmer,sbnl,820,0,7,11,69", "swarmer,sbnl,820,900,10,15,69", "swarmer,sbnl,1000,0,7,10,69", "swarmer,sbnl,1000,900,9,13,69", "swarmer,sbnr,0,0,14,20,69", "swarmer,sbnr,0,900,14,20,69", "swarmer,sbnr,260,0,11,16,69", "swarmer,sbnr,260,900,14,20,69", "swarmer,sbnr,553,0,9,13,69", "swarmer,sbnr,553,900,14,20,69", "swarmer,sbnr,820,0,7,11,69", "swarmer,sbnr,820,900,10,15,69", "swarmer,sbnr,1000,0,7,10,69", "swarmer,sbnr,1000,900,9,13,69", "swarmer,sbpl,0,0,18,27,106", "swarmer,sbpl,0,900,18,27,106", "swarmer,sbpl,260,0,15,22,106", "swarmer,sbpl,260,900,18,27,106", "swarmer,sbpl,553,0,12,17,106", "swarmer,sbpl,553,900,18,26,106", "swarmer,sbpl,820,0,10,14,106", "swarmer,sbpl,820,900,13,20,106", "swarmer,sbpl,1000,0,9,13,106", "swarmer,sbpl,1000,900,12,17,106", "swarmer,sbpr,0,0,18,27,106", "swarmer,sbpr,0,900,18,27,106", "swarmer,sbpr,260,0,15,22,106", "swarmer,sbpr,260,900,18,27,106", "swarmer,sbpr,553,0,12,17,106", "swarmer,sbpr,553,900,18,26,106", "swarmer,sbpr,820,0,10,14,106", "swarmer,sbpr,820,900,13,20,106", "swarmer,sbpr,1000,0,9,13,106", "swarmer,sbpr,1000,900,12,17,106", "swarmer,hhnl,0,0,13,25,78", "swarmer,hhnl,0,900,13,25,78", "swarmer,hhnl,260,0,10,20,78", "swarmer,hhnl,260,900,13,25,78", "swarmer,hhnl,553,0,8,16,78", "swarmer,hhnl,553,900,13,24,78", "swarmer,hhnl,820,0,6,13,78", "swarmer,hhnl,820,900,9,18,78", "swarmer,hhnl,1000,0,6,12,78", "swarmer,hhnl,1000,900,8,16,78", "swarmer,hhnr,0,0,13,25,78", "swarmer,hhnr,0,900,13,25,78", "swarmer,hhnr,260,0,10,20,78", "swarmer,hhnr,260,900,13,25,78", "swarmer,hhnr,553,0,8,16,78", "swarmer,hhnr,553,900,13,24,78", "swarmer,hhnr,820,0,6,13,78", "swarmer,hhnr,820,900,9,18,78", "swarmer,hhnr,1000,0,6,12,78", "swarmer,hhnr,1000,900,8,16,78", "swarmer,hhpl,0,0,17,31,120", "swarmer,hhpl,0,900,17,31,120", "swarmer,hhpl,260,0,14,25,120", "swarmer,hhpl,260,900,17,31,120", "swarmer,hhpl,553,0,11,20,120", "swarmer,hhpl,553,900,17,30,120", "swarmer,hhpl,820,0,9,16,120", "swarmer,hhpl,820,900,12,23,120", "swarmer,hhpl,1000,0,8,15,120", "swarmer,hhpl,1000,900,11,20,120", "swarmer,hhpr,0,0,17,31,120", "swarmer,hhpr,0,900,17,31,120", "swarmer,hhpr,260,0,14,25,120", "swarmer,hhpr,260,900,17,31,120", "swarmer,hhpr,553,0,11,20,120", "swarmer,hhpr,553,900,17,30,120", "swarmer,hhpr,820,0,9,16,120", "swarmer,hhpr,820,900,12,23,120", "swarmer,hhpr,1000,0,8,15,120", "swarmer,hhpr,1000,900,11,20,120", "swarmer,hbnl,0,0,15,25,82", "swarmer,hbnl,0,900,15,25,82", "swarmer,hbnl,260,0,12,20,82", "swarmer,hbnl,260,900,15,25,82", "swarmer,hbnl,553,0,9,16,82", "swarmer,hbnl,553,900,15,24,82", "swarmer,hbnl,820,0,7,13,82", "swarmer,hbnl,820,900,11,18,82", "swarmer,hbnl,1000,0,7,12,82", "swarmer,hbnl,1000,900,9,16,82", "swarmer,hbnr,0,0,15,25,82", "swarmer,hbnr,0,900,15,25,82", "swarmer,hbnr,260,0,12,20,82", "swarmer,hbnr,260,900,15,25,82", "swarmer,hbnr,553,0,9,16,82", "swarmer,hbnr,553,900,15,24,82", "swarmer,hbnr,820,0,7,13,82", "swarmer,hbnr,820,900,11,18,82", "swarmer,hbnr,1000,0,7,12,82", "swarmer,hbnr,1000,900,9,16,82", "swarmer,hbpl,0,0,19,31,127", "swarmer,hbpl,0,900,19,31,127", "swarmer,hbpl,260,0,15,25,127", "swarmer,hbpl,260,900,19,31,127", "swarmer,hbpl,553,0,12,20,127", "swarmer,hbpl,553,900,19,30,127", "swarmer,hbpl,820,0,10,16,127", "swarmer,hbpl,820,900,14,23,127", "swarmer,hbpl,1000,0,9,15,127", "swarmer,hbpl,1000,900,12,20,127", "swarmer,hbpr,0,0,19,31,127", "swarmer,hbpr,0,900,19,31,127", "swarmer,hbpr,260,0,15,25,127", "swarmer,hbpr,260,900,19,31,127", "swarmer,hbpr,553,0,12,20,127", "swarmer,hbpr,553,900,19,30,127", "swarmer,hbpr,820,0,10,16,127", "swarmer,hbpr,820,900,14,23,127", "swarmer,hbpr,1000,0,9,15,127", "swarmer,hbpr,1000,900,12,20,127", "swarmer,uhnl,0,0,15,27,88", "swarmer,uhnl,0,900,15,27,88", "swarmer,uhnl,260,0,12,22,88", "swarmer,uhnl,260,900,15,27,88", "swarmer,uhnl,553,0,9,17,88", "swarmer,uhnl,553,900,15,26,88", "swarmer,uhnl,820,0,7,14,88", "swarmer,uhnl,820,900,11,20,88", "swarmer,uhnl,1000,0,7,13,88", "swarmer,uhnl,1000,900,9,17,88", "swarmer,uhnr,0,0,15,27,88", "swarmer,uhnr,0,900,15,27,88", "swarmer,uhnr,260,0,12,22,88", "swarmer,uhnr,260,900,15,27,88", "swarmer,uhnr,553,0,9,17,88", "swarmer,uhnr,553,900,15,26,88", "swarmer,uhnr,820,0,7,14,88", "swarmer,uhnr,820,900,11,20,88", "swarmer,uhnr,1000,0,7,13,88", "swarmer,uhnr,1000,900,9,17,88", "swarmer,uhpl,0,0,19,33,136", "swarmer,uhpl,0,900,19,33,136", "swarmer,uhpl,260,0,15,27,136", "swarmer,uhpl,260,900,19,33,136", "swarmer,uhpl,553,0,12,21,136", "swarmer,uhpl,553,900,19,32,136", "swarmer,uhpl,820,0,10,17,136", "swarmer,uhpl,820,900,14,24,136", "swarmer,uhpl,1000,0,9,16,136", "swarmer,uhpl,1000,900,12,21,136", "swarmer,uhpr,0,0,19,33,136", "swarmer,uhpr,0,900,19,33,136", "swarmer,uhpr,260,0,15,27,136", "swarmer,uhpr,260,900,19,33,136", "swarmer,uhpr,553,0,12,21,136", "swarmer,uhpr,553,900,19,32,136", "swarmer,uhpr,820,0,10,17,136", "swarmer,uhpr,820,900,14,24,136", "swarmer,uhpr,1000,0,9,16,136", "swarmer,uhpr,1000,900,12,21,136", "swarmer,ubnl,0,0,17,27,92", "swarmer,ubnl,0,900,17,27,92", "swarmer,ubnl,260,0,14,22,92", "swarmer,ubnl,260,900,17,27,92", "swarmer,ubnl,553,0,11,17,92", "swarmer,ubnl,553,900,17,26,92", "swarmer,ubnl,820,0,9,14,92", "swarmer,ubnl,820,900,12,20,92", "swarmer,ubnl,1000,0,8,13,92", "swarmer,ubnl,1000,900,11,17,92", "swarmer,ubnr,0,0,17,27,92", "swarmer,ubnr,0,900,17,27,92", "swarmer,ubnr,260,0,14,22,92", "swarmer,ubnr,260,900,17,27,92", "swarmer,ubnr,553,0,11,17,92", "swarmer,ubnr,553,900,17,26,92", "swarmer,ubnr,820,0,9,14,92", "swarmer,ubnr,820,900,12,20,92", "swarmer,ubnr,1000,0,8,13,92", "swarmer,ubnr,1000,900,11,17,92", "swarmer,ubpl,0,0,21,33,142", "swarmer,ubpl,0,900,21,33,142", "swarmer,ubpl,260,0,17,27,142", "swarmer,ubpl,260,900,21,33,142", "swarmer,ubpl,553,0,13,21,142", "swarmer,ubpl,553,900,21,32,142", "swarmer,ubpl,820,0,11,17,142", "swarmer,ubpl,820,900,15,24,142", "swarmer,ubpl,1000,0,10,16,142", "swarmer,ubpl,1000,900,13,21,142", "swarmer,ubpr,0,0,21,33,142", "swarmer,ubpr,0,900,21,33,142", "swarmer,ubpr,260,0,17,27,142", "swarmer,ubpr,260,900,21,33,142", "swarmer,ubpr,553,0,13,21,142", "swarmer,ubpr,553,900,21,32,142", "swarmer,ubpr,820,0,11,17,142", "swarmer,ubpr,820,900,15,24,142", "swarmer,ubpr,1000,0,10,16,142", "swarmer,ubpr,1000,900,13,21,142", "counter_puncher,jhnl,0,0,7,14,42", "counter_puncher,jhnl,0,900,7,14,42", "counter_puncher,jhnl,260,0,5,11,42", "counter_puncher,jhnl,260,900,7,14,42", "counter_puncher,jhnl,553,0,4,9,42", "counter_puncher,jhnl,553,900,7,14,42", "counter_puncher,jhnl,820,0,3,7,42", "counter_puncher,jhnl,820,900,5,10,42", "counter_puncher,jhnl,1000,0,3,7,42", "counter_puncher,jhnl,1000,900,4,9,42", "counter_puncher,jhnr,0,0,8,14,42", "counter_puncher,jhnr,0,900,8,14,42", "counter_puncher,jhnr,260,0,6,11,42", "counter_puncher,jhnr,260,900,8,14,42", "counter_puncher,jhnr,553,0,5,9,42", "counter_puncher,jhnr,553,900,8,14,42", "counter_puncher,jhnr,820,0,4,7,42", "counter_puncher,jhnr,820,900,6,10,42", "counter_puncher,jhnr,1000,0,4,7,42", "counter_puncher,jhnr,1000,900,5,9,42", "counter_puncher,jhpl,0,0,11,20,65", "counter_puncher,jhpl,0,900,11,20,65", "counter_puncher,jhpl,260,0,9,16,65", "counter_puncher,jhpl,260,900,11,20,65", "counter_puncher,jhpl,553,0,7,13,65", "counter_puncher,jhpl,553,900,11,20,65", "counter_puncher,jhpl,820,0,5,11,65", "counter_puncher,jhpl,820,900,8,15,65", "counter_puncher,jhpl,1000,0,5,10,65", "counter_puncher,jhpl,1000,900,7,13,65", "counter_puncher,jhpr,0,0,12,20,65", "counter_puncher,jhpr,0,900,12,20,65", "counter_puncher,jhpr,260,0,10,16,65", "counter_puncher,jhpr,260,900,12,20,65", "counter_puncher,jhpr,553,0,8,13,65", "counter_puncher,jhpr,553,900,12,20,65", "counter_puncher,jhpr,820,0,6,11,65", "counter_puncher,jhpr,820,900,9,15,65", "counter_puncher,jhpr,1000,0,6,10,65", "counter_puncher,jhpr,1000,900,8,13,65", "counter_puncher,jbnl,0,0,9,14,46", "counter_puncher,jbnl,0,900,9,14,46", "counter_puncher,jbnl,260,0,7,11,46", "counter_puncher,jbnl,260,900,9,14,46", "counter_puncher,jbnl,553,0,5,9,46", "counter_puncher,jbnl,553,900,9,14,46", "counter_puncher,jbnl,820,0,4,7,46", "counter_puncher,jbnl,820,900,6,10,46", "counter_puncher,jbnl,1000,0,4,7,46", "counter_puncher,jbnl,1000,900,5,9,46", "counter_puncher,jbnr,0,0,10,14,46", "counter_puncher,jbnr,0,900,10,14,46", "counter_puncher,jbnr,260,0,8,11,46", "counter_puncher,jbnr,260,900,10,14,46", "counter_puncher,jbnr,553,0,6,9,46", "counter_puncher,jbnr,553,900,10,14,46", "counter_puncher,jbnr,820,0,5,7,46", "counter_puncher,jbnr,820,900,7,10,46", "counter_puncher,jbnr,1000,0,5,7,46", "counter_puncher,jbnr,1000,900,6,9,46", "counter_puncher,jbpl,0,0,13,20,71", "counter_puncher,jbpl,0,900,13,20,71", "counter_puncher,jbpl,260,0,10,16,71", "counter_puncher,jbpl,260,900,13,20,71", "counter_puncher,jbpl,553,0,8,13,71", "counter_puncher,jbpl,553,900,13,20,71", "counter_puncher,jbpl,820,0,6,11,71", "counter_puncher,jbpl,820,900,9,15,71", "counter_puncher,jbpl,1000,0,6,10,71", "counter_puncher,jbpl,1000,900,8,13,71", "counter_puncher,jbpr,0,0,14,20,71", "counter_puncher,jbpr,0,900,14,20,71", "counter_puncher,jbpr,260,0,11,16,71", "counter_puncher,jbpr,260,900,14,20,71", "counter_puncher,jbpr,553,0,9,13,71", "counter_puncher,jbpr,553,900,14,20,71", "counter_puncher,jbpr,820,0,7,11,71", "counter_puncher,jbpr,820,900,10,15,71", "counter_puncher,jbpr,1000,0,7,10,71", "counter_puncher,jbpr,1000,900,9,13,71", "counter_puncher,shnl,0,0,12,20,65", "counter_puncher,shnl,0,900,12,20,65", "counter_puncher,shnl,260,0,10,16,65", "counter_puncher,shnl,260,900,12,20,65", "counter_puncher,shnl,553,0,8,13,65", "counter_puncher,shnl,553,900,12,20,65", "counter_puncher,shnl,820,0,6,11,65", "counter_puncher,shnl,820,900,9,15,65", "counter_puncher,shnl,1000,0,6,10,65", "counter_puncher,shnl,1000,900,8,13,65", "counter_puncher,shnr,0,0,12,20,65", "counter_puncher,shnr,0,900,12,20,65", "counter_puncher,shnr,260,0,10,16,65", "counter_puncher,shnr,260,900,12,20,65", "counter_puncher,shnr,553,0,8,13,65", "counter_puncher,shnr,553,900,12,20,65", "counter_puncher,shnr,820,0,6,11,65", "counter_puncher,shnr,820,900,9,15,65", "counter_puncher,shnr,1000,0,6,10,65", "counter_puncher,shnr,1000,900,8,13,65", "counter_puncher,shpl,0,0,16,27,100", "counter_puncher,shpl,0,900,16,27,100", "counter_puncher,shpl,260,0,13,22,100", "counter_puncher,shpl,260,900,16,27,100", "counter_puncher,shpl,553,0,10,17,100", "counter_puncher,shpl,553,900,16,26,100", "counter_puncher,shpl,820,0,8,14,100", "counter_puncher,shpl,820,900,12,20,100", "counter_puncher,shpl,1000,0,8,13,100", "counter_puncher,shpl,1000,900,10,17,100", "counter_puncher,shpr,0,0,16,27,100", "counter_puncher,shpr,0,900,16,27,100", "counter_puncher,shpr,260,0,13,22,100", "counter_puncher,shpr,260,900,16,27,100", "counter_puncher,shpr,553,0,10,17,100", "counter_puncher,shpr,553,900,16,26,100", "counter_puncher,shpr,820,0,8,14,100", "counter_puncher,shpr,820,900,12,20,100", "counter_puncher,shpr,1000,0,8,13,100", "counter_puncher,shpr,1000,900,10,17,100", "counter_puncher,sbnl,0,0,14,20,69", "counter_puncher,sbnl,0,900,14,20,69", "counter_puncher,sbnl,260,0,11,16,69", "counter_puncher,sbnl,260,900,14,20,69", "counter_puncher,sbnl,553,0,9,13,69", "counter_puncher,sbnl,553,900,14,20,69", "counter_puncher,sbnl,820,0,7,11,69", "counter_puncher,sbnl,820,900,10,15,69", "counter_puncher,sbnl,1000,0,7,10,69", "counter_puncher,sbnl,1000,900,9,13,69", "counter_puncher,sbnr,0,0,14,20,69", "counter_puncher,sbnr,0,900,14,20,69", "counter_puncher,sbnr,260,0,11,16,69", "counter_puncher,sbnr,260,900,14,20,69", "counter_puncher,sbnr,553,0,9,13,69", "counter_puncher,sbnr,553,900,14,20,69", "counter_puncher,sbnr,820,0,7,11,69", "counter_puncher,sbnr,820,900,10,15,69", "counter_puncher,sbnr,1000,0,7,10,69", "counter_puncher,sbnr,1000,900,9,13,69", "counter_puncher,sbpl,0,0,18,27,106", "counter_puncher,sbpl,0,900,18,27,106", "counter_puncher,sbpl,260,0,15,22,106", "counter_puncher,sbpl,260,900,18,27,106", "counter_puncher,sbpl,553,0,12,17,106", "counter_puncher,sbpl,553,900,18,26,106", "counter_puncher,sbpl,820,0,10,14,106", "counter_puncher,sbpl,820,900,13,20,106", "counter_puncher,sbpl,1000,0,9,13,106", "counter_puncher,sbpl,1000,900,12,17,106", "counter_puncher,sbpr,0,0,18,27,106", "counter_puncher,sbpr,0,900,18,27,106", "counter_puncher,sbpr,260,0,15,22,106", "counter_puncher,sbpr,260,900,18,27,106", "counter_puncher,sbpr,553,0,12,17,106", "counter_puncher,sbpr,553,900,18,26,106", "counter_puncher,sbpr,820,0,10,14,106", "counter_puncher,sbpr,820,900,13,20,106", "counter_puncher,sbpr,1000,0,9,13,106", "counter_puncher,sbpr,1000,900,12,17,106", "counter_puncher,hhnl,0,0,14,25,78", "counter_puncher,hhnl,0,900,14,25,78", "counter_puncher,hhnl,260,0,11,20,78", "counter_puncher,hhnl,260,900,14,25,78", "counter_puncher,hhnl,553,0,9,16,78", "counter_puncher,hhnl,553,900,14,24,78", "counter_puncher,hhnl,820,0,7,13,78", "counter_puncher,hhnl,820,900,10,18,78", "counter_puncher,hhnl,1000,0,7,12,78", "counter_puncher,hhnl,1000,900,9,16,78", "counter_puncher,hhnr,0,0,14,25,78", "counter_puncher,hhnr,0,900,14,25,78", "counter_puncher,hhnr,260,0,11,20,78", "counter_puncher,hhnr,260,900,14,25,78", "counter_puncher,hhnr,553,0,9,16,78", "counter_puncher,hhnr,553,900,14,24,78", "counter_puncher,hhnr,820,0,7,13,78", "counter_puncher,hhnr,820,900,10,18,78", "counter_puncher,hhnr,1000,0,7,12,78", "counter_puncher,hhnr,1000,900,9,16,78", "counter_puncher,hhpl,0,0,18,31,120", "counter_puncher,hhpl,0,900,18,31,120", "counter_puncher,hhpl,260,0,15,25,120", "counter_puncher,hhpl,260,900,18,31,120", "counter_puncher,hhpl,553,0,12,20,120", "counter_puncher,hhpl,553,900,18,30,120", "counter_puncher,hhpl,820,0,10,16,120", "counter_puncher,hhpl,820,900,13,23,120", "counter_puncher,hhpl,1000,0,9,15,120", "counter_puncher,hhpl,1000,900,12,20,120", "counter_puncher,hhpr,0,0,18,31,120", "counter_puncher,hhpr,0,900,18,31,120", "counter_puncher,hhpr,260,0,15,25,120", "counter_puncher,hhpr,260,900,18,31,120", "counter_puncher,hhpr,553,0,12,20,120", "counter_puncher,hhpr,553,900,18,30,120", "counter_puncher,hhpr,820,0,10,16,120", "counter_puncher,hhpr,820,900,13,23,120", "counter_puncher,hhpr,1000,0,9,15,120", "counter_puncher,hhpr,1000,900,12,20,120", "counter_puncher,hbnl,0,0,16,25,82", "counter_puncher,hbnl,0,900,16,25,82", "counter_puncher,hbnl,260,0,13,20,82", "counter_puncher,hbnl,260,900,16,25,82", "counter_puncher,hbnl,553,0,10,16,82", "counter_puncher,hbnl,553,900,16,24,82", "counter_puncher,hbnl,820,0,8,13,82", "counter_puncher,hbnl,820,900,12,18,82", "counter_puncher,hbnl,1000,0,8,12,82", "counter_puncher,hbnl,1000,900,10,16,82", "counter_puncher,hbnr,0,0,16,25,82", "counter_puncher,hbnr,0,900,16,25,82", "counter_puncher,hbnr,260,0,13,20,82", "counter_puncher,hbnr,260,900,16,25,82", "counter_puncher,hbnr,553,0,10,16,82", "counter_puncher,hbnr,553,900,16,24,82", "counter_puncher,hbnr,820,0,8,13,82", "counter_puncher,hbnr,820,900,12,18,82", "counter_puncher,hbnr,1000,0,8,12,82", "counter_puncher,hbnr,1000,900,10,16,82", "counter_puncher,hbpl,0,0,20,31,127", "counter_puncher,hbpl,0,900,20,31,127", "counter_puncher,hbpl,260,0,16,25,127", "counter_puncher,hbpl,260,900,20,31,127", "counter_puncher,hbpl,553,0,13,20,127", "counter_puncher,hbpl,553,900,20,30,127", "counter_puncher,hbpl,820,0,11,16,127", "counter_puncher,hbpl,820,900,15,23,127", "counter_puncher,hbpl,1000,0,10,15,127", "counter_puncher,hbpl,1000,900,13,20,127", "counter_puncher,hbpr,0,0,20,31,127", "counter_puncher,hbpr,0,900,20,31,127", "counter_puncher,hbpr,260,0,16,25,127", "counter_puncher,hbpr,260,900,20,31,127", "counter_puncher,hbpr,553,0,13,20,127", "counter_puncher,hbpr,553,900,20,30,127", "counter_puncher,hbpr,820,0,11,16,127", "counter_puncher,hbpr,820,900,15,23,127", "counter_puncher,hbpr,1000,0,10,15,127", "counter_puncher,hbpr,1000,900,13,20,127", "counter_puncher,uhnl,0,0,16,27,88", "counter_puncher,uhnl,0,900,16,27,88", "counter_puncher,uhnl,260,0,13,22,88", "counter_puncher,uhnl,260,900,16,27,88", "counter_puncher,uhnl,553,0,10,17,88", "counter_puncher,uhnl,553,900,16,26,88", "counter_puncher,uhnl,820,0,8,14,88", "counter_puncher,uhnl,820,900,12,20,88", "counter_puncher,uhnl,1000,0,8,13,88", "counter_puncher,uhnl,1000,900,10,17,88", "counter_puncher,uhnr,0,0,16,27,88", "counter_puncher,uhnr,0,900,16,27,88", "counter_puncher,uhnr,260,0,13,22,88", "counter_puncher,uhnr,260,900,16,27,88", "counter_puncher,uhnr,553,0,10,17,88", "counter_puncher,uhnr,553,900,16,26,88", "counter_puncher,uhnr,820,0,8,14,88", "counter_puncher,uhnr,820,900,12,20,88", "counter_puncher,uhnr,1000,0,8,13,88", "counter_puncher,uhnr,1000,900,10,17,88", "counter_puncher,uhpl,0,0,20,33,136", "counter_puncher,uhpl,0,900,20,33,136", "counter_puncher,uhpl,260,0,16,27,136", "counter_puncher,uhpl,260,900,20,33,136", "counter_puncher,uhpl,553,0,13,21,136", "counter_puncher,uhpl,553,900,20,32,136", "counter_puncher,uhpl,820,0,11,17,136", "counter_puncher,uhpl,820,900,15,24,136", "counter_puncher,uhpl,1000,0,10,16,136", "counter_puncher,uhpl,1000,900,13,21,136", "counter_puncher,uhpr,0,0,20,33,136", "counter_puncher,uhpr,0,900,20,33,136", "counter_puncher,uhpr,260,0,16,27,136", "counter_puncher,uhpr,260,900,20,33,136", "counter_puncher,uhpr,553,0,13,21,136", "counter_puncher,uhpr,553,900,20,32,136", "counter_puncher,uhpr,820,0,11,17,136", "counter_puncher,uhpr,820,900,15,24,136", "counter_puncher,uhpr,1000,0,10,16,136", "counter_puncher,uhpr,1000,900,13,21,136", "counter_puncher,ubnl,0,0,18,27,92", "counter_puncher,ubnl,0,900,18,27,92", "counter_puncher,ubnl,260,0,15,22,92", "counter_puncher,ubnl,260,900,18,27,92", "counter_puncher,ubnl,553,0,12,17,92", "counter_puncher,ubnl,553,900,18,26,92", "counter_puncher,ubnl,820,0,10,14,92", "counter_puncher,ubnl,820,900,13,20,92", "counter_puncher,ubnl,1000,0,9,13,92", "counter_puncher,ubnl,1000,900,12,17,92", "counter_puncher,ubnr,0,0,18,27,92", "counter_puncher,ubnr,0,900,18,27,92", "counter_puncher,ubnr,260,0,15,22,92", "counter_puncher,ubnr,260,900,18,27,92", "counter_puncher,ubnr,553,0,12,17,92", "counter_puncher,ubnr,553,900,18,26,92", "counter_puncher,ubnr,820,0,10,14,92", "counter_puncher,ubnr,820,900,13,20,92", "counter_puncher,ubnr,1000,0,9,13,92", "counter_puncher,ubnr,1000,900,12,17,92", "counter_puncher,ubpl,0,0,22,33,142", "counter_puncher,ubpl,0,900,22,33,142", "counter_puncher,ubpl,260,0,18,27,142", "counter_puncher,ubpl,260,900,22,33,142", "counter_puncher,ubpl,553,0,14,21,142", "counter_puncher,ubpl,553,900,22,32,142", "counter_puncher,ubpl,820,0,12,17,142", "counter_puncher,ubpl,820,900,16,24,142", "counter_puncher,ubpl,1000,0,11,16,142", "counter_puncher,ubpl,1000,900,14,21,142", "counter_puncher,ubpr,0,0,22,33,142", "counter_puncher,ubpr,0,900,22,33,142", "counter_puncher,ubpr,260,0,18,27,142", "counter_puncher,ubpr,260,900,22,33,142", "counter_puncher,ubpr,553,0,14,21,142", "counter_puncher,ubpr,553,900,22,32,142", "counter_puncher,ubpr,820,0,12,17,142", "counter_puncher,ubpr,820,900,16,24,142", "counter_puncher,ubpr,1000,0,11,16,142", "counter_puncher,ubpr,1000,900,14,21,142"] \ No newline at end of file diff --git a/web/hands/src/types.ts b/web/hands/src/types.ts index d08d4b1c..e5e4225a 100644 --- a/web/hands/src/types.ts +++ b/web/hands/src/types.ts @@ -1,4 +1,4 @@ -export const PROTOCOL_VERSION = 3 as const; +export const PROTOCOL_VERSION = 4 as const; export const MAX_SERVER_FRAME_BYTES = 65_536; export type Hand = "left" | "right"; @@ -6,9 +6,13 @@ export type PunchClass = "jab" | "straight" | "hook" | "uppercut"; export type Target = "head" | "body"; export type Power = "normal" | "power"; export type Stance = "orthodox" | "southpaw"; +export type FighterStyle = "balanced" | "boxer" | "slugger" | "swarmer" | "counter_puncher"; export type DefensivePose = "none" | "guard_high" | "guard_low" | "slip_left" | "slip_right" | "weave" | "pull"; export type HeldDefense = "none" | "guard_high" | "guard_low"; -export type MovementKind = "slip_left" | "slip_right" | "weave" | "pull" | "clinch" | "switch_stance" | "get_up_left" | "get_up_right" | "taunt"; +export type CornerKind = "corner_cut" | "corner_swelling" | "corner_breath"; +export type MovementKind = "slip_left" | "slip_right" | "weave" | "pull" | "clinch" | "switch_stance" | "get_up_left" | "get_up_right" | "taunt" | CornerKind; +/** What the corner did between rounds: the fighter's instruction, or the balanced treatment at the bell when none came. */ +export type CornerChoice = "cut" | "swelling" | "breath" | "balanced"; export type ActionKind = "punch" | MovementKind | "foul"; export type Foul = "low_blow" | "headbutt"; export type MatchPhase = "countdown" | "fight" | "knockdown" | "foul_recovery" | "rest" | "complete"; @@ -21,12 +25,15 @@ export interface FoulAction { readonly kind: "foul"; readonly foul: Foul; readon export type SemanticAction = PunchAction | MovementAction | FoulAction; export interface InputFrame { readonly moveX: number; readonly moveY: number; readonly defense: HeldDefense; readonly actions: readonly SemanticAction[] } -export interface PublicPlayer { readonly id: string; readonly name: string; readonly avatar: string | null; readonly rating: number; readonly connected: boolean } +export type CpuLevel = "rookie" | "contender" | "champion"; +/** `cpu` marks the computer opponent, which plays unrated bouts. */ +export interface FighterRecord { readonly wins: number; readonly losses: number; readonly draws: number; readonly knockouts: number } +export interface PublicPlayer { readonly id: string; readonly name: string; readonly avatar: string | null; readonly rating: number; readonly connected: boolean; readonly cpu?: boolean; readonly record?: FighterRecord; readonly style?: FighterStyle } export interface TraumaSnapshot { readonly head: number; readonly body: number; readonly left_eye: number; readonly right_eye: number; readonly left_cut: number; readonly right_cut: number; readonly swelling: number; readonly bleeding: number } export interface FighterSnapshot { readonly player_id: string; readonly x: number; readonly y: number; readonly facing: number; readonly facing_x: number; readonly facing_y: number; - readonly velocity_x: number; readonly velocity_y: number; readonly stance: Stance; readonly defense: DefensivePose; + readonly velocity_x: number; readonly velocity_y: number; readonly stance: Stance; readonly style: FighterStyle; readonly defense: DefensivePose; readonly stamina: number; readonly maximum_stamina: number; readonly conditioning: number; readonly guard: number; readonly poise: number; readonly trauma: TraumaSnapshot; readonly knockdowns: number; readonly warnings: number; readonly deductions: number; readonly stunned_ticks: number; readonly is_downed: boolean; @@ -42,6 +49,7 @@ export interface FighterSnapshot { readonly queued_actions: number; readonly clinch_startup_ticks: number; readonly clinch_ticks: number; readonly is_foul_recovery_target: boolean; readonly taunt_ticks: number; + readonly corner_choice: CornerChoice | null; readonly get_up_prompt: "get_up_left" | "get_up_right" | null; readonly get_up_meter: number; readonly get_up_required: number; readonly get_up_count: number; readonly get_up_window_start_tick: number; readonly get_up_window_end_tick: number; @@ -58,22 +66,31 @@ export interface MatchResult { } export interface EngineSnapshot { readonly tick: number; readonly phase: MatchPhase; readonly round_number: number; readonly phase_ticks_remaining: number; readonly fighters: readonly [FighterSnapshot, FighterSnapshot]; readonly events: readonly CombatEvent[]; readonly result: MatchResult | null; readonly checksum: string } -interface WelcomeBase { readonly version: 3; readonly type: "welcome"; readonly player_id: string; readonly players: readonly PublicPlayer[]; readonly server_tick: number; readonly reconnect_ticket?: string } +interface WelcomeBase { readonly version: typeof PROTOCOL_VERSION; readonly type: "welcome"; readonly player_id: string; readonly players: readonly PublicPlayer[]; readonly server_tick: number; readonly reconnect_ticket?: string } export interface FighterWelcomeMessage extends WelcomeBase { readonly role: "fighter"; readonly seat: 1 | 2; readonly rating: number; readonly next_sequence: number } -export interface SpectatorWelcomeMessage extends WelcomeBase { readonly role: "spectator"; readonly players: readonly [PublicPlayer, PublicPlayer] } +/** A spectator who arrives before the bell can find one corner, or both, still empty. */ +export interface SpectatorWelcomeMessage extends WelcomeBase { readonly role: "spectator" } export type WelcomeMessage = FighterWelcomeMessage | SpectatorWelcomeMessage; -export interface TicketMessage { readonly version: 3; readonly type: "ticket"; readonly reconnect_ticket: string; readonly refresh_id: string } -export interface WaitingMessage { readonly version: 3; readonly type: "waiting"; readonly open_seats: 1 } -export interface ReadyMessage { readonly version: 3; readonly type: "ready"; readonly players: readonly [PublicPlayer, PublicPlayer] } -export interface PausedMessage { readonly version: 3; readonly type: "paused"; readonly player_id: string; readonly grace_ms: number } -export interface ResumedMessage { readonly version: 3; readonly type: "resumed"; readonly player_id: string } -export interface SnapshotMessage { readonly version: 3; readonly type: "snapshot"; readonly payload: EngineSnapshot } +export interface TicketMessage { readonly version: typeof PROTOCOL_VERSION; readonly type: "ticket"; readonly reconnect_ticket: string; readonly refresh_id: string } +/** A fighter waits for an opponent (one open seat); a spectator can be watching both corners fill. */ +export interface WaitingMessage { readonly version: typeof PROTOCOL_VERSION; readonly type: "waiting"; readonly open_seats: 1 | 2 } +/** + * The fighters pick their styles before the bout; `ready` lists who has settled and `connected` who + * is here. Styles are revealed together in the ready: before it a fighter sees only his own, settled. + */ +export interface SelectMessage { readonly version: typeof PROTOCOL_VERSION; readonly type: "select"; readonly deadline_ms: number; readonly players: readonly [PublicPlayer, PublicPlayer]; readonly ready: readonly string[] } +export interface ReadyMessage { readonly version: typeof PROTOCOL_VERSION; readonly type: "ready"; readonly players: readonly [PublicPlayer, PublicPlayer] } +export interface PausedMessage { readonly version: typeof PROTOCOL_VERSION; readonly type: "paused"; readonly player_id: string; readonly grace_ms: number } +export interface ResumedMessage { readonly version: typeof PROTOCOL_VERSION; readonly type: "resumed"; readonly player_id: string } +export interface SnapshotMessage { readonly version: typeof PROTOCOL_VERSION; readonly type: "snapshot"; readonly payload: EngineSnapshot } export interface RatingDelta { readonly before: number; readonly after: number } -export interface FinalMessage { readonly version: 3; readonly type: "final"; readonly match_id: string; readonly winner_id: string | null; readonly method: FinishMethod; readonly round: number; readonly scorecards: readonly JudgeCard[]; readonly ratings: Readonly> } -export interface ErrorMessage { readonly version: 3; readonly type: "error"; readonly code: string } -export type ServerMessage = WelcomeMessage | TicketMessage | WaitingMessage | ReadyMessage | PausedMessage | ResumedMessage | SnapshotMessage | FinalMessage | ErrorMessage; +/** A fighter's punches over the whole bout as the engine counted them; landed means clean, not blocked. */ +export interface PunchTotals { readonly thrown: number; readonly landed: number; readonly jabs_thrown: number; readonly jabs_landed: number } +export interface FinalMessage { readonly version: typeof PROTOCOL_VERSION; readonly type: "final"; readonly match_id: string; readonly winner_id: string | null; readonly method: FinishMethod; readonly round: number; readonly scorecards: readonly JudgeCard[]; readonly ratings: Readonly>; readonly punches?: Readonly> } +export interface ErrorMessage { readonly version: typeof PROTOCOL_VERSION; readonly type: "error"; readonly code: string } +export type ServerMessage = WelcomeMessage | TicketMessage | WaitingMessage | SelectMessage | ReadyMessage | PausedMessage | ResumedMessage | SnapshotMessage | FinalMessage | ErrorMessage; export interface SimulationInfo { readonly tick_rate: number; readonly ring_half_width: number; readonly ring_half_height: number } -export interface BootstrapResponse { readonly client_id: string; readonly state: string; readonly protocol: 3; readonly simulation: SimulationInfo } +export interface BootstrapResponse { readonly client_id: string; readonly state: string; readonly protocol: typeof PROTOCOL_VERSION; readonly simulation: SimulationInfo } export interface TokenPlayer { readonly id: string; readonly name: string; readonly avatar: string | null; readonly rating: number } export interface TokenResponse { readonly access_token: string; readonly ticket: string; readonly player: TokenPlayer }